claude-fleet
The adapter that lets an agent run Claude Code sessions for you — on this machine and across your other ones. By hand you'd ssh → tmux → claude to a remote box (or just run claude locally); claude-fleet drives that for you — always over tmux, so you can attach to take over any session anytime.
What it is
You already run Claude Code one session per project — terminal → claude, many in parallel. The same pattern works whether the project is on this machine or another — one ladder:
base: claude you run it
remote: ssh → tmux → claude on another machine
agent: adapter → [ssh →] tmux → claude an agent drives it
The base is a Claude session. tmux makes it a durable surface you attach to (and ssh reaches one on another machine). claude-fleet drives that same session for you — exactly as you would by hand — and you can attach to take over any one, anytime. It works over tmux, the handle it drives and you attach to, so its floor is tmux → claude; a bare claude in your own terminal has no handle, so the adapter leaves it alone. A local host just drops the ssh →.
You and chief drive the sessions — and attach to any one anytime:
flowchart LR
A["you / chief"] -->|send| B(claude-fleet)
B -->|"tmux → claude"| S1["session · repo"]
B --> S2["session · repo"]
A -.->|attach| S1
A -.->|attach| S2
Two ideas hold the whole design together:
- Parity — the agent works exactly as you would. One binary, run the same way whether you type
claude-fleet send box2 "..." or chief calls it, and whether the host is local or remote (a local host just drops the ssh → hop). No separate "automation" path.
- A session is live, not fire-and-forget. Both you and chief drive it, and either can
attach to watch, steer, or take over at any moment — you're joining the session, not waiting on a job. To check on work, read the session (read/status); don't trust that send returned or the ssh exit code.
The model & session lifecycle
The tool probes the tmux pane and classifies each host into one of these states. Detection is fail-safe: it keys on structure (a run of ─ box-rule glyphs = the input box; 1./2. lines = a menu) plus claude's esc to interrupt busy footer and the foreground process. Crucially, ready is proven, never a fallback — every ambiguous case lands in a state that refuses send.
| State | Meaning | How it's detected |
|---|
down | no claude tmux session exists | tmux has-session fails |
busy | claude is mid-turn / running a tool | the esc to interrupt footer is present |
gate | at a permissions/accept/trust menu | a numbered menu (1./2. …) with no input box |
ready | idle at the input prompt — safe to type into | the claude TUI owns the pane, the input box is present, and there's no menu or busy footer |
crashed | claude exited to a shell | a shell is the foreground process and there's no input box |
unauth | the credential expired (401) — renders ready but can't work | the pane shows an auth error (authentication_error / Please run /login) — the silent failure |
unknown | unrecognized/ambiguous (a full-screen app grabs the pane, or a half-rendered frame) | a full-screen app (alternate_on), else anything unrecognized — send refuses |
unreachable | the ssh call itself failed | host down / network |
The intended lifecycle is the same whether the session is remote or local: a session starts (down → gate on first run — on a remote host the boot-time systemd unit does this; locally up/attach creates it) → claude-fleet up clears the gate (gate → ready) → claude-fleet send dispatches a prompt (ready → busy) → the turn finishes (busy → ready). If claude ever dies (crashed) up relaunches it; if its token expires (unauth) reauth re-logs-in; for any other wedge, restart kills and recreates the session.
A note on auth. Headless Claude Code can't refresh subscription OAuth (a platform limitation), so the durable credential is a 1-year claude setup-token — not a short-lived copy of a desktop credential. claude-fleet reauth <host> brokers the one browser step and deploys the token. Without this, a session 401s after hours and — crucially — still renders the ready prompt; the unauth state is what stops status from lying about it.
The setup-token is inference-only — fine for dispatch, but Claude Code Remote Control (and other full-scope features) need a real claude auth login; don't reauth such a box (it redeploys the inference-only token).
Guardrails
The driver is deliberately conservative about typing into a live session: