Chapter 11. Claude Code Agent Teams, Fleets, and Parallel Work
Claude Code agent teams are experimental and off until
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 is set in the
environment or under env in settings.json.
Enabled, a team is a lead session, teammates that are separate Claude
Code instances, a shared task list claimed with file locking, and a
mailbox.
The survey I ran for this chapter caught the machine in a normal
state: six claude processes running at once, and a load
line reading
18:31 up 3:40, 1 user, load averages: 16.28 11.03 7.92. The
one-minute average sat close to twice the fifteen-minute average, which
means the concurrent work had ramped up in the previous few minutes
rather than idling at a steady hum. Multiple agent sessions against one
Mac is my standing operating condition, recorded in my own memory notes
on 2026-06-10 as the baseline rather than the exception. This chapter
covers both halves of that arrangement: the multi-agent machinery Claude
Code ships, and the discipline that keeps six concurrent sessions from
destroying each other’s work on one filesystem.
How to enable agent teams, and what you get
Agent teams are experimental and disabled by default, and the docs
are plain about both. Without
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 in the environment
or under env in settings.json, no team is set
up at session start, no team directories get written, and Claude neither
spawns nor proposes teammates.
The shape once enabled: a team lead, which is the main session; teammates, which are separate Claude Code instances; a shared task list; and a mailbox. Tasks carry three states (pending, in progress, completed), support dependencies that unblock on their own, and are claimed with file locking so two teammates cannot grab the same task. The mailbox validates entries on read: invalid entries are reported as errors and removed while valid messages still deliver. Before v2.1.207, one malformed entry blocked that mailbox. Plumbing this young still has its sharp edges documented in its own changelog.
Storage is keyed by a session-derived team name:
session- plus the first eight characters of the session ID.
Team config lands at
~/.claude/teams/{team-name}/config.json, the task list at
~/.claude/tasks/{team-name}/, and mailboxes at
~/.claude/teams/{team-name}/inboxes/{agent-name}.json. The
config directory is removed at session end; the task directory persists
under cleanupPeriodDays. Two traps are worth naming. A
project-level .claude/teams/teams.json is not recognized as
configuration, so authoring one buys nothing. And
config.json holds runtime state (session IDs, tmux pane
IDs) that gets overwritten on the next state update, so hand-editing or
pre-authoring it loses too.
Display is governed by teammateMode:
in-process is the default, with auto,
tmux, and iterm2 available (the iTerm2 mode
needs v2.1.186 and the it2 CLI). Split panes require tmux
or iTerm2 and are unsupported in VS Code’s integrated terminal, Windows
Terminal, and Ghostty. The per-session override,
claude --teammate-mode auto, is an experimental flag that
does not appear in claude --help. A working flag that hides
from help is a fair sample of this surface’s maturity.
The documented limitations draw the boundaries. One team per session,
no sharing across sessions. No nested teams: teammates cannot spawn
teammates. The lead is fixed for the session and cannot be transferred.
/resume and /rewind do not restore in-process
teammates. An in-process teammate cannot run background subagents;
asking returns an error. Teammates start with the lead’s permission
mode, and per-teammate modes cannot be set at spawn time. The docs
recommend starting with 3-5 teammates and 5-6 tasks per teammate, with
no hard ceiling on team size. Teammates can be required to plan first,
staying in read-only plan mode until the lead approves, and a rejected
plan keeps the teammate in plan mode to revise. The lead grants those
plan approvals without a separate prompt to the user, the one designed
exception to teammate permission prompts surfacing in the lead
session.
Model selection has its own wrinkle. Teammates do not inherit the
lead’s /model selection by default; the default comes from
“Default teammate model” in /config, where choosing
“Default (leader’s model)” makes them follow the lead. A teammate’s
model and fast mode are fixed at spawn, so /model and
/fast affect the lead alone, while /effort
applies to the viewed teammate’s later turns and teammates inherit the
lead’s effort level.
A subagent definition can serve as a teammate spec, with caveats the
docs spell out. The teammate honors the definition’s tools
allowlist and model, and the definition body is appended to
the teammate’s system prompt rather than replacing it. The
skills and mcpServers frontmatter fields are
not applied on this path; teammates load skills and MCP servers from
project and user settings instead. And SendMessage plus the
task tools stay available even when tools restricts the
rest, since a teammate that cannot reach its team is dead weight.
The security model matters more than the ergonomics. A message
arriving over SendMessage is presented to the receiving
agent as coming from another Claude session, not from the user. A
teammate cannot approve a permission prompt or supply consent on my
behalf, a denied action cannot be relayed through another teammate to
bypass the check, and in auto mode the classifier treats a relayed
approval claim as untrusted input. On a machine where six agents run at
once, an approval that can be laundered between agents would be the
first thing to go wrong.
One more thing the docs record: churn. As of v2.1.178 the
TeamCreate and TeamDelete tools no longer
exist, the team_name input on the Agent tool is accepted
but ignored, and the team_name field in team hook payloads
carries the session-derived name and is deprecated. That is what
experimental means in practice: the API moved under existing scripts
within a handful of releases. My parallel work today runs on plain
concurrent sessions, not on teams, and the incident record below comes
from that practice. The team machinery earns its space here because its
constraints echo lessons I paid for before the feature existed.
Parallel subagents inside one session, and their caps
Below teams sits the fleet available inside a single session:
subagents. As of v2.1.198 they run in the background by default, and a
background subagent’s permission prompts surface in the main session
naming the subagent that asked (v2.1.186 and later). Ctrl+B
backgrounds a running task. The documented scale knobs: a session limit
of 200 subagents by default, raised with
CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION; a concurrency limit
of 20 running subagents, changed with
CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS; and a nesting depth
set with CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH, defaulting
to three layers below the main conversation. Exceeding the first fails
the Agent tool with Subagent spawn limit reached; exceeding
the second fails with
Concurrent subagent limit reached.
Then comes version reality. This machine runs Claude Code 2.1.211, which sits under several of those gates.
| Feature | Requires | On this machine’s 2.1.211 |
|---|---|---|
/subtask forked-subagent command |
v2.1.212 | absent; the command is still /fork
(v2.1.161-v2.1.211) |
CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION |
v2.1.212 | absent |
CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS |
v2.1.217 | absent |
CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH |
v2.1.217 | absent |
Skill background: false, extended boolean
frontmatter |
v2.1.218 | absent |
| Depth-3 nesting default | v2.1.219 | n/a; v2.1.172-v2.1.216 fixed depth at five layers |
on this install, nesting sits at five layers with no way to change
it, the fork command is /fork, and the cap knobs in the
current docs do not exist yet. Reading version-gated documentation
against the installed binary is part of the job; several knobs on the
subagents page postdate mine.
The harness has also grown its own version of the rule this chapter
turns on. A subagent declared with isolation: worktree runs
in a temporary git worktree branched from the default branch rather than
the parent session’s HEAD, cleaned up if it makes no changes. From
v2.1.216, a Bash command that redirects git into the main checkout
fails, including through git -C, --git-dir,
GIT_DIR, GIT_WORK_TREE, or a preceding
cd. That enforcement is above my installed version, but the
direction is the point: the harness converged on the invariant I learned
by losing work. Writers get isolated worktrees, and the main checkout
stays untouched.
The branch switch that erased my uncommitted edits
On 2026-06-10 I was editing ~/code/Suede-AI-App in the
main checkout, the shared one at the canonical path, while other
sessions ran elsewhere on the machine. One of those sessions switched
that checkout from main to its own feature branch,
feat/master-registry-consolidation, mid-task. My
uncommitted edits went with it. The memory note I wrote afterward keeps
the phrasing from the moment: “two edits in, everything was gone.” No
error, no prompt, no conflict marker. A branch switch by any session
wipes every other session’s uncommitted work in that checkout, because
the checkout is shared mutable state and git has no reason to know that
six processes think they own it.
The same session had also appended its own dev-server entry, named
“registry-dev”, to ~/.claude/launch.json. That file is the
machine’s port registry and a second shared-state surface: sessions that
all need dev ports and all write to one JSON file will collide there
too, at lower stakes.
Cutting a
worktree from origin/main for each task
The fix is structural: no task edits the main checkout of a busy repo, and each task gets its own worktree in a sibling directory.
git -C ~/code/Suede-AI-App worktree add ~/code/Suede-AI-App.worktrees/<task-name> -b <branch> origin/main
cp ~/code/Suede-AI-App/frontend/.env.local ~/code/Suede-AI-App.worktrees/<task-name>/frontend/Three choices sit in those two lines. The branch point is
origin/main, not local main, because this
machine is a stale mirror of the computer that pushes to GitHub. On
2026-06-08 this same checkout sat 458 commits behind
origin/main on an abandoned branch,
feat/nft-collectibles-gallery, with a paused
REBASE_HEAD and uncommitted work; other worktrees on the
machine were 48 to 449 commits behind. A worktree cut from local state
inherits all of that drift. The copy line exists because
frontend/.env.local is gitignored and required for dev, so
a fresh worktree from origin/main arrives without it. The
third step happens off-screen: the new dev server gets registered in
~/.claude/launch.json on a unique port.
The historical records disagreed on ports, so I checked the live
registry for this revision. On 2026-07-29,
~/.claude/launch.json holds 41 configurations on 41 unique
ports; the relevant entries put strumly on 3001, suede-muse on 3007,
suede-agent-studio on 3210, and registry-dev on 3333. The older incident
note’s 3017 and 3211 values are stale. Read the registry before claiming
a port, because summaries drift and the file is what sessions collide
on.
A companion rule rides along. Dirty state found in a checkout gets preserved as found: not resolved, not aborted, not stashed. The paused rebase and uncommitted work on that abandoned branch were left alone, because on this machine dirty state may belong to another session or another computer, and no single session has standing to decide it is garbage.
The worktree census on this machine
The survey behind this chapter, run read-only with no fetch against
any remote, counted 64 top-level directories in ~/code
excluding the worktree siblings, and adoption of the rule is measurable
from there. Sixty of them are git repos; the other four (INBOX, backups,
handoffs, the-signal-chain.fleet) are not. Alongside them sit 33
*.worktrees parent directories holding 52 worktrees in
total.
| Worktree parent | Worktrees |
|---|---|
Suede-AI-App.worktrees |
21 |
suede-promo.worktrees |
8 |
suede-voice.worktrees |
4 |
suede-brand-domains.worktrees |
3 |
| six more repos | 2 each |
| four more repos | 1 each |
The distribution tracks the collision risk. Suede-AI-App is the
monorepo where the 2026-06-10 wipe happened and the repo the most
sessions touch, and it carries 21 worktrees, over a third of the
machine’s total. Meanwhile 19 of the 33 parent directories are empty,
zero subdirectories, shells left behind by prior cleanups. An empty
.worktrees shell is the residue of the system working as
intended: tasks got worktrees, finished, and were removed.
The same survey caught the guarded-against failure mode live. The
suede-agent-studio main checkout is in detached HEAD state,
on no branch, with a dirty tree: src/app/landing.css
modified plus an untracked fastlane/ directory. That work
is anchored to nothing and would be easy to lose. Under the preserve
rule it stays as found. The Suede-AI-App main checkout is
on main but dirty with 10 changed entries. Both readings
are local truth only. Without a fetch they say nothing about origin, and
on this machine local truth and origin truth diverge by default.
What worktrees do not isolate: the disk
Worktrees isolate branches. They do not isolate the disk, and 52
worktrees plus their builds and dependency trees draw on one pool. On
2026-07-22 that pool hit zero. Every Bash call in my session failed with
ENOSPC: no space left on device, including
df -h itself, and Write failed everywhere, scratchpad and
repo alike. The cause was other concurrent sessions’ worktrees, builds,
and node_modules. The condition healed on its own: free space went from
0 to 4-5GB within 1-2 minutes as another session’s build finished. Two
rules came out of that day. First, retry a plain df -h /
after a short wait before concluding the machine is unrecoverable.
Second, no blind cleanup: with Bash down there is no safe
ls or du to find what is removable, and
guessing risks deleting another session’s work in progress.
The npm variant is sharper. The Suede-AI-App/frontend
install is 3.1GB+ across a heavy web3/wallet SDK dependency tree, and it
can fail with ENOSPC mid-install even when df shows a few
GB free, because npm’s tarball-extraction peak exceeds the final
installed size. Free-space math has to budget for the peak, not the
result. The workaround on APFS: when a sibling worktree or the main
checkout holds a working node_modules and an identical
package-lock.json (diff them first to confirm), clone it
instead of installing.
cp -Rc <source>/node_modules./node_modulesThe -c flag uses APFS clonefile(),
copy-on-write. Measured on the 3.1GB, 1173-entry tree: df
stayed flat within 0.1GB across the clone, since blocks duplicate only
if modified later. The wall clock still charges for the file count,
minutes for a tree that size, so a cp -Rc running past
about 180 seconds gets backgrounded rather than declared hung.
One instrument note closes the loop. df -h / on this Mac
reports the sealed read-only system volume, 12Gi used at 11% capacity,
which reads as an almost-empty disk. The real number lives at
/System/Volumes/Data: 342Gi used of 466Gi, 99Gi available,
78% capacity, and both mounts share the same 99Gi pool. Whatever that
pool holds, five other writers share it with whichever session is
asking.
Where the harness and my session habits agree
The team docs and my session habit answer the same question at
different layers: how do several agents write at once without writing
over each other? The harness’s answers are file-locked task claims,
validated mailboxes, permission prompts that cannot be approved by a
peer agent, and worktree-isolated subagents. Mine are a worktree per
task cut from origin/main, a gitignored env file copied by
hand, a port registry checked before it is trusted, and a standing order
to leave other sessions’ dirty state alone. Where the two converge, on
isolation as the default and shared files treated as contended, I trust
the convergence because I hold receipts for the alternative: the wiped
edits of 2026-06-10, the 458-commit drift found on 2026-06-08, and the
machine-wide ENOSPC of 2026-07-22. Each rule in this chapter is the
residue of one of those.