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_modules

The -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.