Authoring an agent file
By the end you can
- Write a valid .claude/agents file with a name, a description and a scoped tool list
- Resolve tools against disallowedTools and predict the tool pool that results
- Choose a model, a memory scope and isolation deliberately rather than by default
- Explain which scope wins when the same agent name is defined in two places
- Say which fields are enforcement and which are only a request
- Keep a subagent description short enough to route on, and say where the detail belongs instead
You have now delegated the same job three times. The same read-only reviewer, the same four paragraphs of instructions, retyped from memory and slightly different on each pass. The docs have a one-line answer:
Define a custom subagent when you keep spawning the same kind of worker with the same instructions. — Create custom subagents
Lesson 5.2 put "a second opinion" on the escalation ladder and said that authoring the agent was Level 8's job. This is that file. Nearly every field in it looks like a gate, and several of them are not — so this lesson is less about YAML than about which lines actually hold.
The file, and the reasons Claude Code skips one
A subagent is a Markdown file with YAML frontmatter. Only name and
description are required.
---
name: code-reviewer
description: Reviews code for quality and best practices
tools: Read, Glob, Grep
model: sonnet
---
You are a code reviewer. When invoked, analyze the code and provide
specific, actionable feedback on quality, security, and best practices.The body is not documentation. It is the whole prompt:
The body becomes the system prompt that guides the subagent's behavior. Subagents receive only this system prompt plus basic environment details like the working directory, not the Claude Code system prompt. — Create custom subagents
Now the part that costs people an afternoon. Claude Code skips a malformed agent file without reporting it in the session:
| The frontmatter has | What Claude Code does |
|---|---|
No name | Treats the file as documentation kept beside your agents |
An opening --- that is not the file's first line | Reads the file as having no frontmatter, treats it as documentation |
A name that contains a colon | Skips the file, writes an error to the debug log |
A name but no description | Skips the file, writes the reason to the debug log |
| YAML that does not parse | Reads no fields at all, skips the file, logs the parse error |
So the symptom is not an error. The symptom is Claude never delegating to an
agent you are certain you wrote. Run with --debug to see the log. Lesson 7.2
made the same first-line point about skills; the rule is identical here and the
failure is quieter, because a skill you cannot invoke announces itself the first
time you type its name.
Claude Code watches both agent directories, so an edit is picked up within
seconds. Three cases still need a restart: the first file in an agents
directory that did not exist at session start, a change under an --add-dir
directory, and sessions started with --disable-slash-commands.
Scope: where the file lives decides who wins
When two definitions share a name, location decides:
| Location | Scope | Priority |
|---|---|---|
| Managed settings | Organization-wide | 1 (highest) |
--agents CLI flag | Current session | 2 |
.claude/agents/ | Current project | 3 |
~/.claude/agents/ | All your projects | 4 |
A plugin's agents/ directory | Where the plugin is enabled | 5 (lowest) |
Two wrinkles inside the project scope. Project agents are discovered by walking
up from the working directory, so every .claude/agents/ between there and the
repository root is scanned; as of v2.1.178, when several of those nested
directories define the same name, the definition closest to the working
directory wins. But two files sharing a name inside one directory have no
documented precedence at all — Claude Code loads one of them, chosen by
filesystem read order. /doctor reports the duplicate.
--agents takes JSON at launch, exists only for that session and is never
written to disk, which makes it the right tool for testing a definition before
you commit one. And you do commit one:
Check into version control: share project subagents with your team — Create custom subagents
Tools: the pool, and how disallowedTools narrows it
A subagent's tool pool is not your tool pool. It starts from the built-in and MCP tools available in the main conversation and passes through two filters that apply whether or not your frontmatter lists a tool.
| Filter | Applies to | Effect |
|---|---|---|
| First | Every subagent except a fork | Removes AskUserQuestion, EndConversation, EnterPlanMode, ScheduleWakeup, TaskOutput, WaitForMcpServers and Workflow; ExitPlanMode unless the subagent's permissionMode is plan; Agent at the depth limit — even when you listed them in tools |
| Second | Subagents running in the background, which is the default — forks again excepted | Keeps every MCP tool but only a fixed list of built-ins: Read, Grep, Glob, Bash, PowerShell, Edit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite, Skill, ToolSearch, EnterWorktree, ExitWorktree, Monitor, TaskStop, SendMessage and Artifact |
Forks are the exception because they are not built from a definition at all: a fork skips both filters and receives the main conversation's exact tool pool, which is one more reason Lesson 8.3 treats it as a different animal.
Read the second row twice. Background is where a spawned subagent runs by
default, so the same definition resolves to different tools in the foreground
and the background, and the removal reports no error unless it leaves the
tools list resolving to nothing. Which one you get turns on fork mode, which
has been the interactive default since v2.1.232: where it is on, Claude Code runs
the subagents Claude spawns in the background — apart from a few cases that stay
in the foreground — and removes Claude's ability to ask for the foreground. Where
it is off, which is the default for non-interactive -p runs and the Agent SDK
and is what CLAUDE_CODE_FORK_SUBAGENT=0 does in every kind of session, Claude
backgrounds by default and takes the foreground when it needs the result before
continuing. Both resolutions of the same file are therefore reachable, and
nothing in the file records which one you got.
Within that pool, tools is an allowlist and disallowedTools is a denylist.
Both accept MCP server patterns as well as tool names, so mcp__github removes
every tool from that server. The resolution order is the thing to memorise:
If both are set,
disallowedToolsis applied first, thentoolsis resolved against the remaining pool. A tool listed in both is removed. — Create custom subagents
Which produces a failure mode with no obvious symptom. If disallowedTools
happens to remove everything your tools list named, you do not get an error:
Omitting the
toolsfield never triggers this refusal. If you leave thetoolslist empty, ordisallowedToolsremoves every entry in it, Claude Code also skips the refusal and launches the subagent without tools. — Errors
The refusal that quote is carving out — Claude Code declining to launch a
subagent whose tools entries all failed to resolve, and naming the bad entries
— arrived in v2.1.208. Before that, a list of typos launched a subagent with no
tools, which returned an empty or confusing result rather than an error. You get
the error now, but the carve-out is the live half: disallowedTools emptying
your tools list produces the silent version to this day. If your delegate comes
back with nothing to say, check the spelling of the tool names before you rewrite
the prompt.
Two smaller rules. To stop a subagent spawning subagents of its own, omit
Agent from tools or deny it; the parenthesised Agent(type) allowlist
applies only to an agent running as the main thread under claude --agent, and
the type list is ignored inside a subagent definition. And to preload skill
content use the skills field, not Skill in tools — that entry governs
whether the subagent may invoke skills at runtime, not what it starts with.
Model, permissions, and what you cannot loosen
model takes an alias (sonnet, opus, haiku, fable), a full model ID, or
inherit. Four sources compete, resolved in this order:
| Order | Source |
|---|---|
| 1 | The model parameter Claude passes on that specific invocation |
| 2 | The definition's model frontmatter, where inherit means the main conversation's model |
| 3 | The CLAUDE_CODE_SUBAGENT_MODEL environment variable |
| 4 | The main conversation's model |
That order is younger than it looks, and it used to be inverted at the top:
before v2.1.251, CLAUDE_CODE_SUBAGENT_MODEL came first and overrode both the
per-invocation parameter and the frontmatter, model: inherit included. A
colleague's variable in a settings file no longer beats your model: line — but
on an older machine, or in an older bug report, it did.
permissionMode is the field most often mistaken for a gate. Left unset, the
subagent inherits the main conversation's mode. Setting it overrides that mode —
except:
- The parent is in
bypassPermissionsoracceptEdits. The parent's mode "takes precedence and can't be overridden". Whatever your frontmatter says, stricter or looser, is ignored. - The parent is in auto mode. The subagent inherits auto mode, your
permissionModeis ignored, and the classifier evaluates the subagent's tool calls with the same block and allow rules as the parent session. - The agent came from a plugin. For security reasons plugin subagents do not
support
hooks,mcpServersorpermissionModeat all; the fields are ignored on load. Copy the file into.claude/agents/if you need them.
The tool list is a property of the subagent and it is computed; permissionMode
is a request the parent session can overrule. If the reviewer must never write,
take Write and Edit away rather than setting a stricter mode and calling it a
gate.
Memory and isolation
memory gives a subagent a directory that survives across conversations.
| Scope | Location | Use when |
|---|---|---|
user | ~/.claude/agent-memory/ plus the agent's name | The learnings apply across all your projects |
project | .claude/agent-memory/ plus the agent's name | The knowledge is project-specific and shareable in version control |
local | .claude/agent-memory-local/ plus the agent's name | Project-specific, but should not be checked in |
projectis the recommended default scope. — Create custom subagents
It is not a standalone feature. Subagent memory is part of auto memory: turn
auto memory off with autoMemoryEnabled or CLAUDE_CODE_DISABLE_AUTO_MEMORY
and the memory field has no effect — the subagent launches without the memory
instructions and without the memory tool access. Every switch Lesson 6.4 listed,
including the per-project and per-session ones, disables every subagent memory
you wrote.
When memory is on, the subagent's system prompt gains the memory instructions
plus the first 200 lines or 25KB of its MEMORY.md, whichever comes first — and
one more thing:
isolation: worktree — one line in the frontmatter of, say, a refactorer that
rewrites many files at once — gives the subagent a temporary, isolated copy of
the repository. It is the same isolation Claude can apply ad hoc, made permanent
for this agent, and it has three consequences.
The worktree is branched by default from your default branch rather than the
parent session's HEAD, so it does not carry your unpushed work unless
worktree.baseRef is set to "head". Cleanup is conditional on the outcome:
Each subagent gets a temporary worktree that Claude Code removes automatically when the subagent finishes without changes; a worktree with changes stays on disk until the periodic sweep below can remove it without losing work. — Worktrees
And the enforcement itself arrived in pieces. A subagent with isolation: worktree runs its Bash and PowerShell commands inside the worktree, and a
command whose working directory resolves to your main checkout instead fails
with an error; before v2.1.203 such a command could simply run in the main
checkout. The check widened afterwards, too: before v2.1.210 it covered only the
directory you launched Claude Code from, so a command that resolved elsewhere in
the same repository — the repository root, when you launched from a monorepo
subdirectory — ran there rather than failing. Lesson 8.6 takes worktrees as a
workflow; the point here is only that the frontmatter field is where you make it
permanent, and that "isolated" is a claim with a version attached.
Keeping the description cheap
description is how Claude decides to delegate, which means it is loaded for
routing, which means it sits in your main conversation whether the agent runs or
not.
Those descriptions take up context, so keep them short. When the combined descriptions of your subagents, except the built-in ones, exceed 15,000 tokens, Claude Code shows a warning at startup with the total token count. — Create custom subagents
Note what the warning is and is not. Claude Code loads every agent whether or not the total is over: it is a notice, not a limit. Detail belongs in the system prompt, which loads only when that subagent runs — the same progressive disclosure Lesson 7.5 made for skills, in a different file. Short is not automatically good, either: a description has to single out one agent from the others, or Claude routes to the wrong one.
The paired failure is the one this level exists to prevent. A handful of essay-length descriptions quietly spends, in every session, the context the subagents were created to save.
What actually holds
| What you wrote | What it is |
|---|---|
tools and disallowedTools | Enforcement — but computed after two filters, so a background run has a smaller built-in set |
permissionMode stricter than the parent | A request, overruled when the parent is in acceptEdits, bypassPermissions or auto mode |
memory | Conditional on auto memory being on; it enables Read, Write and Edit, and the docs stop there |
isolation: worktree | Enforcement, with the command check arriving in v2.1.203 |
model | Second in the resolution order, behind only the model Claude passes on the invocation |
description | A routing hint that costs main-conversation context, never a constraint |
Design one agent that does one thing, give it the smallest tool list that lets it finish, and read the resulting definition as a stranger would: what could this file do if the prompt were ignored entirely? That question, not the prompt, is what you are actually writing.
Checkpoint
-
A definition sets
tools: Read, Grep, Globandmemory: project, and a colleague calls it a read-only reviewer. Which documented sentence makes that description unsafe, what does it still leave unanswered, and what would you change in the file rather than assume an answer?Show answerAnswer
The definition is not established as read-only. The sentence that spoils it is the one the docs give about
memory: with it enabled, "Read, Write, and Edit tools are automatically enabled so the subagent can manage its memory files."What it leaves unanswered is exactly what you would need to call the file safe — how that automatic enabling interacts with a restrictive
toolsline, and whether the writes are confined to the memory directory. Compare the tool-filter section, which states its override in as many words, "even when listed in thetoolsfield"; this sentence states nothing of the kind either way, so the write access is undocumented rather than absent. If read-only is the property you are relying on, takememory: projectout of the file rather than resolve the ambiguity by assumption. -
A file sets
tools: Read, BashanddisallowedTools: Bash, Write. What is left, and in what order was it worked out? Now cuttoolsdown toBashalone: does the subagent refuse to launch, launch with nothing, or launch with Bash — and which documented sentence decides it?Show answerAnswer
Readis left.disallowedToolsis applied first andtoolsis then resolved against the remaining pool, and a tool listed in both is removed — soBashgoes,Writewas never intoolsto begin with, andReadsurvives. (The pool those lists compute against has already been through the two filters described earlier; neither of them removes Read.)With
tools: Bashalone it launches with no tools, and says nothing about it. The refusal that names bad entries, added in v2.1.208, is fortoolsentries that fail to resolve, and the docs carve this case out of it: "If you leave thetoolslist empty, ordisallowedToolsremoves every entry in it, Claude Code also skips the refusal and launches the subagent without tools." That is the half still live today, and the symptom is a delegate that comes back with nothing to say. -
You want a reviewer that cannot modify code. A colleague suggests
permissionMode: plan. Your session is running inacceptEdits. What happens, and what should you have written instead?Show answerAnswer
Nothing happens —
permissionMode: planis ignored. When the parent session is inacceptEditsorbypassPermissions, the parent's mode takes precedence and cannot be overridden, whatever the frontmatter says, stricter or looser. So the reviewer runs underacceptEditsand can edit the code it was sent to review. Auto mode behaves the same way: the subagent inherits it and your field is ignored.You should have written a tool list. Take Write and Edit away —
tools: Read, Grep, Glob— because the tool list is a property of the subagent and it is computed, whilepermissionModeis a request the parent session can overrule. -
Your project agent is never used and there is no error anywhere in the session. Name two things in its frontmatter that would produce exactly that silence, and the flag that would show you which one it is.
Show answerAnswer
Any two of the frontmatter faults that make Claude Code skip the file without reporting it in the session: a
namecontaining a colon (refused from v2.1.218), anamewith nodescription, YAML that does not parse, nonameat all, or an opening---that is not the file's first line — the last two leave the file treated as documentation kept beside your agents. Each produces the identical symptom, which is not an error but Claude never delegating to an agent you are certain you wrote.Run with
--debugand read the log to see which. For the parse case there is alsoclaude plugin validate .claude/agents, from v2.1.233. And if the log is clean the file loaded fine and the fault is elsewhere — adescriptionthat does not single this agent out from the others sends the work to a different one just as quietly.