Chapter 10. Claude Code Subagents

A subagent is a second Claude spawned from my session with its own context window: it receives a task message, works, and hands back one report. A Markdown file with YAML frontmatter under .claude/agents/ or ~/.claude/agents/ defines one, and the report is the whole product. The tens of thousands of tokens of grep output, dead-end file reads, and abandoned hypotheses it burned through on the way stay inside its window and die with it. My main thread pays for a task message going out and a summary coming back.

That accounting is the reason subagents exist. Tool scoping, model override, and worktree isolation are real levers, but they are riders on the primary contract: context isolation. The main session’s window is the scarcest resource in this whole setup, and a subagent spends a different one.

The definition file, and the five places it can live

A subagent definition is a Markdown file with YAML frontmatter: frontmatter for configuration, body for system-prompt material. Claude Code reads definitions from five places, highest priority first.

Priority Source Scope
1 Managed settings Organization-wide
2 --agents CLI flag Current session
3 .claude/agents/ Project
4 ~/.claude/agents/ User, all projects
5 A plugin’s agents/ directory Wherever the plugin is enabled

The filename does not have to match the name field, and on my disk the two match in one file out of 61. My ~/.claude/agents/ holds 61 flat files named <domain>-<role>.md: 32 engineering-, 10 security-, 9 design-, 8 testing-, plus impeccable-manual-edit-applier.md with no domain prefix. Sixty of the 61 declare a display-string name instead of the slug: engineering-code-reviewer.md declares name: Code Reviewer, testing-reality-checker.md declares name: Reality Checker. Skills bind identity to a directory name; agents bind it to the name field, and subfolders under project or user agents/ change nothing about identity. Plugin agents are the exception: agents/review/security.md in plugin my-plugin registers as my-plugin:review:security, subfolder included.

The documented frontmatter surface: name and description required, then tools, disallowedTools, model, permissionMode, maxTurns, skills, mcpServers, hooks, memory, background, effort, isolation, color, and initialPrompt. The --agents JSON flag accepts the same fields, with prompt standing in for the Markdown body. What my 61 files use is a different picture.

Field Documented Files using it (of 61)
name required 61
description required 61
emoji no 60
color yes 60
vibe no 59
tools yes 1
model yes 1
effort yes 1
maxTurns yes 1
the other nine documented fields yes 0

emoji and vibe appear in no documented field list. Sixty of my files carry a single glyph and 59 carry a one-sentence character line, and nothing complains: unrecognized keys ride along inert. color is documented, with eight accepted values (red, blue, green, yellow, purple, orange, pink, cyan) controlling display in the task list and transcript. Which leaves the execution-relevant configuration of my whole fleet in one file:

name: impeccable-manual-edit-applier
description: Applies leased Impeccable live manual copy-edit batches to source and returns canonical Apply results.
tools: Read, Write, Edit, Bash, Glob, Grep
model: inherit
effort: medium
maxTurns: 12

Two details in it are worth stealing. tools here is a comma-separated inline string, not a YAML list, and the loader takes it. And maxTurns: 12 puts a hard ceiling on how long the worker can wander before it has to report.

The other 60 files run on defaults, and the defaults carry them: omit tools and the agent inherits every tool a subagent can hold; omit model and it runs on the session’s model. A useful subagent needs a name, a description sharp enough that the right task routes to it, and a body that sets its behavior. The rest is tuning.

There is no wizard anymore. As of v2.1.198, /agents prints a reminder to edit .claude/agents/ and does nothing else; the file is the interface. Deactivation is a directory move: my retired pack sits at ~/.claude/agents-archive/gsd/ (gsd-advisor-researcher.md, gsd-code-fixer.md, gsd-codebase-mapper.md, and siblings), out of the loader’s sight. Invocation takes four forms: name the agent in prose, pick it from the @-mention typeahead, type @agent-<name> by hand, or run claude --agent <name> to make it the main thread for a whole session. In that last mode its system prompt replaces the Claude Code system prompt outright while CLAUDE.md and project memory still load.

Lifecycle hooks hang off the type name: SubagentStart and SubagentStop events match on it, matchers are unanchored regular expressions (a hyphenated matcher such as db-agent needs the anchors ^db-agent$ before v2.1.195), and an exit code 2 from a SubagentStop hook prevents the subagent from stopping. Stop hooks written in agent frontmatter convert to SubagentStop at runtime.

One trap for anyone auditing an estate like mine: the agents/ directories bundled inside skills are not subagent definitions. All 14 skill-bundled agents/ directories on this machine hold one file each, openai.yaml, a Codex-shaped manifest with display_name, short_description, and default_prompt under an interface key. Same directory name, different machine.

Which tools a subagent can hold

Two fields, one resolution rule: disallowedTools applies first, then tools resolves against what remains, so a tool named in both is removed. Both fields accept server-level MCP patterns (mcp__<server>, mcp__<server>__*), and disallowedTools also takes mcp__* to strip MCP wholesale. If nothing in tools resolves, the Agent tool errors with Agent would be spawned with zero tools and names the unresolved entries; before v2.1.208 the subagent launched with no tools instead, and you found out from its report.

