localterm
Your terminal should just be a browser tab.
Run npx @monotykamary/localterm@latest start and every browser tab is one shell. Open a new tab to spawn another. Close a tab and its shell waits in the session switcher (top-right) for a short grace window — switch back to it in that window, or it's reaped. That's the whole product.

Install
Run this command anywhere:
npx @monotykamary/localterm@latest startThis boots a local daemon and opens a browser tab. The URL depends on what's
installed on the machine — localterm status shows the active one:
| Surface | URL | Requires |
|---|---|---|
| tailnet | https://<your-node>.ts.net |
Tailscale connected, HTTPS certs enabled for the tailnet |
| local | https://localterm.localhost |
portless (workspace dev dep) — installed via localterm install |
| loopback | http://localterm.localhost:3417 |
nothing — .localhost resolves to 127.0.0.1 via RFC 6761, no /etc/hosts edit needed |
To install globally:
npm install -g @monotykamary/localterm
localterm startUsage
The mental model is shell = browser tab, but a tab is just a view onto a shell that outlives it for a grace window:
- New tab → new shell (one authority spawns it)
- Close tab → the shell detaches and waits (dormant) in the session switcher (top-right) for ~30s; reattach in that window (from this tab or another joining alongside) or it's reaped — no zombies. A dormant shell that's still producing output (a build, a long command), or still running a foreground program even when quiet (a
sleep, a paused build), is kept alive, so a closed tab never kills a running command mid-stream. - Reload tab → fresh shell for this tab (the prior one waits in the switcher like any closed tab)
- Switch → the session switcher re-points this tab at any live shell; the one you left detaches and waits its grace window
Transient connection drops silently reattach to the same shell (auto-reconnect is built in for transport failures). A shell nobody is viewing is reaped once it's truly idle — a shell still producing output, or still running a foreground program even when quiet, is kept alive even with no viewers, and only an idle one dies within the grace window. Kill the ones you're done with sooner from the switcher. If you want a shell that survives a full page reload in the same tab, run tmux inside localterm.
Sessions
The session switcher (top-right, or ⌘/Ctrl+I) lists every live shell — the one this tab is viewing, others attached in different tabs, and dormant ones waiting out their grace window. Each row's terminal icon is colored by activity, matching the tab favicon: green while output is streaming, blue while a foreground program runs quietly, grey when idle at the prompt. Click a row to switch this tab onto that shell; hover a row to kill it. Search by title, path, or shell. It's also in the command palette (⌘/Ctrl+K → Sessions).
CLI
localterm start [-p 3417] [-H 127.0.0.1] [--open] # daemonizes by default
localterm stop
localterm status
localterm restart
localterm install [-p 3417] [-H 127.0.0.1] # auto-start at login (macOS)
localterm uninstall # remove auto-start service
localterm secret list # per-program secrets (names + policy; never values)
localterm secret get <name> # print a value (resolved from Keychain, not the daemon)
localterm secret set <name> -e <VAR> [-p a,b] [-v <value>|-] # -v - reads stdin
localterm secret delete <name>State lives in ~/.localterm/ (PID, port, server log at ~/.localterm/server.log).
Auto-start (macOS)
localterm install creates a launchd plist in ~/Library/LaunchAgents/ with RunAtLoad and KeepAlive enabled:
- RunAtLoad — localterm starts automatically when you log in.
- KeepAlive — launchd restarts the daemon immediately if it crashes.
The same command also configures the optional URL surfaces (best-effort, with actionable hints when a prerequisite is missing):
- portless — installs a root-owned launchd proxy on
:443so the daemon is reachable athttps://localterm.localhost(HTTPS, no port). Skipped with an install command ifportlessisn't on PATH. - Tailscale — runs
tailscale serve --bg --https 443 localhost:<port>so the daemon is reachable on your tailnet athttps://<node>.ts.netwith a real Let's Encrypt cert. Skipped with a hint if Tailscale isn't installed, the node is offline, or HTTPS certificates aren't enabled on the tailnet (enable them at https://login.tailscale.com/admin/settings/features, then re-runlocalterm install).
One-time setup:
npx @monotykamary/localterm@latest install
# or with a global install:
localterm installRemove with localterm uninstall (also tears down the Tailscale serve rule).
Dev server (workspace contributors)
pnpm dev runs the terminal's Vite dev server through portless at
https://dev.localterm.localhost (the daemon's https://localterm.localhost
hostname is reserved for the built daemon). The two tsc --watch packages keep
running in parallel via turbo. Escape hatch: pnpm dev:app runs vp dev
without portless.
Automations
Schedule commands as server-managed jobs. When one is due, localterm opens a new browser tab in the automation's directory and runs the command in a fresh shell — the tab stays open afterwards so you can see that it ran and whether it succeeded. The tab opens in the background so a scheduled run never steals your focus (via the DevTools Protocol over a connection opened once at start when a Chromium browser has remote debugging on, otherwise the OS opener / macOS open -g; set LOCALTERM_DISABLE_CDP_TABS=1 to force the fallback).
Enable remote debugging by launching your browser with --remote-debugging-port=9222 (e.g. open -na "Google Chrome" --args --remote-debugging-port=9222); localterm picks it up automatically on the next run. localterm status shows whether the daemon is currently connected via CDP, and localterm install checks for a debug-enabled browser as part of its setup checklist.
- Open the full-screen panel from the top-right toolbar (calendar icon) or with ⌘J / Ctrl+J.
- Build schedules from friendly presets — daily, weekdays/weekends, specific days, multiple times a day, every N minutes/hours — with raw 5-field cron available as an advanced escape hatch. Evaluated in local time.
- Or trigger on a folder change instead of a schedule — the job runs when its directory changes, detected via native filesystem events (no polling). Bursts are debounced into one run and a new run won't start while a previous one is still going, so a command that writes into the watched folder won't loop.
- Cap a job with a run limit ("stop after N runs"); when reached it's marked finished and stays listed until you reset it. Or let it run forever.
- Toggle Close tab when finished to have a run's tab close once its command exits (needs the CDP background-tab path; the toggle is locked off until a debug-enabled Chromium is connected). Off by default — tabs stay open so you can see what ran.
- A recent runs view and a per-automation history show which runs succeeded, failed, were missed, or were skipped because the machine was asleep at that scheduled time (reconstructed when the daemon next starts).
- Definitions persist in
~/.localterm/automations.json(auto-migrated from older versions; a sibling~/.localterm/daemon-heartbeat.jsonrecords liveness for downtime detection); the daemon must be running for jobs to fire. - Everything is also available over HTTP at
/api/automations(list/create/update/delete/run-now/reset).
Agents can manage automations too — install the API playbook as a skill with skills:
npx skills add monotykamary/localtermSecurity
- By default, binds loopback (
127.0.0.1) and enforces loopbackHost/Originheaders to defeat DNS-rebinding and cross-origin attacks. - To share the daemon across machines, prefer
localterm install's Tailscale step — it surfaces the daemon on your tailnet athttps://<node>.ts.netover a real Let's Encrypt cert, on an end-to-end-encrypted WireGuard mesh, with no port exposed to the public internet. - Pass
-H 0.0.0.0(or any non-loopback address) to expose the server on all network interfaces. In this mode,Host/Originmust be from a private network (RFC 1918, CGNAT/Tailscale100.64.127.x, link-local,*.localhost) and WebSocket source IPs are filtered to private ranges — only use on trusted networks. - One PTY per WebSocket. Closing a tab detaches — the shell waits in the session switcher for a grace window (~30s) and is reaped if nobody reattaches; kill it sooner from the switcher.
Resources & Contributing Back
Looking to contribute back? Check out the Contributing Guide and AGENTS.md for code style.
Find a bug? Head over to our issue tracker and we'll do our best to help. We love pull requests, too!
→ Start contributing on GitHub
License
localterm is MIT-licensed open-source software.