Are you the author? Sign in to claim
Mission control for every Claude Code session on your machine.
One board for every Claude Code session on your machine.
Run more than two or three Claude Code sessions and you lose track of them: which terminal is which, which one is blocked on a permission prompt, and which two are editing the same file. Fleet Deck puts all of them on one local board at http://127.0.0.1:4711, with a callsign, a live status column, and a mailbox each.
queued → working → verifying → needs-you → idle → offline, computed from hook telemetry. The model badge is read from the session transcript, so it follows a mid-session /model switch instead of freezing on whatever the session launched with.⎿ Allowed by PermissionRequest hook and continues.assign auto: fix the flaky test picks a candidate — idle first, least buried, right repo — with a SQL query. The core makes zero model calls.+ Spawn starts a fresh interactive claude in a daemon-owned tmux window. Watch it on the board, attach to the pane, or kill it.tmux -C attach. No PTY, no native deps. Typing sends real keystrokes to the agent's pane. Shift+Enter inserts a newline rather than submitting — the board is the emulator, so it sends the ESC CR that Claude Code's keybinding expects, with nothing to configure. Ctrl+V pastes a screenshot: the board uploads the image, writes it server-side, and types the path into the composer; you press Enter yourself. (PNG/JPEG/GIF/WebP, ≤10 MB, written owner-only under FLEETDECK_HOME, pruned after 24 h or 50 files.)▦ Terminals opens every live agent at once. All tiles stream; exactly one accepts input — the focused tile wears an amber ring, and every other tile is stdin-disabled at the terminal itself, not filtered on the way out. The grid shares one tmux control client, so the tenth tile costs a WebSocket, not a tenth tmux process.⟲ click resumes a dead agent in its own worktree with full history — same callsign, same card. Whole columns at a time.
claude plugin marketplace add lacion/fleet-deck # the repo is its own marketplace
claude plugin install fleetdeck@fleetdeck
Your next claude — any terminal, any repo, no wrapper — brings the fleet up and appears on the board. Type /fleet in any session for a live summary.
A marketplace install tracks the repo's default branch, not a pinned release: every push to
mainruns at your next SessionStart. The pipeline is gated (CODEOWNERS, hook-integrity CI, human-approved npm publishes), but if you want releases only, use the npm channel —npm i -g fleetdeckfor standalone mode — or pin your marketplace clone to a tag.
Requirements. Node 22.5+. Nothing to npm install: the daemon ships as one bundled file and keeps state in Node's built-in node:sqlite. Add tmux 3.4+ to spawn workers or open their panes in the browser — Fleet Deck relies on 3.4's no-start probe to avoid attaching to a replacement server. Everything else works without tmux. Linux, WSL2 and macOS; Windows-native is untested.
claude plugin marketplace add /path/to/fleet-deck # a local clone
claude plugin marketplace add your-org/your-fork # or your fork
claude plugin install fleetdeck@fleetdeck
After changing anything under scripts/, run npm run bundle — the daemon runs the bundle, not the source. After changing the board, run npm run build in board/. Then restart the daemon, or bump the version and let the upgrade takeover do it.
claude anywhere. A card appears in about a second.assign auto: add input validation to the signup form. The daemon picks a session and wakes it if idle.permission mode: plan), capture its plan, and execute it later with an unsupervised worker.Tick batch and the prompt box becomes a task list — one agent per line:
fix the flaky worktree test
update the README install section
3x find the race in the spawn path
That is five agents in one click. Each gets its own git worktree (<repo>--fd-<callsign>, or <repo>--fd-<TICKET>-<animal> when the source branch carries a Jira ticket) on its own branch (fd/<callsign> / fd/<TICKET>-<animal>), so they share a repo without standing on each other's edits — isolation is forced for a batch, not offered. The 3x prefix runs a line several times, which is how you race independent attempts at one bug.
Before anything launches the form shows the exact list and count. That preview is the guardrail: there is deliberately no cap on how many agents may be live. It is your machine and your token budget.
terminal 1..n : plain `claude` + this plugin's hooks
│ events (http hooks, fail-open) ▲ whispers, blocks, decisions
▼ │ (hook responses)
fleetd — one Node daemon, loopback by default, SQLite state
▲ │ WebSocket push
└────────── the board (React) ─────────┘
claude fleet-aware. The first session's SessionStart hook elects and launches the daemon; the port bind is the election.127.0.0.1. FLEETDECK_BIND=0.0.0.0 opens it to your network, and a token then becomes mandatory (LAN mode). Either way it does not phone home.Panes die; the work does not. Each agent's git worktree and Claude transcript stay on disk, and the daemon knows both paths.
An OFFLINE card whose worktree and transcript both survive grows a ⟲ revive chip. Clicking it relaunches that spawn in the same worktree with claude --resume <session-id> (plus --dangerously-skip-permissions if it had it). Because --resume keeps the session id, the revived agent's first hook un-tombstones the card it already had. When several cards qualify, the OFFLINE column head offers ⟲ Revive all (N).
It refuses rather than guessing:
If the tmux server itself dies, a watchdog notices. That is a fleet-wide event rather than a per-pane one, so it is handled as one: the board says so once, settles the affected cards to offline, and starts a fresh fleet session so the next revive has somewhere to land. Settling the cards is what makes ⟲ revive available instead of a 409. The watchdog never relaunches an agent on its own — resuming one spends money, so that stays a human click.
Sessions you started yourself appear on the board via hooks, but the board doesn't own their pane: no terminal chip, no revive, no mail-to-pane. ⇥ move to tmux resumes the session into a board-owned tmux window (claude --resume <session-id> — same session id, same card, full history), after which it is a first-class fleet worker.
Two entry states:
⧗ armed — exit CLI to move; clicking again disarms, and a forgotten arm expires after ~30 minutes (FLEETDECK_ADOPT_ARM_MS). A /clear is not an exit, and the arm survives it.The arm is durable intent, consumed once: it lives in SQLite, not a timer. Disarm any time, including while a move is settling — the cancel wins. If the session comes back to life first, the move cancels rather than ambushing your next exit. If the daemon dies in that window, the next boot's sweep finishes the move.
The dialog offers the same red, asks-twice unsupervised gate as the Spawn form. Left unchecked, the resumed session's permission prompts land on the board.
It refuses like revive:
Remote (claude.ai/code) sessions can be adopted, but resuming a web session's transcript locally is untested; the transcript check is the gate, not the session's origin.
/rc)Claude Code can hand a session to claude.ai. Fleet Deck names the session after its callsign, so what you find in the claude.ai session list matches your board.
--remote-control <callsign>./rc <callsign> into its pane, waits for the TUI to render, and harvests the https://claude.ai/code/session_… link from the pane's scrollback — that URL is written to no file, so reading the screen is the only source. If the capture misses it, the chip says so and the live terminal shows the link.The guard: enabling remote control is refused (409) unless the session is at a turn boundary — queued or idle. An agent mid-turn, or sitting on a permission dialog, is not waiting for a slash command; typing one there would answer the dialog.
Claude Code talks to anything speaking the Anthropic wire format — CLIProxyAPI, a corporate gateway, your own proxy — via ANTHROPIC_BASE_URL. Fleet Deck makes that a per-session choice, so some agents route through a proxy and others go straight to Anthropic, and the board shows which is which.
Configure once on the Spawn form: set up next to 🛰 gateway.
| Setting | What it is |
|---|---|
gateway_base_url | Where the gateway lives — http://127.0.0.1:8317 for a stock CLIProxyAPI. http:// and https:// only, and no credentials in the URL: this value is shown on the board and travels in /state, so user:pass@host and ?api_key=… are refused. |
gateway_token | The credential. For CLIProxyAPI, any entry from its api-keys: list. |
gateway_auth_style | bearer (default) sends Authorization: Bearer …; api-key sends x-api-key. A 401 is almost always this — the credential is fine, it is arriving in a header the gateway doesn't read. |
gateway_model_discovery | On by default. Asks the gateway for its model list at startup so gateway-only models appear in /model. |
gateway_default | Off by default. When on, a spawn that says nothing routes through the gateway. |
Then tick 🛰 route through … on any spawn. Gateway-routed cards carry a 🛰 chip, and a revived agent keeps its lineage's routing — resuming a conversation against a different provider than the one that wrote its transcript should not happen quietly.
The token never comes back. It is stored on this machine and handed to the pane through tmux's own environment (new-window -e), keeping it out of the pane's command line and out of ps. The board is told token_set: true and nothing else, because /state is broadcast to every connected board, phones included. The base URL is not secret and deliberately is in there — which is why it refuses to carry a credential.
Upgrade note. Spawned panes no longer inherit ANTHROPIC_* from the daemon's environment; that inheritance let a single export in one terminal silently reroute every session on the machine. If you authenticate with an ANTHROPIC_API_KEY exported in your shell, move it to ~/.claude/settings.json under env. Sessions you start yourself are unaffected.
Two refusals. A half-configured gateway (URL without token, or the reverse) fails the spawn with a 400 rather than quietly billing your Anthropic account. And remote control is unavailable on a gateway-routed session — Claude Code disables it whenever ANTHROPIC_BASE_URL points somewhere that isn't Anthropic, because claude.ai has no route to a session it isn't serving. The form greys out whichever you didn't pick.
Sessions you start yourself are outside this; Fleet Deck only steers panes it launches. To put everything on a gateway, use
~/.claude/settings.json.
A callsign is <animal>-<4 hex> by default (raven-4b7f). When a session sits on a branch carrying a Jira key (feature/PROJ-123-checkout, fd/PROJ-123-otter), Fleet Deck swaps the hex for the ticket: raven-PROJ-123. The card, the mailbox target and the tmux window all read as the ticket.
ticket <callsign> <PROJ-123> from Compose → ORCHESTRATOR pins one by hand. A manual pin wins over auto-detection and is never overwritten. ticket <callsign> clear restores the birth name.PROJ-123 gets a different animal. When all twelve are taken, the thirteenth falls back to the hex suffix and says so in the ticker.<repo>--fd-PROJ-123-<animal> on branch fd/PROJ-123-<animal>, so worktrees and branch lists group by ticket.The animal is the fleet's; the ID is yours. Rename wren-a9e1 to wren-docs-review with the ✎ chip on the card or name wren-a9e1 docs-review in Compose. name <callsign> clear restores the automatic name — the ticket name if the card has one, otherwise its birth name.
The animal never changes: twelve animals rotating through the fleet is what makes cards recognizable at a glance, and it keeps one-animal-per-ticket intact. Names are letters, digits and dashes — a space or dot would break the card's timeline filter and its tmux window name.
A hand-typed name wins. Branch auto-detection never renames over a name you chose. An explicit ticket command still does, because that is also you. Mail addressed to the old name keeps arriving.
/clear keeps your cardClaude Code does not keep a session id across a /clear — it ends the old session and starts a fresh one. Fleet Deck follows the handoff: the new session continues the same card, callsign, tmux pane, ticket, mailbox and armed move-to-tmux. One ticker line notes the context was cleared; nothing else changes.
Upgrading an existing fleet is automatic as of 0.7.0: a new session's SessionStart hook notices an older running daemon, asks it to step down (SIGTERM, graceful; state is SQLite, nothing is lost), and boots its own newer build. Strictly newer, never a downgrade, and it fails open onto the running daemon if the handoff looks uncertain. A manual restart is always safe.
Cards do not pile up, and nothing is deleted:
Knobs: FLEETDECK_PRESUME_DEAD_MS, FLEETDECK_RETAIN_OFFLINE_MS.
Fleet Deck is a Claude Code plugin whose daemon is booted lazily by a SessionStart hook. That is fine on a laptop and useless on a remote dev box, where there may be no Claude Code session at all and the only way in is a browser tab. So it also runs as a service:
npm install -g fleetdeck
fleetdeck doctor # Node 22.5+? tmux? claude? the plugin?
fleetdeck service install # a systemd user unit, or a supervised wrapper without systemd
fleetdeck service start
The board is now always on, and you can spawn an agent with no Claude Code session anywhere: type a repo path, click, and it comes up in a tmux pane you can watch, type into, and answer prompts for. What standalone adds is a daemon that exists without a session to boot it, and a way to reach it from somewhere other than localhost.
To reach it from another machine, put it behind a reverse proxy and name the origin:
export FLEETDECK_TRUSTED_ORIGINS="https://board.example.com"
export FLEETDECK_PROXY_AUTH="trust" # only if the proxy really authenticates; default is `token`
The same-origin wall is what stops any website you visit from driving your fleet over loopback, so it does not switch off — you widen it by exactly the origin you name. An unnamed origin is still refused, and a typo is a startup refusal rather than a board that mysteriously 403s.
The board is prefix-agnostic: it resolves assets, API calls and WebSockets relative to where it was loaded, so it works at a domain root or under a path prefix (/apps/fleetdeck/) behind nginx, Traefik, or a Coder path-based app.
Keep the plugin installed. The board can launch an agent without it, but status, model, edits and permission prompts all arrive through the plugin's hooks — without it, cards appear and never move.
→ docs/CODER.md is the full guide for Coder workspaces.
A managed daemon is never evicted by a plugin hook. Normally the newest installed plugin takes the port by SIGTERMing an older daemon; against a supervised service that would fight the supervisor, so the service wins and the version drift is reported in your session brief.
assign auto routes to existing sessions only.--dangerously-skip-permissions workers never produce permission cards. The checkbox is red and asks twice. Pair it with a fresh worktree.claude -p sessions deny permission-needing tools without consulting hooks — CLI behavior, not ours. Spawned workers are interactive precisely so their prompts reach the board.FLEETDECK_PORT / FLEETDECK_HOME. Hooks default to 4711, so a truly separate fleet also needs the port swapped in a copy of hooks/hooks.json. On multi-user machines give each OS user their own port.FLEETDECK_TMUX_SOCKET runs every tmux command against a named server (tmux -L <socket>) instead of your default one. Tests and demo/ scripts always set it, for a concrete reason: tmux bakes the first client's environment into a new server's global env, and every window created later inherits it. An acceptance run once started the default tmux server from inside a test session, and that evening's production spawns inherited the test FLEETDECK_PORT/FLEETDECK_HOME and reported to a daemon nobody was watching. The demo scripts now use a per-run socket and kill-server it on exit. Leave this unset in production — or set it to move your fleet off the shared default socket, where any unscoped tmux kill-server on the machine would take it down.scripts/fleet-hook.mjs) that honors FLEETDECK_PORT, so a custom port no longer splits hook traffic. The shims also authenticate hooks: they read $FLEETDECK_HOME/token and attach it, which is why a tokenless /hook/* curl gets a 401.By default fleetd listens on 127.0.0.1. Set FLEETDECK_BIND=0.0.0.0 and it binds every interface, printing a ready-to-paste URL per address:
fleetd up on http://0.0.0.0:4711 (pid 12345, …)
fleetd LAN http://192.168.8.223:4711/?t=2a62f3c9…
fleetd LAN http://fleetdeck.local:4711/?t=2a62f3c9… (mDNS — needs a resolver on the peer)
Open one on the other machine; the key is consumed at boot and scrubbed out of the address bar. The header's ⇄ Share panel shows the same links with a QR code.
The ?t= is a password. This API can spawn agents with --dangerously-skip-permissions and type keystrokes into their terminals: unauthenticated, it is remote code execution for anyone on the network. LAN mode therefore requires a token — there is no insecure switch, and fleetd refuses to start rather than open an unauthenticated listener.
/ws/term), POST /mail, gateway_* settings writes, and unsupervised spawns. Hooks authenticate automatically, and the daemon prints the credentialed local link at startup. On a shared box, other local users sit inside the loopback trust zone; FLEETDECK_REQUIRE_TOKEN=on closes every route behind the token — though it cannot protect you from processes running as your user, which can read the token file. See SECURITY.md.Authorization: Bearer <token> or ?t=<token>. Wrong or missing → 401.<script> tag inside the page it is already loading, so gating the shell would serve a blank board rather than hide it. The printed link carries the key in the query string for the same reason: no Authorization header exists on a first navigation.FLEETDECK_HOME/token (mode 0600), or set FLEETDECK_TOKEN yourself (16+ chars). Rotate by deleting the file and restarting.In LAN mode the daemon advertises itself over multicast DNS / DNS-SD with a dependency-free responder (no Avahi or Bonjour install): an A record for fleetdeck.local, plus _fleetdeck._tcp and _http._tcp services. FLEETDECK_MDNS=off disables it; FLEETDECK_MDNS_NAME renames it — two fleets on one network will collide on fleetdeck.local.
It is a convenience, never a dependency: if port 5353 is taken or multicast is blocked, mDNS degrades to a no-op and the printed IP URLs keep working. Resolving .local needs a resolver on the other machine — macOS and iOS have one, most Linux boxes need avahi-daemon, Windows is unreliable without Bonjour. If fleetdeck.local doesn't resolve for a peer, use the IP URL.
For moving between your own machines, Tailscale beats mDNS: a stable private IP that works off-LAN, with MagicDNS for names. Bind LAN mode, open the board at the tailnet address, and the token still guards it.
All optional; the defaults are what we run.
| Variable | Default | What it does |
|---|---|---|
FLEETDECK_PORT | 4711 | Daemon port. The hook shims honor it — read the one-port rule above before changing it. |
FLEETDECK_HOME | ~/.fleetdeck | State directory: SQLite db, LAN token, watcher pid files. |
FLEETDECK_BIND | 127.0.0.1 | Bind address. 0.0.0.0 is LAN mode, which makes a token mandatory. |
FLEETDECK_TOKEN | generated into $FLEETDECK_HOME/token | Bearer token, 16+ characters. Generated on every boot since 0.16.0, because hooks, /ws/term, /mail, gateway writes and unsupervised spawns all present it. |
FLEETDECK_REPOS_DIR | ~/projects (/workspace on Coder) | Where repo-mode spawns clone repositories that aren't local yet. The dialog's destination field can override and persist a different root. |
FLEETDECK_BROWSE_ROOT | home (/workspace on Coder) | Root of the ⌸ Files explorer and the spawn form's folder picker. The browse_root setting wins over this; always resolved server-side. |
FLEETDECK_TRUSTED_ORIGINS | unset | Comma-separated origins allowed to reach the daemon behind a reverse proxy — https://board.example.com, or one leading wildcard label (https://*.coder.example.com). Scheme required. Without it, a proxied board loads and then 403s. |
FLEETDECK_PROXY_AUTH | token | Who authenticates a proxied browser. token: it must present the bearer token. trust: the proxy already authenticated it (only sane when it really does). |
FLEETDECK_TRUST_LOOPBACK | off | on waives the 0.16.0 loopback power gates so a plain-loopback board needs no key — for single-user machines and port-forwarded Coder workspaces. Hooks stay authenticated. Refuses to combine with LAN mode or FLEETDECK_REQUIRE_TOKEN. |
FLEETDECK_REQUIRE_TOKEN | off | on requires the token even over loopback on every route; only /health and the public shell stay exempt. Closes the loopback trust zone against other OS users. Does not protect against processes running as your own user. |
FLEETDECK_MANAGED | unset | Set to 1 by fleetdeck serve. Marks the daemon supervisor-owned so a plugin hook never SIGTERMs it. Not set by hand. |
FLEETDECK_MDNS | on (LAN only) | off disables the mDNS/DNS-SD responder. |
FLEETDECK_MDNS_NAME | fleetdeck | The advertised name, i.e. fleetdeck.local. |
FLEETDECK_SPAWN | on | off disables spawning; the board hides every spawn control. |
FLEETDECK_STALE_MS | 600000 (10 min) | How long a working card runs without telemetry before it's badged stale. |
FLEETDECK_HOLD_MS | 50000 (50 s) | How long a question hook is held open awaiting a board answer. Clamped 250 ms–60 s, under the 65 s hook timeout. |
FLEETDECK_NUDGE_MS | 8000 (8 s) | Grace before a silent new pane gets its one bring-up Enter. Exactly once, and never into a folder-trust or MCP-approval dialog. |
FLEETDECK_SPAWN_REGISTER_MS | 90000 (90 s) | How long a spawned pane may run without phoning home before it's flagged stalled — loudly, never auto-respawned. |
FLEETDECK_PANE_MAIL_GRACE_MS | 1500 (1.5 s) | Head start given to the watcher before mail is typed into an owned pane. |
FLEETDECK_PRESUME_DEAD_MS | 10800000 (3 h) | Silence after which a session is presumed ended. A late hook undoes it. |
FLEETDECK_RETAIN_OFFLINE_MS | 86400000 (24 h) | How long an offline card stays on the board before archiving (never deleted). |
FLEETDECK_RETAIN_LEDGER_MS | 86400000 (24 h) | How long file-touch, command, conflict and settled-mail rows live before aging out. |
FLEETDECK_CAPTURE_PAYLOADS | off | on writes every hook payload to $FLEETDECK_HOME/hook-payloads.jsonl (0600) to debug schema drift. Secret-looking keys and known token shapes are redacted, but a secret in free text is only best-effort caught — leave it off unless chasing a hook bug. |
FLEETDECK_RC_HARVEST_MS | 2500 (2.5 s) | Delay before reading a pane's scrollback for the claude.ai remote-control link. |
FLEETDECK_ADOPT_ARM_MS | 1800000 (30 min) | How long an armed move to tmux waits for you to exit the CLI. |
FLEETDECK_ADOPT_DELAY_MS | 750 | Grace after exit before the armed move resumes, so the CLI can flush its transcript. |
FLEETDECK_CLEAR_SUCCESSION_MS | 30000 (30 s) | How long after a /clear a new session id in the same directory reads as that session continuing. |
FLEETDECK_TERM | on | off disables the live terminal entirely. |
FLEETDECK_TERM_REPAINT_MS | 80 | Repaint coalescing window for the terminal bridge. |
FLEETDECK_TMUX_SOCKET | unset | Run every tmux command against a named server (tmux -L). See the one-port rule above. |
FLEETDECK_AGENTS_CMD | the claude agents CLI | Override the agents-listing command. Whitespace-split into argv and run with no shell, so quotes, pipes and $() are never interpreted — wrap a pipeline in a script. false or blank disables that poller. |
FLEETDECK_AGENTS_POLL_MS | 10000 (10 s) | Agents-poll cadence, which also drives owned-pane liveness. Floor 100 ms. |
FLEETDECK_WATCH_POLL_MS | 25000 (25 s) | The idle-session watcher's long-poll hold per request. Clamped 50 ms–25 s. |
FLEETDECK_WATCH_MAX_MS | 7200000 (2 h) | Lifetime cap on a watcher process before it exits and waits for the next turn. |
Deep-tuning knobs that rarely need touching: FLEETDECK_TERM_CMD_TIMEOUT_MS (10 s, the terminal bridge's tmux command timeout), FLEETDECK_TERM_INPUT_MAX_BYTES (256 KiB, the ceiling on queued terminal stdin), FLEETDECK_WS_BUFFER_MAX (1 MiB, per-peer WebSocket send-buffer cap before that peer is dropped and resynced), and FLEETDECK_AGENTS_IDLE_POLL_MS (60 s, how often an empty agents registry is re-polled).
FLEETDECK_SPAWN_CMD, FLEETDECK_TERM_CMD and the FLEETDECK_TEST_* family replace tmux and the daemon with fixtures for the test harness. They are not a supported way to run a fleet.
npm install
npm test # 600+ contract tests against a real daemon
npm run bundle # rebundle the daemon after touching scripts/fleetd/
npm run build:board # rebuild the React board into board-dist/
npm test runs serially (--test-concurrency=1) by necessity, not preference: every test boots a real daemon that binds a real port and drives a real tmux server. Run them in parallel and they contend for both, producing one or two failures a run — a different one each time, each passing in isolation.
The demo/ scripts are live acceptance gates that start real Claude sessions and therefore cost money: run-smoke.sh (two sessions colliding on purpose), run-accept-phase3.sh (a permission and a trailing question answered from the board), run-accept-spawn.sh (spawn → assign → board-approved permission → kill), run-accept-plan.sh (plan → capture → unsupervised execution). Run them deliberately.
Built by a fleet of Claude agents coordinating through contracts, reviewed by Codex, supervised by one human with a board — which is to say Fleet Deck was built the way Fleet Deck works.
Board design: "Console" direction — ink navy, amber means yours to act, IBM Plex Mono for anything that's data.
MIT.
Design enforcement with memory — keeps your UI consistent across a project
Create animation-rich HTML presentations using a coding agent's frontend skills
Agent harness performance optimization with skills, instincts, memory, and security
1000+ skills curated from Anthropic, Vercel, Stripe, and other engineering teams