Run several agents at once, and know which of them worked
The unit never changes: one agent, one branch, one worktree, one container. Everything on this page adds something to that unit — a background container, a file describing several of them, a check that decides whether the work is done. None of it changes the boundary, because a fleet task becomes exactly the options a single --worktree run produces.
You can stop at any rung
These are not four features to choose between. Each one is the previous one plus a single addition, and most days the first is enough.
- 1
One agent per branch
--worktree feature-aA git worktree of its own, so two agents never edit the same files or fight over the same branch.
Enough when: You are running one agent and watching it.
- 2
In the background
--detachThe container outlives the terminal, so one window can start several.
Enough when: You want two or three going and will check on them by hand.
- 3
A fleet
fleet runAll of them from one file, plus the answer the rung above cannot give: which of these actually worked?
Enough when: You want the work checked, not just started.
- 4
Handing files over
--shareOne directory two sandboxes can both see, for an artifact that crosses between them.
Enough when: One agent produces something another needs.
~/projects/appYour checkout, untouched and still on whatever branch you had. The worktrees live in a sandbox-owned directory, so the project folder stays clean.
~/.config/sandbox/worktrees/app-9f95/<branch>- feature-a
-p "implement the API"sandbox-dk0gtrd15s2gmem 412MiBcpu 82% - feature-b
-p "port the tests"sandbox-9f2la8hq4vznmem 308MiBcpu 61% - docs/rewrite
-p "rewrite the guide"sandbox-m4x1pq7bd0csmem 196MiBcpu 24%
git diff main...feature-asandbox-cli worktree commit feature-a -m "…"sandbox-cli worktree rm feature-aA fleet is one file and one command
Write a fleet.yaml next to your project. Every task gets its own branch, its own worktree and its own detached container; your checkout is never touched and never changes branch.
agent: claude # the default for tasks that name no agentmax_parallel: 2defaults:memory: 4gcpus: "2"git: true # so the agents' commits carry your name and emailtasks:- branch: feature-loginprompt: Implement the login form in src/auth/. Add tests. Commit when they pass.verify: go build ./... && go test ./...- branch: feature-ratelimitagent: codex # a different agent for this branchmemory: 8g # and its own limitsprompt: Add per-IP rate limiting to src/server/. Add tests. Commit when they pass.verify: go test ./src/server/...
Then the whole cycle, all of it from your normal checkout. fleet run looks for fleet.yaml in the current directory; -f path names another.
- 1Terminal$sandbox-cli claude
Log in once per agent. A detached container cannot answer a login prompt, so every agent the file names needs this first.
…and again for each other agent: sandbox-cli codex
- 2Terminal$sandbox-cli fleet run --dry-run
See what each task would do — the agent argv, the verify, the limits, the mounts — without launching anything.
- 3Terminal$sandbox-cli fleet run
Fan out. One branch, one worktree and one container per task.
- 4Terminal$sandbox-cli fleet status
One line per branch: which agent, whether it is running, how long, what it left uncommitted, how far ahead it is.
--watch redraws until you stop it
- 5Terminal$sandbox-cli fleet logs feature-login
What one agent actually said. Works after it exits, because fleet containers are kept.
-f to follow it live
- 6Terminal$sandbox-cli fleet land --all
Commit whatever each agent left, then merge every branch that can be merged, oldest first.
or one at a time: sandbox-cli fleet land feature-login
- 7Terminal$sandbox-cli fleet clean --worktrees
Reap the finished containers, and the checkouts too — skipping any with uncommitted work rather than discarding it.
When part of it goes wrong
You do not re-run the file. Commenting the other tasks out is the thing people reach for, and a fleet file with half its tasks commented out is one that will be run that way again by mistake.
$sandbox-cli fleet run --only feature-login
Retry the one task that failed. A branch the file does not contain is an error listing the ones it does — launching nothing looks exactly like success.
$sandbox-cli fleet run --resume
Pick up an interrupted run: skip branches whose agent is still working and branches that already exited 0, start the rest.
Passed by name, and read in a container that holds two directories
Two things account for most of the confusion a first fleet produces: the file is not found for you, and a task can see far less of your machine than the prompt writing it assumes.
Named, never discovered
$sandbox-cli fleet run -f fleet.yml
fleet run reads exactly the path you give it, and defaults to ./fleet.yaml. It never walks up the directory tree looking for one, unlike .sandbox.yaml. That is what lets the file carry command-line trust: naming it is an act you performed, so a fleet.yaml in a parent directory or a repository you cloned can never be picked up by accident.
A typo is an error, not a default
$line 1: field max_parallell not found in type fleet.Spec
Unknown keys are rejected rather than ignored. max_parallell: 4 fails the load, because the alternative is a fleet running with limits its author believed they had set — including the memory caps.
-f and -c are different files
$sandbox-cli fleet run -f ci.yml -c ~/.config/sandbox/prod.yaml
-f is the task list. -c is the sandbox config, and it is where image:, mounts: and env_allow: live — a task does not choose what it runs in, so a fleet file has no key for any of them.
What a task can actually see
An agent asked for something that is not mounted does not invent it — it reports the path as missing, which reads as the agent failing rather than as the prompt naming somewhere it was never going to reach.
The worktree, the login, and the repository's .git
/workspace is the worktree and /sandbox/home is the agent's persisted login. A worktree task also gets the parent repository's .git — read-write, mounted at its own host path, because a linked worktree cannot commit without it — with .git/hooks read-only over the top. Nothing else on your machine is there: not /tmp, not your other repositories, not the skills your own Claude Code has. An agent told to write outside those paths writes into a container that is about to be thrown away.
Skills travel with the repository, not with you
A skill committed at .claude/skills/ arrives inside /workspace and every task sees it. One installed at ~/.claude/skills/ on the host does not — the container's HOME is a different directory entirely. It has to be committed, too: a worktree is a checkout of the branch, so an untracked skill in your main checkout is invisible to it.
The handover directory is opt-in
/shared exists only when you pass --share, and a fleet file cannot turn it on. Without the flag an agent asked to read /shared/x is being asked for a path that is not there, which it will report as missing rather than invent.
The image is not the fleet's to choose
The base image carries node and python3 and no other toolchain — no pip, no go, no compiler beyond build-essential. A verify: of go test or pip install exits 127 in it. The fix is a config layer rather than a fleet one: image: in your own config, or -c naming one.
A fleet container has no keyboard
Deliberately, and it is not the same as being unreachable. Nothing is attached to a background container, so an agent that stopped to ask a question would not fail — it would hang, holding a slot until somebody noticed.
You read it, you do not type at it
$sandbox-cli fleet logs feature-login -f
A fleet container is created with no tty and no stdin, so there is nothing to answer a question with. That is the point: a fleet is unattended, and an agent that stopped to ask would not fail but hang, holding a max_parallel slot. To watch one live, sandbox-cli attach <branch> — the top-level command, since there is no fleet attach — streams its output and says in as many words that it has no keyboard.
Silence is usually not a hang
$sandbox-cli fleet status --watch
An agent in headless mode buffers and prints its answer at the end rather than narrating as it goes, so an empty log a minute in is normal. fleet status is the better progress signal: it reports what each branch has produced — commits, uncommitted files, how far ahead it is — rather than waiting on stdout.
To talk to one, run it interactively instead
$sandbox-cli fleet stop feature-login$sandbox-cli claude --worktree feature-login
Same worktree, same branch, attached. Stop the fleet task first: nothing refuses this for you. The duplicate-name rule that enforces one agent per branch only applies to detached containers, and a foreground run is named for the moment it started — so two agents will happily edit one worktree, which is the silent loss that rule exists to prevent everywhere else.
Claude on one branch, Codex on another
agent: at the top of the file is the default. A task that names its own overrides it — so you can put two agents on the same problem and compare what comes back, or use whichever is better at each job.
agent: claudetasks:- branch: feature-loginprompt: Implement the login form.- branch: feature-ratelimitagent: codexprompt: Add per-IP rate limiting.
The fleet-wide agent: becomes optional once every task names one. Mixing costs nothing at the boundary — each agent gets exactly the container it would get on its own. What it does cost is setup: every agent you name needs its own login before the run, because none of them can answer a login prompt from a detached container. sandbox-cli fleet run --dry-run prints a reminder when it sees a mixed file.
Which agents are eligible
A fleet starts every agent detached, so an agent may only appear in a file if it has a verified headless mode — a way to run a prompt to completion without ever asking a human anything. An agent that stops for approval in a fleet does not fail; it hangs until you notice, holding a slot.
| agent: | What the fleet runs | Delivery |
|---|---|---|
| cline | cline PROMPT --auto-approve trueThe prompt is a bare positional and the TUI is the opt-in (-i), which is the inverse of the others. --auto-approve is passed explicitly rather than relying on its default, because an unattended run that starts asking does not fail — it hangs. | on first use |
| claude | claude -p PROMPT --dangerously-skip-permissions | in the image |
| codex | codex exec PROMPTCodex applies its own approval policy on top; relax it through the task's args:. | in the image |
| gemini | gemini --yolo -p PROMPT-p alone runs to completion and then stops at a tool it wants confirmed, so --yolo is not optional here. | in the image |
| opencode | opencode run PROMPT | in the image |
Anything else is rejected when the file is parsed, before a single container starts. The other 9 adapters are perfectly usable interactively — they are simply not ones we have confirmed will never stop and wait. Adding one to this list means running it and recording the argv, which a test pins, so it cannot grow by guesswork.
Per-task limits
A task may also raise its own memory, cpus and allow, for the one branch that needs a bigger build or one more domain. The two rules are not symmetric, on purpose: memory and cpus replace the fleet-wide value, and allow adds to it. A task that could subtract from the allowlist would be asking for less egress than the file's author wrote a line above it; the way to want that is to move the domain onto the tasks that need it.
verify: is what makes a run autonomous rather than merely unattended
Without it, a fleet is a fan-out with a nicer status table: every agent succeeds the moment it stops talking — including the one that confidently did nothing, and the one that deleted the failing test.
How it runs
The command runs inside the container, after the agent, and its exit code becomes the container's. Inside because a check running on your host would be host code selected by a file the agent can write. After the agent, whatever the agent's own exit code was — an agent that exits non-zero having left a tree that builds and tests clean has done the job, and one that exits 0 having deleted the test file has not. The task's definition of done gets the last word.
What it cannot do
The command is fixed, but its meaning is not: go test ./... runs agent-written tests over an agent-written tree, so an agent that deletes the failing test passes. This makes forging a pass require editing the tests rather than merely claiming success — a real improvement over an exit code alone, and not the same thing as being unforgeable.
Exit 90 means the agent finished and its verify said no. A task with no verify still runs — this is a fleet of agents, not a CI system — but it lands reported as unverified rather than passed, because nothing checked it.
The only command that writes to your base branch
fleet land commits whatever the agent left in its worktree, then merges the branch (--no-ff) into the one you have checked out. It refuses rather than guessing, and --all sorts those refusals into two kinds.
| It refuses when… | Under --all | Why |
|---|---|---|
| The agent is still running | skips this branch | Its next action could change what you just merged. |
| The work failed its verify | skips this branch | Nothing has said this work is right. --force lands it anyway, and says so. |
| There is nothing to merge | skips this branch | No commits beyond the base, so a merge would be of zero commits. |
| Your checkout moved since launch | stops the run | Each container records the branch its work was meant for. Landing onto a branch nobody chose needs a rewrite to undo. --onto says you mean it. |
| The base checkout is dirty | stops the run | The merge commit would sweep up your unrelated in-progress work. |
| An agent is working in the base checkout | stops the run | The merge rewrites files under it, mid-edit. |
| The merge conflicts | stops the run | It stops with git's own message and leaves the merge in place. land never resolves anything itself. |
The split is the design, not a convenience. A problem with the branch skips it and the rest carry on. A problem with the branch being merged into stops there, because it will be just as wrong for the next branch and landing more on top of it makes it harder to undo. What already landed is printed either way — those merges are commits in your branch whatever happens next.
The parts that refuse before you find out the hard way
Each of these exists because the failure it prevents is silent, expensive, or both.
One agent per branch
Enforced by construction, not by a check: a detached container is named sandbox-<repo>-<branch>, and docker refuses a duplicate name. Two agents in one checkout lose work silently.
The fleet has to fit in the machine
Before anything starts, sandbox-cli multiplies how many agents run at once by the widest per-task memory cap and compares it with what the host has. Too big and it refuses, naming the arithmetic. It is the concurrent count, not the task count — twenty tasks at max_parallel: 2 is two agents' worth.
A fleet never touches your own session
fleet stop --all does not reach an interactive --detach session in the same repository, fleet clean does not reap one, and max_parallel does not count one. sandbox-cli list marks which is which, because that is where you decide what to kill.
A fleet agent is a session
fleet status prints the same id sandbox-cli list does, and logs, attach and kill all take a branch name — so there is one way to reach a running agent whatever started it.
Run it under --profile prod
dev warns when a control cannot be satisfied; prod refuses. Nobody is watching a fleet, so a warning goes into a log no one reads. prod also declines to mount the persisted login, so each agent needs its key in the environment instead.