Fleet For Claude
docs/Fleet-for-Claude.md
Fleet for Claude — orchestrating parallel Claude Code worktrees
A lightweight system for running many Claude Code agents in parallel, each in its own
git worktree, all coordinated from one "mission control" session. Built on
workmux (git-worktree + tmux orchestrator) plus a handful of
Claude Code slash-commands, a skill, and an agent.
The whole thing is project-agnostic — the example below uses a repo called kilter,
but it works in any git repo whose trunk is main.
The problem it solves
You want to run several tasks at once — a fix here, an investigation there, a doc
update — without them tripping over each other in one checkout. Git worktrees give each
task its own working directory + branch. But worktrees alone are fiddly: you have to
remember to branch off fresh main, wire up a terminal, launch the agent, and later
clean it all up. And once you have five of them, you lose track.
Fleet makes each task a one-liner to start, a window away to drive, and a one-liner to land or drop — with a dashboard to see everything at once.
The model: a hub session, one window per worktree
- There's a hub tmux session named
<project>_fleet(e.g.kilter_fleet). It's mission control. - Each task runs as a window under that hub session. Launching a task creates:
a new git worktree (off
origin/main) + a tmux window + a Claude agent running in it. So windowkilter_fleet:3↔ the worktree for that task. - Unrelated projects keep their own tmux sessions — they're not windows under your hub. The hub is per-project.
- One worktree is the orchestrator itself: a worktree sitting on a detached HEAD at
origin/mainthat does no work of its own — it's just the stable vantage point you run the dashboard and launches from. (See "Why the orchestrator runs detached" below.)
kilter_fleet (hub tmux session)
├─ window 1: orchestrator (detached-on-main worktree — watch & launch only)
├─ window 3: sops-flow → worktree …/kilter__worktrees/sops-flow
├─ window 4: update-docker-file→ worktree …/kilter__worktrees/update-docker-file
└─ window 6: explore-… → worktree …/kilter__worktrees/explore-…
Why the orchestrator runs detached (the drift problem)
"Drift" is the silent gap that opens between a local branch tip and the canonical remote
main. It's the core hazard of a merge-to-main worktree workflow: you branch off main,
work, merge the PR — but your local branch ref stays parked at the old commit. It doesn't
follow main forward. A week later that ref is 20 commits behind, and the next task that
branches off it inherits stale code without anyone noticing.
The orchestrator is long-lived and you return to it constantly, so it's the worktree most exposed to drift. Three ways it could be set up, and why detached wins:
- A named branch pinned to
main(e.g. a localfleetbranch) — drifts. Asorigin/mainadvances, the localfleetref sits still unless you remember to fast-forward it. It also looks like a real branch, which invites accidental commits — and the moment one lands, it's a genuinely diverged branch, not a mirror ofmain. - Checking out
maindirectly — not allowed. Git refuses to check out the same branch in two worktrees, andmainis already held by the primary checkout. Even if you could, it reintroduces the "local ref parked behind the remote" problem. - Detached HEAD at
origin/main— no ref to drift. The worktree is anchored to a specific commit, not a branch, so there's no persistent local branch that can silently fall behind. "Refreshing" is justgit fetch origin main && git checkout origin/main— re-anchor to the new tip. And because the orchestrator carries no work of its own, re-anchoring is always safe: nothing to merge, nothing to reset, nothing to lose. Drift simply can't accumulate, because there's no branch holding a stale position.
The rule that makes this airtight: no work in the orchestrator worktree. The moment it
carries a commit, it stops being a disposable mirror of main and becomes just another
worktree that can drift.
The pieces
workmux — the shell tool that does the work
Creates/merges/removes worktrees + their tmux windows + agents. Key commands:
workmux add <slug> (create), merge (merge branch + clean up), remove (drop
unmerged), ls (status), resurrect (restore windows after a crash), dashboard (TUI).
Configured once in ~/.config/workmux/config.yaml with base_branch: origin/main so
every worktree branches off fresh trunk.
Claude slash-commands (the interface)
These are what you type into Claude. They're the consistent front door:
| Command | Scope | What it does |
|---|---|---|
/fleet | global | Front door — explain the model (/fleet help) or quick status (/fleet status) |
/orchestrate | global | Full read-only dashboard: every worktree's branch · ahead/behind · dirty · PR · tmux window, plus cleanup candidates |
/refresh | global | Re-checkout the orchestrator worktree onto origin/main's tip (guarded: only if detached + clean) |
/wm | global | Wraps workmux so the lifecycle is slash-driven too: /wm add|merge|remove|ls|help |
/platform-check | project-local | (kilter-specific example) deploy-pending + read-only cluster health glance |
Supporting pieces (work under the hood)
tmux-fleetskill — teaches Claude the session/window↔worktree mapping and the safe tmux verbs (read / capture / send / launch), graduated by risk (reading is free; sending keystrokes into a live pane needs confirmation).fleet-sweepagent — a stateless per-worktree status probe./orchestratefans it out in parallel (one per worktree) to build the dashboard fast.
Daily flow
/wm add my-fix "what to do" # spin up worktree + window + agent, off origin/main
# (no description → empty pane you brief yourself)
# … switch to the new window (Ctrl-b <n>) and drive the agent …
/orchestrate # see where everything stands: ahead/behind, PRs, cleanup
/wm merge my-fix # land it (merge branch, remove worktree + window)
# …or…
/wm remove my-fix # drop it unmerged
/refresh # keep the orchestrator worktree current with main
Mental model: read/guide → a slash command to Claude; create/merge/remove → /wm.
Everything is a slash command; /wm is the only one that reaches into the shell.
Conventions & config
- Trunk is
main.base_branch: origin/mainis set globally in workmux; a repo on a non-maintrunk needs a per-repo.workmux.yamloverride. - Hub session name =
<main-repo-basename>_fleet(derived from the main repo, not the current worktree). - Worktree paths =
<repo>__worktrees/<slug>(workmux's layout). - Launch from the hub session so the new window lands under it. Claude's session
normally runs inside the hub, so
/wm addlands correctly.
Gotchas (learned the hard way)
- Command precedence is
user > project. A user-level (~/.claude/commands/) command shadows a same-named project command — the project one can never override it. So global commands win everywhere; project-specific extras must use a different name (that's why the kilter deploy/cluster checks live in/platform-check, not a project/orchestrate). - No TTY when Claude runs
workmux. The Bash tool has no interactive terminal, so ifworkmuxever pauses for input it hangs./wmguards mutating commands withtimeout 60 … </dev/nulland falls back to telling you to run! workmux …yourself (your pane has a real TTY). - Hyphen vs underscore.
workmux add foo_barmakes a branchfoo_barbut a worktree/window namedfoo-bar. Use the hyphenated name for/wm merge/remove. - Submodules block
git worktree remove. If a worktree contains a submodule, plain removal refuses;workmux removeis clean for submodule-free worktrees. - Detached ≠ commit-proof. A detached HEAD does not stop you from committing — git
allows it; the commit just isn't on any branch, and the next
/refresh(which checks outorigin/main) orphans it. So "no work in the orchestrator" is a discipline, not a git lock. Two guards backstop it:/refreshrefuses to re-anchor when HEAD is ahead ofmain. It checksgit rev-list --count origin/main..HEAD; if there are commits, it stops and prompts you to rescue them (workmux add <slug> --base HEAD, orgit branch salvage/<topic> HEAD) before re-checkout. Crucial because a committed tree is clean — a dirty-check alone would sail right past the mistake and orphan the work.- A worktree-local
pre-commithook in the orchestrator refuses commits while HEAD is detached, blocking the mistake at the source. It's scoped to the orchestrator only (via a per-worktreecore.hooksPath, enabled withgit config extensions.worktreeConfig true), so other worktrees' hooks are untouched.
Setup (to replicate)
- Install
workmux; setbase_branch: origin/mainin~/.config/workmux/config.yaml. - Drop the commands in
~/.claude/commands/:fleet.md,orchestrate.md,refresh.md,wm.md(global, project-agnostic). - Drop the
tmux-fleetskill in~/.claude/skills/tmux-fleet/SKILL.mdand thefleet-sweepagent in~/.claude/agents/fleet-sweep.md. - Per project, optionally add an orchestrator worktree (detached on
origin/main) and any project-specific commands (separately named, e.g./platform-check) in that repo's.claude/commands/. - Start a tmux session named
<project>_fleet, run Claude in it, and/fleetto begin.