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: 12Two 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.