Releases: lacion/fleet-deck
Release list
v0.20.0 — copy and paste actually reach your clipboard
Copying out of a live pane, and pasting into one, now actually reach your clipboard. A minor rather than a patch: Ctrl+V no longer delivers ^V to the program in a pane, and the board gained clipboard capability (OSC 52) it never had.
Fixed
-
The agent's own copy now reaches your clipboard. This is the one that was broken all along. Claude Code's TUI turns mouse reporting on, so a drag across a pane never reaches the emulator as a selection: the TUI does its own selecting, prints its own "copied N characters", and writes the clipboard with OSC 52 — the terminal's clipboard escape — wrapped for tmux passthrough. Two things then threw it away. tmux hands Fleet Deck that wrapper unopened, because our client is tmux control mode, which is not a terminal and so never gets the unwrapping a real client would; and xterm has no OSC 52 handler at all (0, 1, 2, 4, 8, 10–12, 104, 110–112 — and that is the whole table). So the agent reported a copy, the terminal discarded it, and the clipboard kept whatever was in it. The board now does tmux's half of the job and honours the write.
Writes only, never reads. OSC 52 has a read form that answers by typing the clipboard back into the pane as though you had pasted it, and any byte a pane renders — a file, a tool result, a fetched page — can ask for it. It is refused, with a test pinning the refusal. Only the clipboard write is unwrapped from a passthrough; every other passthrough sequence is dropped rather than forwarded to the emulator.
-
Ctrl+V pastes your clipboard into the pane. In a terminal Ctrl+V is not paste — it is the byte
^V, and pasting is Ctrl+Shift+V. That convention assumes the terminal and the program share a machine. Here they do not: your clipboard is in a browser, possibly on another computer, and Claude Code answers^Vby looking for an image on the daemon host's clipboard and reporting "no image found" — truthful, and about the wrong machine. The board now claims the chord and lets the browser perform its own paste, which needs no clipboard permission and still goes through xterm's bracketing, so a multi-line paste cannot submit itself line by line. Ctrl+Shift+V, right-click → Paste and ⌘V are unchanged. -
"✓ copied" now means the clipboard actually changed. The pane reported success from the return value of a clipboard call, and neither call means what it looks like:
navigator.clipboard.writeText()resolves when the write is accepted — Chrome can drop it afterwards, with no callback — anddocument.execCommand('copy')returns true for "the command ran". A copy is now driven through a realcopyevent whose listener writes the data, and it is the listener firing that gets reported. A refused copy keeps the selection on screen, because right-click → Copy is then the remaining path and it needs one. -
A drag that cannot select now says why, where it failed. The agent's TUI owns a plain drag, so it selects nothing. That fact used to live in a header hint, which is the same as nowhere. A sweep across a pane that selects nothing now answers itself: ⇧drag to select — the agent owns a plain drag (⌥ on a Mac). It fires only when a modifier would genuinely have changed the outcome.
-
A terminal refused for want of the board key no longer reports a network fault.
/ws/termis the one loopback route that requires the key, and the daemon refuses it by destroying the socket — so the browser cannot tell 401 from a dead network. The board rendered that as "connection closed". A socket that closes before its first frame now says which it is, and a board holding no key says so and names the fix. -
An upgrade can no longer be invisible.
index.htmlshipped with no cache directives at all, leaving a browser free to keep serving the previous board after an upgrade. The shell is nowno-store; the fingerprinted assets areimmutable.
Unchanged, and verified as such
Pasting a screenshot into a pane still uploads it to the daemon's filesystem (FLEETDECK_HOME/pastes/) and types the path into the pane — the behaviour headless and remote setups depend on, since an agent can read a file but never a clipboard. You still press Enter yourself. One improvement falls out of the Ctrl+V change: on Linux and Windows there was previously no keyboard path to paste an image (Ctrl+V was eaten as ^V, and Ctrl+Shift+V is "paste as plain text", which strips images before the page sees them). Ctrl+V now carries images too.
How this was found: three rounds of "copy still does not work" after the 0.19.2 chord shipped. The tell was in the first report — "copied xxx characters". Fleet Deck prints no character count anywhere, so that message could only have come from the agent, which meant the copy was never the board's to fix.
Known limit: an agent-initiated copy has no user gesture behind it, so it can only use navigator.clipboard — which does not exist on the plain-http LAN board. On loopback it works; from another device it does not, and Ctrl+C remains the path there.
Full changelog: v0.19.2...v0.20.0
v0.19.2 — copying text out of a live pane
Fixed
-
Copying text out of a live terminal works — and no longer interrupts the agent instead. Selecting inside a pane and pressing Ctrl+C put nothing on the clipboard, for two independent reasons that each looked like the other's symptom. Claude Code's TUI turns mouse reporting on, and a terminal emulator answers a mouse-mode application by handing it the drag: the highlight you saw was the agent's own TUI, not a selection, so there was never anything to copy. And Ctrl+C could not have copied it anyway — in a terminal that chord is the interrupt, sent as ETX with the keydown cancelled, so the browser's copy event never fires. Every "copy" from the deck was an interrupt delivered into a live agent.
Both halves are now closed. ⇧drag (⌥drag on a Mac) selects inside a pane, and Ctrl+C (⌘C) copies that selection — only when there is one, so with nothing selected the chord is still the agent's interrupt, and the selection is cleared the moment it is copied so the very next press interrupts again. The pane confirms the copy in the same status pill an image paste uses. Both terminal frames now name the two chords in their header, and the "?" overlay spells them out; neither is guessable from the board. On a Mac the selection modifier had to be enabled explicitly (
macOptionClickForcesSelection), without which a Mac could not select pane text at all.The clipboard write goes through the same helper the LAN panel uses, so it keeps working on the plain-http LAN board where
navigator.clipboarddoes not exist — and reports a failure instead of silently pretending. (0.18.0 made Ctrl+C stop opening Compose; that fixed the board, never the pane.)
How this was found: reported from the board itself — "I open a terminal and copy from the deck, it's not really copying anything, I can't paste." Confirmed against the running fleet: tmux list-panes -F '#{mouse_any_flag}' reports 1 on every claude pane, which is the emulator's instruction to hand drags to the application instead of selecting.
The chords, in one line: ⇧drag (⌥drag on a Mac) selects · Ctrl+C (⌘C) copies · Ctrl+C with nothing selected is still the agent's interrupt.
Also new: the shipped-board test now walks the asset import graph instead of only the names index.html carries. The terminal is a lazily-loaded chunk that index.html never names, so the old check could not have caught a stale or unstaged terminal bundle.
Full changelog: v0.19.1...v0.19.2
v0.19.1 — release a card stranded mid-resume
Fixed
-
A card stranded mid-resume is released instead of reading
reviving…forever. A revive (or adopt) marks its card as an in-flight resume and waits for the new pane's first hook to move it on. If that hook never arrived — the daemon was restarted during the launch, or the pane died the instant it was exec'd — the card kept the transient note permanently, the board offered no way to try again, and the only recovery was editingfleetd.dbby hand. Neither the spawn-row reconciler nor the tombstone path could fix it: both skip a session that already hasended_at, which every resume target has by definition. Such a card is now released back to offline and revivable, at daemon boot and on the liveness tick, so a wedge created at runtime clears without a restart.The check is a proof, not a timeout: a resume inserts its provisional spawn row before it flips the card, so a card presenting as in-flight with no spawn row in
provisioning|spawning|stalled|livecannot be a resume that is merely slow. At boot the heal deliberately runs after row reconciliation, because a row the previous daemon leftspawningwould otherwise read as a live resume.
How this was found: claude was missing from the daemon's PATH, so every revived pane died instantly (env: 'claude': No such file or directory, status 127) and two cards stranded at reviving… with no affordance to retry.
Operator note (not a code change): if you run the daemon as a service, its PATH must be able to resolve claude and the same tmux that owns the fleet session. A service manager's default PATH typically resolves neither. For the systemd unit that means setting PATH= in ~/.fleetdeck/service.env.
Full changelog: v0.19.0...v0.19.1
v0.19.0 — the git failure remedy, on the card
This release makes a failed repository spawn diagnosable. git already prints the remedy for most clone failures — an unregistered key, the URL to register it, a host it does not trust — but the board only ever showed the final fatal: line and discarded the rest into a log nobody reads. The remedy now reaches the card, credential-redacted and bounded.
Added
- The git failure remedy, on the card. A failed clone or fetch keeps a bounded tail of git's stderr as
spawns.fail_detail, exposed on the snapshot assessions[].spawn.fail_detailfor tombstoned and stalled rows, and revealed by a per-card disclosure next to the existing note. The motivating case: a Coder workspace answers a failed SSH clone with the workspace's own public key and the exact URL to register it. That block was printed, thrown away, and rediscovered by hand hours later. The column is added additively and backfilledNULL, so pre-upgrade rows and every healthy card render exactly as before.
Security
- git stderr is redacted before it is stored or served. A clone URL can carry credentials, and git echoes URLs freely. The card note and the new detail are now derived from a single hardened string, so the two sinks cannot disagree about a secret: URL userinfo is scrubbed and fails closed past its length bound, schemeless
user:token@hostpairs and secret-bearing query and fragment parameters are covered, known token shapes are masked, and the caller contributes the exact credential material it already holds for anything shapeless. The excerpt is capped at 20 lines and 2000 bytes, with the cap applied after redaction so truncation can never re-expose a masked secret. Remote-authoredremote:text is control-stripped and labelled as relayed. - The same positional scrub now also covers the stall diagnostic excerpt and the worktree prune and add paths, which previously put raw git stderr into a note or an HTTP body.
Fixed
- The distilled one-line failure note is unchanged for every input without embedded credentials, so existing cards and ticker lines read exactly as they did.
Upgrade note: the spawns.fail_detail column is added additively on first start and backfilled NULL — no action needed on an existing fleetd.db.
Full changelog: v0.18.0...v0.19.0
v0.18.0 — floating terminals, shell sessions, and better workspace launches
This release turns a round of real user feedback into a more direct day-to-day
board: terminals no longer cover the fleet, standalone shells and visible setup
commands make workspace bring-up useful beyond Claude itself, dead cards can be
dismissed one at a time, and stalled launches now show what was actually left on
the pane. Repository spawns also learn a durable default organization, including
Coder's textemma seed and GitLab subgroup inference.
Added
- Floating live terminals. A terminal now opens in a draggable, resizable
window above the board, can be minimized to a dock chip without dropping its
WebSocket, remembers its position, and falls back to the full-screen viewer on
small/touch displays. The terminal keeps keyboard focus visually explicit so
board permission shortcuts cannot be mistaken for input sent to the pane. - In-board help. A
?overlay documents keyboard shortcuts, card and header
actions, and orchestrator command grammar. Compose now includes compact,
clickable examples for broadcast, assign, ticket, and name commands. - Standalone shell sessions. Spawn a shell without Claude, or open a new shell
in any card's workspace. Shell cards remain terminal-first, can be killed
directly, never receive fleet mail, and are excluded from orchestrator routing. - Visible setup commands before Claude starts. Repo and directory spawns can
run an explicit setup command in the pane after materialization and before
launching Claude. Per-repository defaults can be saved; a failing setup remains
visible and prevents Claude from starting, while revive intentionally skips
setup for an already-materialized workspace. - Default organizations for bare repository names. A bare
repocan resolve
through a persisted or environment-provided namespace. Coder workspaces seed
textemma; multi-segment group paths infer GitLab when no host was supplied;
explicit hosts still win or fail loudly. Namespace validation prevents URL and
scp delimiters from changing the requested clone target. - Registration-stall diagnostics. When a spawned pane never registers, Fleet
Deck captures a redacted, UTF-8-safe, byte-bounded tail of the pane and exposes
it in the card and Drawer, making setup errors, trust prompts, and environment
failures diagnosable without opening the terminal. - Per-card dismissal. Individual offline sessions can be removed without
clearing every tombstone. Revivable cards require a second confirmation, and
dismissal keeps their worktrees available in the Worktrees view.
Fixed
- Copy shortcuts no longer open Compose. Modified key chords are excluded
from board hotkeys, so Command/Ctrl+C and ordinary copy/paste work normally. - A vanished pane is reported as an ended agent, not a refused viewer. The
terminal WebSocket sends a clean exit frame and triggers liveness reconciliation
when the database still says live but tmux has already removed the pane. - Cold-start hooks authenticate the first registration. SessionStart now
rereads the bearer token minted by a daemon it just started, so the first card
and roster brief are no longer silently lost on a fresh home. - Shell/setup isolation stays inside Fleet Deck.
remain-on-exitis scoped to
the fleet tmux session rather than the shared server, shell sessions cannot
become mail or auto-assignment black holes, and the liveness watchdog recognizes
legitimate shell/setup phases instead of condemning them as dead Claude panes. - The release smoke test is genuinely isolated. It uses a unique temporary
home, dedicated port and tmux socket, authenticated hooks, exactly two workers,
process-group cleanup, verified graceful daemon shutdown, and never stops the
live board on port 4711.
v0.17.0 — a tmux watchdog, and revive that survives losing the server
Losing the tmux server took the whole board with it — silently, and then permanently. Every spawn vanished at once with nothing said about why, and from then on ⟲ revive answered 503 tmux window lookup failed; revive held to avoid a duplicate session forever, with no way back except starting a tmux server by hand. This release makes that survivable: Fleet Deck now watches the tmux server it depends on, says plainly when it dies, settles the sessions that went down with it so they can be revived, and stands a fresh server back up.
Added
- A tmux server watchdog. The liveness tick was the only thing watching tmux, and it watched in silence. Per-window absence is deliberately UNKNOWN at runtime — right for one window, wrong for the whole server — so a SIGKILLed tmux left every row
liveforever: the board kept showing panes that no longer existed and ⟲ revive answered409 spawn is live, not revivable, with no way for a human to say "it's dead, bring it back". The tick now asks the one question that separates a healthy-but-empty server from a dead one, and acts only on proof (a death certificate plus tmux's own absence verdict — a pane cannot outlive the server that ran it): the loss is announced once, the rows it took down are settled togoneso Revive is offered instead of a 409, and a fresh fleet session is stood back up. It never relaunches an agent — resuming one spends money, so that decision stays with the human. A tmux that merely stops answering is reported too (once, with a matching recovery line) and still condemns nothing.
Fixed
- Revive no longer wedges permanently when the tmux server dies. Proving the recorded owner dead (ESRCH) is what proves its panes died with it — and retiring the claim destroyed that proof at the instant it was obtained, so exactly one call could answer "empty" and every call after it went back to UNKNOWN. That refusal could not heal, because revive/adopt/
/rcask "is this window free?" beforeensureSession, the only code that ever creates a tmux server. The proof is now kept as a death certificate (tmux-generation-<port>.retired) that outlives the claim it replaced and is cleared by the next successful claim. A home that never claimed a server still reports UNKNOWN — with no certificate there is no evidence, and a silent socket is exactly what a live server behind an unlinked socket looks like. - A failed tmux probe is no longer mistaken for an empty fleet. Only tmux's own two absence verdicts (
no server running on …,error connecting to … (No such file or directory)) count as proof that nothing is listening. A timeout, a shadowed or missingtmuxbinary, an unreadable socket directory or fork exhaustion stays UNKNOWN and fails closed — previously any of them could have told boot reconciliation that a board full of live panes was gone. ensureSessioncan no longer report success without a tmux server.has-sessionis a predicate, so an authoritatively-empty short circuit (correct for a listing: there is nothing to list) must not be read as "the session exists".
Full changelog: https://github.com/lacion/fleet-deck/blob/v0.17.0/CHANGELOG.md
v0.16.1 — Findable keys after auto-start, an opt-out where the gates defend nothing, and a hook-auth hole closed
0.16.0's loopback power gates assumed someone saw the daemon print its credentialed link. On auto-started daemons — Coder workspaces, systemd services, hook-spawned daemons — that line goes to a log file, and users hit a locked board with no key in sight. This release makes the key findable everywhere, makes the gates optional where they defend nothing, and closes a hook-auth bypass the 0.16.0 docs had wrongly guaranteed against.
Added
FLEETDECK_TRUST_LOOPBACK=onwaives the four loopback power gates (/ws/term,POST /mail,gateway_*settings writes, the unsupervised arm) for plain-loopback callers — the pre-0.16.0 behavior, opted into explicitly. Keys off the real peer address, not a header. Startup-fatal combined with LAN bind orFLEETDECK_REQUIRE_TOKEN. Does not waive hook auth. For single-user machines and port-forwarded Coder workspaces where no other OS user can reach127.0.0.1.docs/CODER.mdcarries both recipes.- The key is findable after auto-start. The SessionStart brief names where the key lives (
fleetdeck token/$FLEETDECK_HOME/token);fleetdeck statusshows the board URL and points atfleetdeck token; the board key gate explains recovery.fleetdeck statuskeeps the credentialed link out of routine output so it can't leak into logs/scrollback — pass--show-tokento copy the full?t=URL.
Security
gateway_*settings writes stay token-gated even underFLEETDECK_PROXY_AUTH=trust. They reroute every future session's LLM traffic and can exfiltrate the gateway credential; the trusted-proxy signal is derived from caller-controlledHost/Origina direct-loopback process can forge. Only the explicitFLEETDECK_TRUST_LOOPBACK=onopt-out (real-peer keyed) waives them.- Hooks authenticate in every mode, including
FLEETDECK_PROXY_AUTH=trust. 0.16.0 gated/hook/*on the bearer, but the proxy-trust exemption inauthorized()returned before that gate: a direct-loopback process could forge the configured trustedHost/Originand POST tokenless hooks on a trust-mode daemon, re-opening the session-impersonation forgeries 0.16.0 closed. The/hook/*gate now leadsauthorized(), ahead of every loopback and proxy-trust exemption. Regression coverage added (forged headers,TRUST_LOOPBACKon and off).
Full changelog: https://github.com/lacion/fleet-deck/blob/v0.16.1/CHANGELOG.md
v0.16.0 — Security hardening: authenticated hooks, arm gates, trust-nudge gate, fail-closed tmux ownership
A full red-team exercise (five attack teams, five independent verifiers, every
claim re-read against the code) found the daemon's network walls held — and
that the real exposure lived elsewhere: session-to-session isolation was
cosmetic, the bring-up nudge answered Claude Code's trust dialog for you, the
unsupervised two-step existed only in the UI, and the release pipeline had no
human gate. This release closes all of it. Several changes are breaking;
each says who and what to do.
Security
- Hooks are authenticated. Every hook event now reaches the daemon through a command shim (
scripts/fleet-hook.mjs, alongside the existing SessionStart and watch scripts) that reads$FLEETDECK_HOME/tokenand attaches it — Claude Codehttphooks cannot carry headers, which is why the daemon used to exempt/hook/*from the bearer entirely. That exemption meant any local process could impersonate a session with one curl: graft a/clearsuccession and steal a card's identity, pane, mail and remote-control link; plant a permission hold under a sibling's callsign so your Approve lands on the attacker's prompt; drain a mailbox by forgingUserPromptSubmit. All of those now 401. - The daemon's mail identities and frames are reserved.
orchestrator,fleetdeck,fleetdeck-answer,humanand any text opening with a[FLEETDECK ...]frame are refused (422) from external senders — the fleet doctrine teaches agents to treat those envelopes as carrying your authority, so they must be unforgeable. Ordinary callsign senders and plain text are untouched, and the doctrine now tells agents plainly: unframed mail is untrusted peer content, and no mail ever authorizes destructive or credential-touching work. - The unsupervised two-step moved into the daemon. It used to live only in the board's red checkbox flow, so any loopback process — a fleet-spawned agent included — could
curl /api/spawnitself an unsupervised sibling. An unsupervised body (dangerously_skip_permissionsorpermission_mode: 'bypassPermissions') now needs a single-use, 60-second arm token from the token-gatedPOST /api/spawn/arm-unsupervised. The board's checkbox and the permission-mode dropdown (which used to skip the two-step entirely) both route through it. - The bring-up nudge never answers trust dialogs. It used to press Enter through whatever was on screen — including the folder-trust and MCP-approval prompts, which are the human checkpoints between a freshly cloned repo's
.claude/settings.jsonhooks /.mcp.jsonservers and your credentials. The nudge now reads the pane first: a trust/MCP dialog is held for you with a 🔒 note on the card; anything else keeps the historical one-Enter bring-up. - Tmux ownership checks are exact and fail closed. Formatted pane discovery now works across supported tmux versions, filters to Fleet Deck's exact session before parsing, refuses duplicate scoped names, and distinguishes a validated empty fleet from an unreachable or malformed response. A persisted server-generation UUID also prevents a replacement server at the same socket label from impersonating the inaccessible original. Kill, revive, adopt, reconciliation, liveness, mail delivery and launch compensation no longer turn an UNKNOWN tmux state into “gone” or start a duplicate pane; unverifiable cleanup stays visible and retryable instead.
- Power routes require the token even on loopback. Typing into terminals (
/ws/term),POST /mail,gateway_*settings writes and the unsupervised arm now demand the bearer in every mode — these are the powers a malicious local process must not wield anonymously. Ordinary loopback browsing stays open; the trust zone for everything else is unchanged. - The file explorer refuses credential paths. The global explorer's default root is your home directory, and a bearer holder (a phone on LAN, an agent with the token) could read
~/.ssh,~/.awsand the daemon's own token through it..git,.ssh,.aws,.gnupg,.netrc,.kube,.docker/config.jsonand everything under$FLEETDECK_HOMEnow 404 through the explorer. - Release pipeline hardening. Publishing to npm now requires a human approval (the
releaseGitHub environment) before the OIDC credential mints, and refuses tags whose commit isn't an ancestor ofmain. CI and publish installs runnpm ci --ignore-scripts; dependabot PRs arrive one package at a time instead of grouped; GitHub Actions are SHA-pinned;CODEOWNERScovers the artifacts, hook scripts, workflows and lockfiles; and a new hook-integrity CI job fails any PR that touches the hook scripts or their import closure without a version bump — the path marketplace installs execute at every SessionStart, which the bundle drift gate never saw. - Web hygiene. The board shell sends
Referrer-Policy: no-referrer(the Google Fonts stylesheet used to fire before the token could be scrubbed from the URL), the CSP drops the barews:/wss:connect-src wildcards, and the remote-control URL harvest trims terminal junk from the matched URL.FLEETDECK_TOKENis scrubbed from pane environments.
Changed
- BREAKING: hooks require the token, in every mode. A tokenless
/hook/*call is refused. If you have custom tooling that POSTs hook events, attach the bearer from$FLEETDECK_HOME/token— the same header the shims send. The daemon mints the file on every boot now, loopback included, so it is always there to read. - Upgrade path: restart your running sessions once. Sessions started before this release keep their old (tokenless) hooks loaded, so they stop reaching the daemon until restarted — no telemetry, no permission relay, no mail. Three channels make sure that can't go unnoticed: the daemon answers their tokenless hooks with a restart whisper injected into the session's context (the agent tells you); a session that keeps working regardless is interrupted exactly once with the restart instruction at its next turn boundary; and the board shows a persistent ⬆ RESTART N SESSIONS banner plus a per-card "restart me" tag — both shrink to zero on their own as each session is restarted, since its first authenticated hook clears it server-side. The session that triggers the upgrade takeover also hears in its roster brief which sessions still need restarts.
- BREAKING:
POST /mail,/ws/term, gateway settings writes and unsupervised spawns require the token (or arm token) even on loopback. The fleet doctrine's mail recipe already reads the token file and is now unconditional; anything else calling these routes needs the same header. Your board gets the key from the credentialed link the daemon prints at startup (fleetd board http://127.0.0.1:4711/?t=…). - BREAKING: external mail can no longer wear daemon identities or
[FLEETDECK ...]frames. If you had scripts sending "official-looking" fleet mail, they will 422 — send as yourself instead. - BREAKING (UX): fresh directories wait for you at the trust dialog. The nudge no longer presses through it, so the first spawn in a new clone or directory shows 🔒 on the card until you approve the dialog in the terminal (or the board's terminal modal). Once per directory, then never again.
- Docs: what the boundaries actually are. SECURITY.md now says plainly:
FLEETDECK_REQUIRE_TOKENprotects against other OS users, not your own agents (the token file is owner-readable and$FLEETDECK_HOMEis in every pane's env); LAN mode's risk on a hostile network is active (no token needed — the board's JS can be rewritten in flight, andfleetdeck.localcan be spoofed into a phishing gate), not just passive sniffing; and a marketplace install tracksmain, so cautious users should prefer the npm channel or pin a tag.
v0.15.0 — Per-session LLM gateway routing
Point a session at CLIProxyAPI, a corporate gateway, or anything else that speaks the Anthropic wire format — one session at a time, visible on the board, with the credential never leaving your machine.
Setting ANTHROPIC_BASE_URL machine-wide in ~/.claude/settings.json already worked. What this adds is the per-session choice, which that can't express: a fleet can run some agents through a proxy and send the rest straight to Anthropic.
Getting started
Hit set up next to 🛰 gateway on the Spawn form. For a stock CLIProxyAPI that's http://127.0.0.1:8317 with any entry from its api-keys: list, bearer style. Then tick 🛰 route through … on any spawn — gateway-routed cards carry a 🛰 chip.
| Setting | What it is |
|---|---|
gateway_base_url |
Where the gateway lives. http:// and https:// only, and no credentials in the URL. |
gateway_token |
The credential. Never served back to any client. |
gateway_auth_style |
bearer (default) sends Authorization: Bearer …; api-key sends x-api-key. If you get a 401, it's almost always this. |
gateway_model_discovery |
On by default. Surfaces gateway-only model names in /model. |
gateway_default |
Off by default. When on, a spawn that says nothing routes through the gateway. |
A revive inherits its lineage's routing and deliberately does not re-consult gateway_default — resuming a conversation against a different provider than the one that wrote its transcript changes who is billed with nothing on screen saying so.
⚠️ Breaking: ambient ANTHROPIC_API_KEY
Board-spawned panes no longer inherit ANTHROPIC_* from the daemon's environment. If you authenticate Claude Code with an ANTHROPIC_API_KEY exported in your shell, board-spawned panes will stop seeing it. Sessions you start yourself are unaffected. Move it to ~/.claude/settings.json, which Claude Code reads for itself and Fleet Deck never touches:
{ "env": { "ANTHROPIC_API_KEY": "sk-ant-…" } }That inheritance is what this release removes, and it was a real leak: exporting a gateway once, in whichever terminal happened to trigger SessionStart, baked it into the tmux server's global environment — so every board-spawned session on the machine quietly routed through it for as long as that server lived.
Security
- The credential never leaves the daemon. The settings view serves
token_set: trueand nothing more, because/stateis broadcast to every connected board — phones on LAN mode included. It reaches the pane through tmux's own environment (new-window -e) rather than anenv NAME=secretargv, so it stays out ofpsfor the pane's whole life. gateway_base_urlrefuses embedded credentials. It's deliberately not masked — seeing where a session is going is the point — which is exactly whyuser:pass@host,?api_key=…and fragments are rejected.new URL().hrefpreserves all three, so normalizing wouldn't have caught them.
Also
Remote control and the gateway are refused together, naming the cause. That isn't a Fleet Deck policy: Claude Code disables Remote Control whenever ANTHROPIC_BASE_URL points at a non-Anthropic host, so accepting both would hand back a spawn whose 📱 link never appears and whose failure has nothing to point at. The form greys out whichever you didn't pick.
A half-configured gateway (a URL with no token, or the reverse) fails the spawn rather than quietly falling through to Anthropic.
Full changelog: v0.14.0...v0.15.0
v0.14.0 — SSH-first cloning, a movable browse root, and honest clone failures
Repo-mode spawns work where the repos actually live: private forges over ssh, and Coder workspaces whose real disk is /workspace, not home.
Added
- ssh or https, your pick, for
org/reposhorthand. The spawn dialog's host pills gain a transport row (ssh · git@…/https); the destination hint previews the exact origin either way. The choice is a durable daemon setting written when a shorthand spawn is accepted with an explicit transport — the fleet remembers what you actually use. API:POST /api/spawntakesrepo_transport(ssh|https); absent resolves from the setting. - The file explorer's root is yours to choose. A durable
browse_rootsetting (envFLEETDECK_BROWSE_ROOTbetween setting and default) re-roots the global ⌸ Files explorer and the spawn form's 🗀 folder picker. On a Coder workspace the default is auto-detected as/workspace; everywhere else it stays home. The root is resolved server-side only, and a configured root that has vanished fails loud — never a silent fallback. - Favorite folders. ☆ any folder to pin it (up to 20); chips give one-click jumps in the explorer and the picker, and set as default root re-roots everything from where you stand.
POST /api/settingsaccepts any subset ofrepos_dir,repo_transport,browse_root,fav_dirs— validate-all-then-apply-all, unknown keys refused by name.
Changed
- Shorthand clones default to ssh (was https-always in 0.12.0, which cannot clone private repos from a daemon with terminal prompts disabled). Pick the https pill once and it's remembered; existing checkouts are unaffected — origin matching has been transport-insensitive since 0.12.0.
- On Coder, clones land in
/workspaceby default (was~/projects).
Fixed
- A failed clone now tells you why. Tombstones carry git's
fatal:/error:line instead of a truncatedCloning into '…banner; full stderr goes tofleetd.logplus a durableSpawnFailedevent.
Full changelog: https://github.com/lacion/fleet-deck/blob/main/CHANGELOG.md#0140---2026-07-16