An allowlist cannot grant what the harness strips. The harness removes these from every subagent even when listed: Agent at the nesting depth limit, AskUserQuestion, EndConversation, EnterPlanMode, ExitPlanMode (unless permissionMode: plan), ScheduleWakeup, TaskOutput, WaitForMcpServers, Workflow. Conversation forks skip this filter. AskUserQuestion is the removal that changes how you write task messages: a subagent cannot ask the user anything, so the brief has to carry every decision the work might raise.

Background subagents narrow further. They keep every MCP tool but hold a fixed set of built-ins: Read, Grep, Glob, Bash, PowerShell, Edit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite, Skill, ToolSearch, EnterWorktree, ExitWorktree, Monitor, TaskStop, SendMessage, Artifact. Since v2.1.198 subagents run in the background by default, so this narrower set is the one to design against, not the exception. Their permission prompts surface in the main session with the asking subagent named (v2.1.186 and later).

A last quirk of tools: the form Agent(worker, researcher) restricts which agent types can be spawned, but Claude Code honors the parenthesized list when the definition runs as the main thread via claude --agent and ignores it inside a subagent definition. What matters inside a definition is whether Agent appears at all, because omitting it blocks spawning. The tool was named Task until v2.1.63, and Task(...) still parses as an alias.

Which model a subagent runs, and under what permission mode

model accepts the aliases sonnet, opus, haiku, fable, a full model ID such as claude-opus-5, or inherit; omitting it means inherit. Resolution runs: the CLAUDE_CODE_SUBAGENT_MODEL environment variable, then the per-invocation model parameter, then frontmatter, then the main conversation’s model. The env var sitting at the front of that chain is worth remembering: it overrides every definition on the machine at once, which is a fleet-wide cost lever when you set it and a debugging mystery when someone else did. As of v2.1.196, setting it to inherit equals leaving it unset.

permissionMode takes default, acceptEdits, auto, dontAsk, bypassPermissions, plan, and manual (an alias for default, v2.1.200 and later). Parents hold rank: a parent running bypassPermissions or acceptEdits takes precedence and cannot be overridden, and under a parent in auto mode the harness ignores the subagent’s own permissionMode.

Plugin-provided agents lose three fields on security grounds: Claude Code ignores hooks, mcpServers, and permissionMode on that path. A plugin can ship a persona and a tool list; it does not get to ship its own permission posture.

isolation: worktree, and where agent memory persists

isolation: worktree is the field’s one valid value. The subagent runs in a temporary git worktree branched from the default branch, not from the parent session’s HEAD, and Claude Code removes the worktree when the run changes nothing. Since v2.1.216 the escape hatches are shut: a Bash command that redirects git at the main checkout fails, whether through git -C, --git-dir, GIT_DIR, GIT_WORK_TREE, or a cd earlier in the compound, and a command too complex to classify fails too. The check covers Bash alone; PowerShell gets the working-directory check and nothing deeper.

Branching from the default branch cuts both ways. An isolated subagent cannot see my uncommitted work or my feature branch, which is the right posture for a writer I don’t trust and the wrong tool for “continue the edit I have open”. None of my 61 definitions set the field; my worktree isolation happens at the session level, by hand, before an agent ever spawns.

memory is the deliberate hole in the isolation story. user maps to ~/.claude/agent-memory/<name-of-agent>/, project to .claude/agent-memory/<name-of-agent>/, local to .claude/agent-memory-local/<name-of-agent>/. With memory on, the agent’s system prompt receives the first 200 lines or 25KB of its MEMORY.md, whichever cap lands first, and Read, Write, and Edit switch on so it can maintain the file. Disable auto memory globally (the autoMemoryEnabled setting or CLAUDE_CODE_DISABLE_AUTO_MEMORY) and the field is a no-op. The other channels end with the run; this one persists across runs by design, which makes it the one place where a badly written agent can compound its own bad notes.

What a subagent knows at startup

Isolation is useful when you know what crosses the boundary at spawn. A named subagent starts with: its own system prompt plus appended environment details, the delegation task message, the full CLAUDE.md hierarchy (~/.claude/CLAUDE.md, project rules, CLAUDE.local.md, managed policy files), a git-status snapshot taken at the parent session’s start, any skills preloaded through the skills field, and a roster of sibling agents valid as SendMessage targets (v2.1.206 and later). It does not receive my conversation history, my output style, main-conversation auto memory, or the parent’s context-window size.

Two of those items bite. The git snapshot dates from session start, not spawn time; in a session that has run for hours, the subagent’s picture of the working tree is that stale unless the task message corrects it. And the CLAUDE.md hierarchy rides in whole, so a subagent is never free of my standing instructions and pays their token cost on every spawn. The built-in Explore and Plan agents skip CLAUDE.md and the git snapshot, which is part of what makes them cheap to throw at a question.

The skills field preloads full skill bodies rather than descriptions, and the agent can still invoke unlisted skills through the Skill tool. Skills marked disable-model-invocation: true refuse preloading (from v2.1.215 that includes the bundled /verify and /code-review), and a missing entry is skipped with a debug-log warning rather than an error, so a typo in the list fails without a sound.

