Fleet For Claude

docs/Fleet-for-Claude.md

← All docs

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 window kilter_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/main that 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 local fleet branch) — drifts. As origin/main advances, the local fleet ref 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 of main.
  • Checking out main directlynot allowed. Git refuses to check out the same branch in two worktrees, and main is already held by the primary checkout. Even if you could, it reintroduces the "local ref parked behind the remote" problem.
  • Detached HEAD at origin/mainno 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 just git 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:

CommandScopeWhat it does
/fleetglobalFront door — explain the model (/fleet help) or quick status (/fleet status)
/orchestrateglobalFull read-only dashboard: every worktree's branch · ahead/behind · dirty · PR · tmux window, plus cleanup candidates
/refreshglobalRe-checkout the orchestrator worktree onto origin/main's tip (guarded: only if detached + clean)
/wmglobalWraps workmux so the lifecycle is slash-driven too: /wm add|merge|remove|ls|help
/platform-checkproject-local(kilter-specific example) deploy-pending + read-only cluster health glance

Supporting pieces (work under the hood)

  • tmux-fleet skill — 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-sweep agent — a stateless per-worktree status probe. /orchestrate fans 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/main is set globally in workmux; a repo on a non-main trunk needs a per-repo .workmux.yaml override.
  • 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 add lands 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 if workmux ever pauses for input it hangs. /wm guards mutating commands with timeout 60 … </dev/null and falls back to telling you to run ! workmux … yourself (your pane has a real TTY).
  • Hyphen vs underscore. workmux add foo_bar makes a branch foo_bar but a worktree/window named foo-bar. Use the hyphenated name for /wm merge/remove.
  • Submodules block git worktree remove. If a worktree contains a submodule, plain removal refuses; workmux remove is 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 out origin/main) orphans it. So "no work in the orchestrator" is a discipline, not a git lock. Two guards backstop it:
    1. /refresh refuses to re-anchor when HEAD is ahead of main. It checks git rev-list --count origin/main..HEAD; if there are commits, it stops and prompts you to rescue them (workmux add <slug> --base HEAD, or git 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.
    2. A worktree-local pre-commit hook 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-worktree core.hooksPath, enabled with git config extensions.worktreeConfig true), so other worktrees' hooks are untouched.

Setup (to replicate)

  1. Install workmux; set base_branch: origin/main in ~/.config/workmux/config.yaml.
  2. Drop the commands in ~/.claude/commands/: fleet.md, orchestrate.md, refresh.md, wm.md (global, project-agnostic).
  3. Drop the tmux-fleet skill in ~/.claude/skills/tmux-fleet/SKILL.md and the fleet-sweep agent in ~/.claude/agents/fleet-sweep.md.
  4. 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/.
  5. Start a tmux session named <project>_fleet, run Claude in it, and /fleet to begin.