CCLearn

Authoring an agent file

verified against Claude Code 2.1.261 · 2026-09-0918 min

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.

text
---
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 hasWhat Claude Code does
No nameTreats the file as documentation kept beside your agents
An opening --- that is not the file's first lineReads the file as having no frontmatter, treats it as documentation
A name that contains a colonSkips the file, writes an error to the debug log
A name but no descriptionSkips the file, writes the reason to the debug log
YAML that does not parseReads 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:

LocationScopePriority
Managed settingsOrganization-wide1 (highest)
--agents CLI flagCurrent session2
.claude/agents/Current project3
~/.claude/agents/All your projects4
A plugin's agents/ directoryWhere the plugin is enabled5 (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.

FilterApplies toEffect
FirstEvery subagent except a forkRemoves 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
SecondSubagents running in the background, which is the default — forks again exceptedKeeps 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, disallowedTools is applied first, then tools is 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 tools field never triggers this refusal. If you leave the tools list empty, or disallowedTools removes 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:

OrderSource
1The model parameter Claude passes on that specific invocation
2The definition's model frontmatter, where inherit means the main conversation's model
3The CLAUDE_CODE_SUBAGENT_MODEL environment variable
4The 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 bypassPermissions or acceptEdits. 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 permissionMode is 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, mcpServers or permissionMode at 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.

ScopeLocationUse when
user~/.claude/agent-memory/ plus the agent's nameThe learnings apply across all your projects
project.claude/agent-memory/ plus the agent's nameThe knowledge is project-specific and shareable in version control
local.claude/agent-memory-local/ plus the agent's nameProject-specific, but should not be checked in

project is 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 wroteWhat it is
tools and disallowedToolsEnforcement — but computed after two filters, so a background run has a smaller built-in set
permissionMode stricter than the parentA request, overruled when the parent is in acceptEdits, bypassPermissions or auto mode
memoryConditional on auto memory being on; it enables Read, Write and Edit, and the docs stop there
isolation: worktreeEnforcement, with the command check arriving in v2.1.203
modelSecond in the resolution order, behind only the model Claude passes on the invocation
descriptionA 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

  1. A definition sets tools: Read, Grep, Glob and memory: 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 answer

    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 tools line, 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 the tools field"; 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, take memory: project out of the file rather than resolve the ambiguity by assumption.

  2. A file sets tools: Read, Bash and disallowedTools: Bash, Write. What is left, and in what order was it worked out? Now cut tools down to Bash alone: does the subagent refuse to launch, launch with nothing, or launch with Bash — and which documented sentence decides it?

    Show answer

    Read is left. disallowedTools is applied first and tools is then resolved against the remaining pool, and a tool listed in both is removed — so Bash goes, Write was never in tools to begin with, and Read survives. (The pool those lists compute against has already been through the two filters described earlier; neither of them removes Read.)

    With tools: Bash alone it launches with no tools, and says nothing about it. The refusal that names bad entries, added in v2.1.208, is for tools entries that fail to resolve, and the docs carve this case out of it: "If you leave the tools list empty, or disallowedTools removes 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.

  3. You want a reviewer that cannot modify code. A colleague suggests permissionMode: plan. Your session is running in acceptEdits. What happens, and what should you have written instead?

    Show answer

    Nothing happens — permissionMode: plan is ignored. When the parent session is in acceptEdits or bypassPermissions, the parent's mode takes precedence and cannot be overridden, whatever the frontmatter says, stricter or looser. So the reviewer runs under acceptEdits and 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, while permissionMode is a request the parent session can overrule.

  4. 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 answer

    Any two of the frontmatter faults that make Claude Code skip the file without reporting it in the session: a name containing a colon (refused from v2.1.218), a name with no description, YAML that does not parse, no name at 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 --debug and read the log to see which. For the parse case there is also claude plugin validate .claude/agents, from v2.1.233. And if the log is clean the file loaded fine and the fault is elsewhere — a description that does not single this agent out from the others sends the work to a different one just as quietly.

Sources