The built-in roster: Explore (read-only, Write and Edit denied, thoroughness levels quick / medium / thorough), Plan (read-only research for plan mode), general-purpose (every tool a subagent can hold), plus the helpers statusline-setup on Sonnet and claude-code-guide on Haiku. Explore and Plan are one-shot and return no agent ID, so they cannot be resumed; from v2.1.198 Explore inherits the main conversation’s model, capped at Opus on the Claude API. Named agents can be reopened: SendMessage with the agent ID or name resumes a completed subagent in the background, though one the user stopped by hand refuses auto-resume (v2.1.191 and later).

Forks are the design’s other half. A fork inherits the full conversation history, system prompt, tools, and model, and shares the parent’s prompt cache; a named subagent starts fresh on a separate cache. The command is /subtask from v2.1.212 and was /fork from v2.1.161 through v2.1.211. This machine reports 2.1.211, so here it is still /fork. A fork cannot spawn further forks.

The paper trail outlives the run. Transcripts land at ~/.claude/projects/{project}/{sessionId}/subagents/agent-{agentId}.jsonl, survive main-conversation compaction, and age out per cleanupPeriodDays (default 30 days). And from v2.1.210 the harness scans reports at the boundary: into text imitating Claude Code output (<system-reminder> tags, lines opening Human: or Assistant:) it inserts a backslash, and a report that imitates such a tag or mentions bypassPermissions or --dangerously-skip-permissions arrives under a marker line beginning [harness: subagent output matched instruction-shaped pattern(s):. Nothing is removed or reworded. The report boundary is a trust boundary: a subagent that read a hostile file can come home carrying instruction-shaped text, and the scan makes that visible instead of quiet.

Nesting depth, per-session count, and concurrency limits

Limit Default Knob On overflow
Nesting depth below the main conversation 3 CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH (v2.1.217+; 1 disables nesting) n/a
Subagents per session 200 CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION (v2.1.212+) Subagent spawn limit reached
Concurrent subagents 20 CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS (v2.1.217+) Concurrent subagent limit reached

The depth default has churned. From v2.1.172 through v2.1.216 nesting was fixed at five layers with no way to change it; v2.1.217 and v2.1.218 dropped the default to 1; v2.1.219 settled on 3. My CLI reports 2.1.211, which puts this machine in the five-layer band with no knob at all, and below the gates for /subtask, the per-session variable, and both v2.1.217 variables. A limits table read without running claude --version first describes someone else’s machine.

The smaller print: /clear resets the per-session count; /subtask forks spend the same 200-budget while a /fork background session does not; resuming a finished subagent takes a fresh concurrency slot without checking the cap; sessions with ultracode active are exempt from the concurrent limit.

When a subagent saves context or time

The spawn decision is a context-budget decision before it is anything else.

A subagent wins when token burn is high and finding density is low. Codebase reconnaissance is the canonical case: an Explore pass reads file after file to answer one question, and the answer is a paragraph. Run that in the main loop and those files sit in my window for the rest of the session, degrading each later turn until compaction eats them on its schedule, not mine. Run it in a subagent and the paragraph is all that crosses.

It wins when the restriction is the point. A reviewer built on Explore cannot edit what it reviews, because Write and Edit are denied at the definition, not by request. A code-writing agent under isolation: worktree cannot touch my checkout even when its Bash gets creative, because v2.1.216 closed the git -C back door. And it wins when the model or effort should drop: mechanical passes do not need the main thread’s model, and frontmatter model plus the resolution chain make the downshift per-agent instead of per-session.

Parallel fan-out is real, 20 concurrent lanes by default, and I rank it last on purpose. Parallelism multiplies whatever isolation story you already have. Twenty workers with scoped tools and tight briefs are twenty windows of burn I never pay for; twenty workers with vague briefs are twenty confident reports to reconcile.

When the main loop is the cheaper place to work

A subagent loses when the task needs my conversation history. A named agent gets none of it, so the task message is the whole brief, and a brief that takes as many tokens to write as the job would take to do is an upside-down transaction. The fork covers this middle ground: full history, shared prompt cache, no re-briefing, at the cost of dragging my whole window along.

It loses when the work needs a decision I have not made yet. With AskUserQuestion stripped, a subagent facing an ambiguous call does not ask; it picks, and the pick comes back inside a confident report. The failure mode is not an error message, it is a plausible summary of the wrong work. Steering exists through SendMessage to a resumable agent, but each correction is a full round trip to a worker that cannot raise its hand.

And it loses on small tasks. Every spawn pays the startup freight: system prompt, environment details, the CLAUDE.md hierarchy in full, preloaded skills. For a two-file edit the main loop is cheaper and I can watch it happen; the transcript at agent-{agentId}.jsonl is for post-mortems.

A subagent is a context transaction. I spend a task message and receive a report, and the distance between them runs on a separate ledger. The frontmatter sets the terms: which tools, which model, which checkout, what persists afterward. Written well, the arrangement keeps the main window for what it is for: the decisions, not the searching.