- src/session/tmux.ts: sync tmux CLI wrappers (available/has/kill), best-effort - config: useTmux from USE_TMUX env (1/0/auto→detect tmux on PATH) - session: when useTmux, spawn 'tmux new-session -A -s web_<id> <shell>' (the node-pty proc is a tmux CLIENT); createSession takes an optional id for re-attach; Session.tmuxName; kill() runs tmux kill-session - manager: handleAttach re-attaches to a surviving 'web_<id>' tmux session after a restart (Case 3.5); shutdown kills only the client pty for tmux sessions so the shell keeps running - tests: USE_TMUX:0 in shared integration cfg (no tmux leakage); unit CFGs useTmux:false; integration 'H1' (real tmux): set var → restart server → re-attach → var survived. 208 tests green.
Web Terminal
A browser-based terminal that exposes the host machine's local shell over WebSocket. Open http://<host-ip>:3000 from any device on your LAN (phone, tablet, another computer) and you get a live, interactive terminal — primarily for vibe coding: hand Claude Code a task, walk away, reconnect from anywhere to check on it.
Sessions survive disconnects: the shell (and whatever's running in it) keeps going when you close the tab; reconnect and the output replays.
⚠️ This hands a full shell to anyone who can reach the port. LAN-only, no authentication. Never port-forward or tunnel it to the public internet. See Security.
Requirements
- Node.js ≥ 18 (developed on v24)
- macOS/Linux. On macOS,
node-ptycompiles a native addon — install Xcode Command Line Tools (xcode-select --install) ifnpm installfails.
Install
npm install # installs deps; postinstall makes node-pty's spawn-helper executable
Run
npm run build:web # bundle the frontend (public/main.ts → public/build/main.js)
npm start # serve on 0.0.0.0:3000
Then:
# find your LAN IP (macOS):
ipconfig getifaddr en0
# open http://<that-ip>:3000 on any device on the same network
For frontend development, run npm run dev:web (esbuild --watch) alongside npm start.
Configuration
All via environment variables (no hardcoding):
| Var | Default | Meaning |
|---|---|---|
PORT |
3000 |
listen port |
BIND_HOST |
0.0.0.0 |
listen address |
SHELL_PATH |
$SHELL or /bin/zsh |
shell to spawn |
IDLE_TTL |
86400 (s) |
reclaim a detached session after this idle time |
SCROLLBACK_BYTES |
2097152 |
per-session replay ring buffer (bytes) |
MAX_PAYLOAD_BYTES |
1048576 |
max single WS frame |
ALLOWED_ORIGINS |
(derived) | extra allowed WS origins, comma-separated |
allowedOrigins is derived from the host's network-interface IPs (plus localhost and anything in ALLOWED_ORIGINS) — never from BIND_HOST, since 0.0.0.0 is never a real browser Origin.
Security
This is a no-auth, LAN-only tool by design. The defenses that matter:
- Origin check (cannot be skipped): the WS handshake rejects any
Originnot on the allow-list (HTTP 401). This blocks Cross-Site WebSocket Hijacking — a malicious page in your browser trying to connect tows://<your-lan-ip>:3000. - Path-scoped upgrades: only
/termis accepted for WS upgrade. - Frame size cap: oversized frames are rejected (
MAX_PAYLOAD_BYTES). - Never expose to the public internet.
ws://is unencrypted — on an untrusted network (café/office Wi-Fi) traffic (including keystrokes: passwords, API keys) can be sniffed. The recommended deployment is Tailscale (WireGuard-encrypted), which also lets you usewss://(the frontend auto-selectswsson HTTPS).
Development
npm test # vitest, all modules
npm run typecheck # tsc (backend + frontend)
npm run build # compile backend to dist/
Real-PTY end-to-end tests (test/integration/) auto-skip where posix_spawn is unavailable (e.g. sandboxes) and run everywhere else.
How it works
The server is a byte-shuttle, not a terminal: node-pty provides a pseudo-terminal so the shell believes it has a real TTY, and xterm.js in the browser interprets the ANSI bytes and renders. The server never parses terminal semantics. PTY lifecycle is decoupled from the WebSocket — a disconnect detaches (the PTY keeps running) rather than killing it, which is what makes session survival work.
Design and rationale: docs/TECH_DOC.md (why) and docs/ARCHITECTURE.md (how).