# claudete > A control plane for **Claude Code**. Open, drive, and bulk-manage many Claude Code instances from a single page — in the browser or as a native macOS app. Each tile is the *real* Claude Code terminal (TUI and all), streamed live over a WebSocket from a PTY on the server. Type a prompt once and fan it out to every instance. Tiles can also be plain shells, a Monaco file editor, a git-diff browser, or a to-do list, and they group into named tabs so a big fleet stays organized. claudete spawns `claude` (and shells) inside pseudo-terminals (PTYs) via [`node-pty`](https://github.com/microsoft/node-pty) on a Node server, streams the PTY bytes over a WebSocket to the browser, and renders them with [`xterm.js`](https://xtermjs.org) — a full terminal emulator. Keystrokes travel back through the WebSocket into the PTY, giving a genuine interactive Claude Code shell in a browser tile. Multiply by N PTYs and add a fan-out (broadcast) layer and you get bulk control of a fleet. The desktop app (Electron) runs the same server inside the app on a private loopback port behind a per-launch auth token. - Current version: **0.1.13** (see [Changelog](#changelog) for history) - Update/download feed: **https://updates.claudete.co** (generic `electron-updater` provider; signed + notarized macOS builds) - Stack: React + xterm.js + Monaco (client) · Express + ws + node-pty (server) · Electron (desktop) · TypeScript throughout ## Architecture ``` Browser / Electron window (React + xterm.js) ──WebSocket──► Node server ──node-pty──► claude (PTY) × N ▲ grid of live terminals REST API ──node-pty──► shell (PTY) × M ▲ bulk prompt / control keys (file-editor / git-diff / todo tiles run no process) ``` - The browser holds a grid of live terminals and a bulk prompt/control layer. - The Node server exposes a REST API (session lifecycle, fs, git, todo, usage) and a single WebSocket (`/ws`) for terminal I/O. - `claude` and `shell` tiles each back a PTY; `file-editor`, `git-diff`, and `todo` tiles are **process-less** (no PTY) — they're lightweight tools that stay "running" until removed and are never broadcast/interrupt targets. - The desktop app wraps the same server as a child process, binds it to loopback, mints a random per-launch token, and loads the UI at `http://127.0.0.1:`. ## Requirements - Node.js 18+ (tested on 22). - The `claude` CLI installed and authenticated (`claude --version` must work). claudete spawns it; the desktop app can install + sign you in. - A C/C++ toolchain + Python to build `node-pty`'s native addon (Xcode CLT on macOS; build-essential + python3 on Linux). The project `.npmrc` pins `python=/usr/bin/python3` because `node-gyp`'s bundled `gyp` needs `distutils` (removed in Python 3.12+). ## Run modes - **Dev:** `npm install` then `npm run dev` → server on `:4000`, Vite client on `:5173` (Vite proxies `/api` and `/ws`). Open http://localhost:5173. - **Production (single process):** `npm run build` then `npm start` → server serves API + WS + built UI on `:4000`. Open http://localhost:4000. - **Desktop app (macOS):** `npm run app` builds everything and launches the Electron app; `npm run app:dist` builds a signed, notarized `.dmg` + `.zip` into `release/`; `npm run app:dist:release` runs the full signed release pipeline (`scripts/dist.sh`); `npm run publish` uploads to R2 (`scripts/publish.sh`). ## Instance (tile) types Click the **+ New instance** tile and pick a type. Five kinds exist (`SessionKind` = `claude | shell | file-editor | git-diff | todo`): - **Claude Code** (`claude`) — the live Claude Code TUI in a PTY. Configure **working directory**, **model** (Opus / Sonnet / Haiku, or inherit default), **effort** (low / medium / high / xhigh / max), an optional **initial prompt** typed into the TUI after boot, and a **count** to spin up many identical instances at once (bulk spin-up). Toggles: - **Trust folder automatically** — auto-confirms Claude Code's "Is this a project you trust?" dialog so the instance boots straight to the prompt. - **Hide status line** — blanks the status line for this session only (a no-op `statusLine` override; your `settings.json` is untouched). - **⚠ Skip permissions** — passes `--dangerously-skip-permissions`. - **Extra CLI args** — passes anything else through verbatim; a live command preview shows the exact invocation. - **Shell** (`shell`) — a plain terminal session in a PTY. Pick the shell (zsh / bash / fish / sh or any path) and an optional command to run on launch (re-run on every relaunch). - **File editor** (`file-editor`) — a process-less file browser + editor ([Monaco](https://microsoft.github.io/monaco-editor/)) scoped to a folder: browse the tree, create / rename / delete files and folders, edit with syntax highlighting, save with **⌘S**, **reveal in Finder** (desktop app). New file / New folder / Reveal live under a single **⋮** menu. Optional **auto-refresh** re-reads the folder every few seconds so outside changes appear on their own. Resizable split between tree and editor (drag the divider; double-click to reset; width remembered per tile). Per-tile **editor display prefs** in settings — **word wrap**, **minimap**, and **font size** — applied live and remembered per instance. - **Git diff** (`git-diff`) — a process-less changes browser for a git repo: shows branch + ahead/behind vs upstream, lists every changed file with its porcelain status (staged / unstaged / untracked / deleted), shows a HEAD-vs-working-tree **diff** (Monaco), **stage / unstage** files, **discard** (restore) a file's changes, then **commit** (with message) and **push** — all scoped to the folder. Commit stages everything by default, or commits just your staged files when you've staged a subset. Optional **auto-refresh** updates the diff in place without flashing "Loading…". Resizable split (remembered per tile). - **To-do** (`todo`) — a process-less checklist: add tasks, check them off, delete them, clear out completed ones. Saved automatically and kept per instance (persisted server-side); included in backups. New tiles inherit their tab's **default working directory**, falling back to the app-wide default when unset. ## Tabs & groups - Tiles live under named **group tabs** across the top. **+** adds a tab. - **Double-click** a tab (or click its ✎) to rename it and set a **default working directory** for instances launched into it. - **Drag** tabs to reorder (live drop marker); **drag a tile onto a tab** to move it there. - Column count and tile density (**Comfortable / Compact / Overview**) are remembered **per tab**. - A background tab flashes an attention dot when one of its instances finishes a turn; it clears when you switch to that tab. ## Driving & managing instances - **Click a tile** and type — it's the live TUI (or shell). The **maximize** button opens a focused full-window view (Esc to exit). - **Rename** anytime: double-click a title (sidebar or tile), or click ✎. Renames persist. - **Resume (↻)** relaunches a stopped/exited Claude session and **continues its conversation** (`claude --resume `); a **New** action starts a fresh conversation in the same directory. Shells resume into their directory and re-run their startup command. - **Stop / Kill** (per-tile ✕, or the bulk bar) tear instances down; stopped instances stay resumable. Closing a process-less tile (File Editor / Git Diff / Todo) closes immediately with no "Kill this instance?" confirmation. - The **sidebar** searches by title/working-dir and filters by status (**All / Running / Stopped / Exited**); its header shows the app version. ## Broadcast & bulk actions - **▷ Broadcast** (top bar, or `B`) opens a modal to send one prompt to all **selected** instances — or all **running** instances when nothing is selected — plus quick control keys (`⏎ / Esc / Ctrl-C / Ctrl-D`). Each group header has its own broadcast button targeting just that group. - Selecting tiles reveals a **bulk bar** with separate **Run** (resume every stopped/exited selected instance), **Stop**, and **Kill** buttons — each showing how many selected instances it acts on. - **Shift-click** a tile's checkbox to select a range; a single sidebar checkbox selects a whole group (or all ungrouped instances). ## Command palette & keyboard shortcuts Press **⌘K / Ctrl+K** for the command palette — run any action (new instance / shell / editor / git-diff / todo, broadcast, resources, select/clear, columns, density, group, minimize all…) or fuzzy-jump to a session by title or directory. Single-key shortcuts (when no modal is open and you're not typing in a field): | Key | Action | | --- | --- | | `N` | New Claude Code instance | | `B` | Broadcast | | `A` | Select all (in the active tab) | | `/` | Focus the sidebar search | | `j` / `k` | Move keyboard focus down / up through tiles | | `g` | Jump to the next instance needing attention (exited/stopped) | | `m` | Minimize / expand the focused tile | | `` ` `` (backtick) | Cycle to the next tab | | `Tab` / `Shift+Tab` | Cycle the highlight through the current tab's instances | | `Esc` | Exit focus view, clear selection, or clear keyboard focus | ⌘/Ctrl combos (fire even while a terminal is focused): | Key | Action | | --- | --- | | `⌘/Ctrl+K` | Command palette | | `⌘/Ctrl+1`–`9` | Jump to tab by position | | `⌘/Ctrl+N` | Open the instance-type picker | | `⌘/Ctrl+T` | Add a tab | | `⌘S` | Save (File Editor) | ## Resources, usage & notifications - **Resources** (top-bar gauge, or palette) — per-instance **CPU / memory / process-count** meters summed across each PTY's whole process tree, plus aggregate totals and sparkline trends. Each tile also has its own stats popover. - **Usage meter** (top bar) — your Claude **5-hour** and **7-day** rate-limit windows with a countdown to reset, sourced from each session's status line (claudete wraps your existing status line so tiles look unchanged) and refreshed by an idle background probe so it stays current with no active instances. - **Notifications** — a chime + desktop alert when claudete isn't the focused app and an instance **finishes a turn**, when a usage window crosses **90%**, and the moment a usage window **resets**. Routine turn-finishes use the tab flash, chime, and title badge; system banners are reserved for important moments (usage high / reset). Chime and banners toggle in **Settings → Notifications**. ## Settings The gear opens app settings with tabs: - **Usage** — background poller controls. - **New instances** — saved defaults for the new-Claude dialog. - **Notifications** — chime + desktop banner toggles. - **Backup** — export/import your preferences *and* instance launch definitions as JSON. Scrollback and the auth token are **deliberately excluded**; to-do lists are included. - **About** — version + update controls (incl. **Restart to install** once an update has downloaded). - **Changelog** — what each release changed. Each tile also has its own settings dialog for editing its launch config, with **Save & restart** / **Save & resume** to apply immediately. ## Persistence & resume Sessions persist to disk (default `~/.claudete/`) and survive a server restart: - Each Claude session gets a UUID passed to Claude Code as `--session-id `, so claudete's session id **is** the Claude conversation id. - Config (cwd, model, effort, flags) + title + scrollback are written to `~/.claudete/sessions.json` and `~/.claudete/scrollback/.log`. - A live PTY can't outlive the server, so after a restart sessions come back as **stopped** (gray dot) with scrollback viewable. **↻ Resume** relaunches `claude --resume ` in the same directory and the conversation continues. - Resume uses Claude Code's own session store (the conversation must have been persisted by Claude — the default). Non-claude commands are relaunched fresh. `CLAUDETE_PERSIST=0` disables persistence entirely. ## Configuration (server env vars) | Variable | Default | Meaning | | --- | --- | --- | | `PORT` | `4000` | HTTP + WS port (when set explicitly the server won't move off it) | | `CLAUDETE_HOST` | `127.0.0.1` | Bind address (loopback by default) | | `CLAUDETE_CLAUDE_CMD` | `claude` | Executable spawned for each Claude instance | | `CLAUDETE_SHELL` | `$SHELL` → `zsh` | Shell launched for "New Shell" instances | | `CLAUDETE_DEFAULT_CWD` | `~/claudete` | Default working dir for new instances (a subfolder, not `$HOME`, to avoid macOS TCC prompts) | | `CLAUDETE_SCROLLBACK` | `500000` | Bytes of scrollback kept per session | | `CLAUDETE_EXITED_TTL` | `0` | ms before an exited session auto-prunes (0 = keep) | | `CLAUDETE_DATA_DIR` | `~/.claudete` | Where session metadata + scrollback are persisted | | `CLAUDETE_PERSIST` | `1` | Set `0` to disable on-disk persistence | | `CLAUDETE_USAGE` | `1` | Forward each session's Claude usage (5h/7d) from its status line to the meter. `0` disables | | `CLAUDETE_USAGE_POLL` | `1` | Periodically probe usage when idle (briefly spawns a hidden Claude, one-token prompt). Skipped if a real session reported usage within the interval. `0` disables | | `CLAUDETE_USAGE_POLL_MS` | `300000` | Interval between idle usage probes (floored at 60s) | | `CLAUDETE_USAGE_POLL_MODEL` | `haiku` | Model for the throwaway usage probe | | `CLAUDETE_MAX_EXITED` | `200` | Cap on retained non-running sessions (LRU-evicted) | | `CLAUDETE_MAX_SESSIONS` | `100` | Hard cap on concurrent live sessions (0 = no cap) | | `CLAUDETE_ALLOWED_ORIGINS` | _empty_ | Extra browser origins allowed (loopback is always allowed) | | `CLAUDETE_TOKEN` | _empty_ | If set, REST needs `Authorization: Bearer ` and the WS URL needs `?token=`. Match it with `localStorage.claudete_token` in the browser. The desktop app sets a random one per launch | ## REST API Base path `/api`. JSON in/out. When `CLAUDETE_TOKEN` is set, send `Authorization: Bearer `. All requests have their `Origin` validated (loopback or `CLAUDETE_ALLOWED_ORIGINS`); non-browser clients send no Origin and are allowed. Sessions & lifecycle: - `GET /api/health` — liveness probe (used by the desktop wrapper before opening the window). - `GET /api/config` — server config surfaced to the client (defaults, caps). - `GET /api/update-check` — latest available version for the in-app update banner. - `GET /api/sessions` — list all sessions (`SessionInfo[]`). - `POST /api/sessions` — create one or more sessions (`CreateSessionRequest`; `count` for bulk spin-up). - `GET /api/sessions/export` — portable launch definitions (`ExportedSession[]`; no scrollback/token). - `POST /api/sessions/import` — recreate sessions from a backup (`ImportSessionsRequest` → `ImportSessionsResult`). - `DELETE /api/sessions/:id` — kill + remove a session. - `POST /api/sessions/:id/rename` — rename (applies live, persists). - `POST /api/sessions/:id/update` — change launch config (`UpdateSessionRequest`; `restart: true` relaunches now). - `POST /api/sessions/:id/restart` — relaunch (resume the conversation). - `POST /api/sessions/:id/stop` — stop (stays resumable). - `POST /api/sessions/:id/input` — write bytes to one PTY. - `POST /api/sessions/:id/signal` — send a control signal. - `POST /api/broadcast` — write one payload to many PTYs (`BroadcastRequest`; empty `sessionIds` = all running; `submit` appends CR). Filesystem (File Editor, scoped to instance cwd): - `GET /api/fs/list` → `FsListResponse` · `GET /api/fs/read` → `FsReadResponse` - `POST /api/fs/write` · `POST /api/fs/rename` · `POST /api/fs/create` · `POST /api/fs/delete` Git (Git Diff, scoped to instance cwd): - `GET /api/git/status` → `GitStatusResponse` · `GET /api/git/diff` → `GitDiffResponse` - `POST /api/git/stage` · `POST /api/git/unstage` · `POST /api/git/stage-all` · `POST /api/git/restore` - `POST /api/git/commit` · `POST /api/git/push` → `GitActionResult` To-do (per todo instance): - `GET /api/todo?id=` → `TodoListResponse` · `POST /api/todo` (`TodoSaveRequest`, replaces the whole list). Usage: - `POST /api/usage` — a session reports its parsed usage. · `POST /api/usage/refresh` — force an idle probe now. - `GET /api/usage/poll` — poller state. · `POST /api/usage/poll` — update poller settings. Static: any non-`/api`, non-`/ws` path serves the built SPA (`client/dist`). ## WebSocket protocol (`/ws`) One socket carries terminal I/O. When `CLAUDETE_TOKEN` is set, append `?token=` to the URL. Client → server (`ClientMessage`): `attach` · `detach` · `input` · `resize`. (Broadcast is REST, not WS.) Server → client (`ServerMessage`): `sessions` · `created` · `removed` · `status` · `history` (scrollback replay on attach) · `output` (live PTY bytes) · `exit` · `usage` · `stats` · `error`. Control bytes for bulk terminal control (`CTRL`): `ENTER = \r`, `ESC = \x1b`, `INTERRUPT = \x03` (Ctrl-C), `EOF = \x04` (Ctrl-D). ## Security This server spawns processes and exposes their terminals over a socket, so it **binds to loopback only** by default. Don't expose it to a network without authentication (`CLAUDETE_TOKEN`), TLS, and access controls — anyone who can reach it gets a shell running as you. - Both REST and WS validate the request **Origin** and reject anything that isn't a loopback origin (or one you list in `CLAUDETE_ALLOWED_ORIGINS`) — blocking cross-origin / DNS-rebinding attacks. Non-browser clients send no Origin and are allowed. - The desktop app generates a random **per-launch token**, binds to loopback, **strips `CLAUDETE_TOKEN`** from spawned children's environment, and pins window navigation to the local origin (defense-in-depth so a co-resident process or stray link can't reach the control plane). - Backups deliberately exclude scrollback (which can hold secrets) and the auth token. ## Desktop app (macOS, Electron) The server runs inside the app on a private loopback port; the window loads the UI — no terminal, no `npm`, just double-click. `claude` still has to be installed + authenticated; the app shells out to it (and can install it). - `electron/main.cjs` resolves your login-shell `$PATH` (so a GUI launch finds `claude`), picks a port, starts the server child, waits for `/api/health`, then opens the window at `http://127.0.0.1:`. It mints a random per-launch auth token (env → server, URL hash → renderer). The port is **reused across launches** (saved under `userData`) so the renderer origin — and your saved tabs/layout/prefs in `localStorage` — survive a relaunch or update. - `scripts/bundle-server.mjs` esbuilds `server/src/index.ts` into `electron/build/server.cjs`; `node-pty` stays external (native addon). In dev the child runs under system Node (reusing the locally built `node-pty`); when packaged, electron-builder rebuilds `node-pty` against Electron's ABI and the child runs under Electron's Node (`ELECTRON_RUN_AS_NODE`). - Closing the window leaves the app (and sessions) in the dock; **⌘Q** quits and tears sessions down. - **Claude onboarding:** on launch the app checks whether `claude` is on PATH; if not, a dialog offers to **install it in Terminal** (`curl … | bash` → `~/.local/bin`, then launches `claude` to sign in), open the setup guide, or continue. The same actions live in a **Claude** menu (Install / Sign In / Check for Claude / Setup Guide). Because `~/.local/bin` + Homebrew are already on the handed-down PATH, a freshly installed `claude` is picked up without a restart. - **Auto-update:** packaged builds poll the update feed (`electron-updater`, generic provider at `https://updates.claudete.co`) every ~10 minutes, download a newer **signed + notarized** build in the background, and install it on quit — or right away via **Settings → About → Restart to install** / the sidebar's "Update ready" button once a download finishes. Teardown runs before the bundle swap so the server child and its PTYs aren't orphaned. - Builds target Apple Silicon + Intel, **code-signed + notarized** under a Developer ID with the hardened runtime on (credentials from the environment — see `electron-builder.yml` / `scripts/dist.sh`). The app icon is generated from `client/public/icon.svg` into `build/icon.icns`. ## Release & publish - Add a `CHANGELOG` entry in `client/src/lib/changelog.ts` (newest first; top entry version matches the app version). - Bump `version` in root `package.json` (commit "Cut vX.Y.Z") **before** building. - `npm run app:dist:release` builds the signed + notarized + stapled `.dmg` + `.zip`; `npm run publish` uploads to R2; then a GitHub Release (notes from the changelog + the dmg/zip). - The changelog is the single source of truth for both the in-app Changelog (Settings → Changelog) and the generated GitHub Release notes. ## Project layout ``` shared/protocol.ts # wire types shared by client + server (single source) server/src/ config.ts # env-driven config + origin/token helpers persistence.ts # session index + scrollback on disk sessionManager.ts # PTY lifecycle, scrollback, persistence, resume fileEditor.ts # cwd-scoped filesystem list/read/write for editor tiles gitDiff.ts # cwd-scoped git status/diff/stage/commit/push todo.ts # per-instance to-do list persistence usage.ts / usagePoller.ts # account usage parsing + idle probe stats.ts # per-session CPU/memory/proc-count sampling index.ts # Express REST + ws WebSocket server client/src/ lib/claudeteClient.ts # WebSocket client + per-session output routing lib/api.ts # REST helpers lib/changelog.ts # release notes (in-app Changelog + GitHub Release notes) components/ # Sidebar, SessionGrid, TabBar, TerminalView, FileEditorView, # GitDiffView, TodoView, command palette, dialogs App.tsx electron/ main.cjs # desktop wrapper: server child, window, onboarding updater.cjs # background auto-update (electron-updater) preload.cjs # narrow renderer bridge (native pickers, update IPC) scripts/ # dev.mjs, bundle-server.mjs, dist.sh, publish.sh, # r2-publish.mjs, fix-node-pty.mjs, build-icon.sh ``` ## Changelog Newest first (see `client/src/lib/changelog.ts` for the authoritative list): - **0.1.13** (2026-06-28) — Slim, hover-brightening scrollbars across session list, File Editor / Git Diff lists, todo list, command palette, changelog, and modals. - **0.1.12** (2026-06-27) — Closing a File Editor / Git Diff / Todo tile no longer asks "Kill this instance?" (the confirmation is reserved for live Claude instances). - **0.1.11** (2026-06-27) — Resizable panes in File Editor & Git Diff; reliable "Restart to install"; bulk Run/Stop/Kill with per-action counts + whole-group select; default auto-refresh for File Editor & Git Diff; quieter desktop banners; simpler Git Diff commit bar; File Editor ⋮ menu; terminal re-alignment fixes; usage meter keeps its 5h reading across resets; version shown in sidebar header. - **0.1.10** (2026-06-27) — Auto-refresh toggle for File Editor & Git Diff; in-place diff refresh; quieter update install; update check every 10 min. - **0.1.9** (2026-06-27) — New **To-do list** instance type; To-do in the type picker; To-do lists included in backups; `` ` `` cycles to the next tab. - **0.1.8** (2026-06-27) — Bulk **Run**; ⌘/Ctrl+1–9 jump to tab, ⌘/Ctrl+N picker, ⌘/Ctrl+T add tab, Tab/Shift+Tab cycle highlight; custom tooltips; Claude/Shell scrollbars restored; "Update ready" sidebar button; shells no longer inherit internal `$PORT`. - **0.1.7** (2026-06-26) — New **Git Diff** instance type (browse changes, diff, stage/unstage, discard, commit & push); added to the type picker. - **0.1.6** (2026-06-26) — Instance-type picker; per-tab name + default working dir; tiles inherit tab cwd; draggable tab reordering; notifications when claudete isn't focused; chime + alert on usage-window reset; "Restart to install" in About; more reliable chime; Changelog tab. - **0.1.5** (2026-06-26) — File Editor "Reveal in Finder"; dropped word-wrap + minimap; UI tweaks. - **0.1.4** (2026-06-26) — Native macOS path pickers; reworked logo + controls; pinned server port so tabs/layout survive relaunch; dropped the PWA for the desktop app. - **0.1.2** (2026-06-26) — **File Editor** instance type (Monaco); backup & restore; stop + fresh-restart; aggregate Resources view; macOS app icon; fewer startup permission prompts. - **0.1.0** (2026-06-24) — Initial release: a control panel for running and orchestrating Claude Code instances. ## Notes for programmatic / headless control claudete deliberately uses the **PTY approach** to give the *real* interactive Claude Code shell in the browser. For purely programmatic / headless control (structured messages instead of a TUI) you'd use the Claude Agent SDK or `claude -p` streaming-JSON mode instead. The two approaches can coexist.