You asked Claude Code to find every place your code calls the payments API. It read forty files, pasted a lot of grep output into the conversation, and now the context window is crowded with material you will never look at again. A subagent is the fix: a Claude Code subagent is a separate Claude instance with its own context window, system prompt and tool list that does one job and hands back only a summary.
This guide covers when to write one, a file you can copy, the fields that matter, and what I measured when I tested the restrictions. I read Anthropic's subagents page on 2026-10-09 (fetched with curl and converted to text) and ran the tests below on Claude Code 2.1.295 the same day. Claude Code changes fast, so check the docs for your version.
If you are still deciding between skills, hooks, subagents and MCP, start with which Claude Code feature do you need. This post assumes you already know you want a subagent.
What is a Claude Code subagent, exactly?
It is a Markdown file with YAML frontmatter. The docs list what makes it different from your main session:
- Its own context window. It does not see your conversation history. Claude writes it a task message, and only the final report comes back.
- Its own system prompt. The file's body becomes the prompt. A subagent gets that plus basic environment details, not the full Claude Code system prompt.
- Its own tools and permissions. You can restrict them.
- It still loads your CLAUDE.md files and a git status snapshot, except the built-in Explore and Plan agents, which skip both to stay fast.
- It spends tokens of its own. Its requests count toward the same usage limits as your main conversation.
Claude decides when to delegate by reading each subagent's description, so the description is the trigger, not decoration.
Do you need a custom subagent at all?
Often not. Claude Code ships with built-ins: Explore (read-only search and analysis), Plan (read-only research used in plan mode) and general-purpose (every tool, for multi-step work). Try those first.
Write your own when one of these is true:
| Situation | Why a custom subagent helps |
|---|---|
| You keep giving the same worker the same instructions | The instructions live in a file, not in your head |
| A task produces long output you will not reuse (test logs, search results) | Only the summary reaches your main context |
| You want a reviewer that cannot edit | An allowlist of tools enforces it |
| A cheap task does not need your main model | The model field routes it to a smaller one |
| One task needs an MCP server you do not want loaded all session | Define the server inline in that subagent only |
Stay in the main conversation for quick edits, back-and-forth refinement, or work where planning, coding and testing share a lot of context. The docs say the same: a subagent that is not a fork starts fresh and may need time to gather context. For the planning side of big changes, see the plan mode refactor workflow.
Your first subagent: a copyable PR reviewer
Save this as .claude/agents/pr-reviewer.md in a repository, or in ~/.claude/agents/ to have it everywhere:
---
name: pr-reviewer
description: Reviews the current branch diff for bugs, missing error handling and risky edits. Use proactively before opening a pull request.
tools: Read, Grep, Glob, Bash
model: sonnet
maxTurns: 12
---
You review code changes. Run `git diff main...HEAD`, read surrounding code for any file that looks risky, and report only problems that would matter in production.
Format: a short list, most serious first. For each item give the file, the line, what breaks, and a one-line fix. If you find nothing serious, say so in one sentence. Do not edit files.
Then use it in one of three ways, all from the docs:
- Natural language: "Use the pr-reviewer subagent on my changes." Claude decides whether to delegate.
- @-mention: type
@and pick it from the list. This guarantees that subagent runs for the task. - Whole session:
claude --agent pr-reviewer, which makes the main thread take on that agent's tools and model. Set"agent": "pr-reviewer"in.claude/settings.jsonto make it the default.
The docs say Claude Code watches both agents folders and picks up edits within a few seconds. The exception: if the agents directory did not exist when the session started, restart once.
I did not run this exact reviewer against a real branch. I did run a smaller agent from the same folder layout to prove loading works, covered below.
Which frontmatter fields matter?
Only name and description are required. These are the ones I would actually reach for, condensed from the docs' reference table:
| Field | What it does | Trap |
|---|---|---|
tools | Allowlist of tools | If omitted, the subagent inherits every tool available to subagents |
disallowedTools | Denylist, removed from the inherited or listed set | An entry like Bash(git push *) still removes the whole Bash tool |
model | sonnet, opus, haiku, fable, a full model ID, or inherit | Resolution order: per-call parameter, this field, CLAUDE_CODE_SUBAGENT_MODEL, then the main model |
maxTurns | Stops runaway loops; output returns marked partial | The partial marking needs a recent version (2.1.246+ per the docs) |
permissionMode | Permission mode for the subagent | Ignored if the main session is in bypassPermissions, acceptEdits or auto |
skills | Preloads full skill content at startup | Does not stop it invoking other skills; omit Skill from tools for that |
memory | Persistent notes in user, project or local scope | Does nothing if auto memory is off |
isolation: worktree | Runs in a temporary git worktree | Worktree is cleaned up if nothing changed |
hooks | Hooks that run only while this subagent is active | Project-level ones need the folder trusted first |
Multi-word field names are camelCase and must match exactly. That detail bites, as the next section shows.
How do you check a subagent file before trusting it?
Anthropic documents claude plugin validate .claude/agents for finding files whose frontmatter does not parse. I ran it, and it is not enough on its own.
I made three deliberately broken files and one good one. On version 2.1.295 the validator printed Validation passed with warnings and flagged exactly one thing: the file with no description. It said nothing, even with --strict, about a file with a misspelled disallowedtools and maxturns, or about a description containing a colon and space that strict YAML rejects.
Then I loaded similar files into a real session and read the agent list Claude Code reports at startup:
| File | Claude Code 2.1.295 | What it means |
|---|---|---|
Misspelled disallowedtools | Loaded | The restriction silently does not exist |
| Description with a colon and space | Loaded | Claude Code is more forgiving than a strict YAML parser |
No description | Not loaded | Matches the docs: skipped |
| Correct file | Loaded |
So here is a short checker that does what the validator does not: it flags unknown and wrongly cased fields, missing descriptions, and YAML that strict parsers reject. It needs PyYAML.
#!/usr/bin/env python3
"""Check Claude Code subagent files against the documented frontmatter fields."""
import sys
from pathlib import Path
import yaml
# Field names from the "Frontmatter reference" table on code.claude.com/docs/en/sub-agents
KNOWN = {
"name", "description", "tools", "disallowedTools", "model", "permissionMode",
"maxTurns", "skills", "mcpServers", "hooks", "memory", "background",
"omitClaudeMd", "effort", "isolation", "color", "initialPrompt", "experimental",
}
def check(path: Path) -> list[str]:
text = path.read_text(encoding="utf-8")
if not text.startswith("---\n"):
return ["opening --- is not the first line: treated as documentation, not an agent"]
try:
_, front, _body = text.split("---\n", 2)
except ValueError:
return ["no closing ---"]
try:
meta = yaml.safe_load(front)
except yaml.YAMLError as exc:
line = exc.problem_mark.line + 2 # +1 for 1-based, +1 for the opening ---
return [f"not valid YAML ({exc.problem}, file line {line}): quote the value"]
problems = []
if not isinstance(meta, dict):
return ["frontmatter is not a mapping"]
if not meta.get("name"):
problems.append("missing name: file is treated as documentation")
if not meta.get("description"):
problems.append("missing description: file is skipped")
for key in meta:
if key not in KNOWN:
near = [k for k in KNOWN if k.lower() == str(key).lower()]
hint = f" (did you mean {near[0]}?)" if near else ""
problems.append(f"unknown field {key!r} is silently ignored{hint}")
return problems
def main(folder: str) -> int:
bad = 0
for path in sorted(Path(folder).glob("**/*.md")):
problems = check(path)
print(f"{path.name}: {'ok' if not problems else ''}")
for p in problems:
print(f" - {p}")
bad += bool(problems)
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1]))
Run it with python3 check_agents.py .claude/agents. Real output on my test folder:
colon-agent.md:
- not valid YAML (mapping values are not allowed here, file line 3): quote the value
nodesc-agent.md:
- missing description: file is skipped
pong.md: ok
typo-agent.md:
- unknown field 'disallowedtools' is silently ignored (did you mean disallowedTools?)
- unknown field 'maxturns' is silently ignored (did you mean maxTurns?)
The KNOWN list is copied from the docs on 2026-10-09. New versions add fields, so refresh it when a legitimate field gets flagged.
Which restrictions actually hold? A test
This is the part worth reading twice. I made three subagents that differ only in how they try to be read-only, then asked Claude Code to have each one create a file. One run, Claude Code 2.1.295, headless with --permission-mode acceptEdits:
| Subagent frontmatter | What happened |
|---|---|
disallowedtools: Write, Edit (typo) | Used the Write tool and created the file |
disallowedTools: Write, Edit (correct) | Could not use Write, so it ran printf 'hello\n' > proof-deny.txt through Bash and created the file anyway |
tools: Read, Grep, Glob (allowlist) | Could not create the file; it said so and suggested a shell command for me to run |
Two lessons. A typo is not an error, it is a missing control. And a denylist is only as strong as the tools you forgot to deny: remove Write and Edit and a subagent with Bash still writes files. For a reviewer that must not change anything, use an allowlist and leave Bash out, or keep Bash and add a hook.
This was a single run on one model, and model behaviour varies. Treat it as an existence proof, not a rate. The total cost of that session, reported by Claude Code, was about $0.35.
How do you restrict Bash without removing it?
Use a PreToolUse hook in the subagent's frontmatter. This follows the pattern in Anthropic's docs: the hook gets the tool input as JSON on stdin, and exit code 2 blocks the call and sends your stderr message back to the agent.
---
name: db-reader
description: Runs read-only database queries. Use when analysing data or producing reports.
tools: Bash
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/validate-readonly-query.sh"
---
You are a database analyst with read-only access. Run SELECT queries to answer questions. If asked to change data, explain that you cannot.
#!/bin/bash
# Blocks SQL write keywords, allows everything else.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [ -z "$COMMAND" ]; then
exit 0
fi
if echo "$COMMAND" | grep -iE '\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|REPLACE|MERGE)\b' > /dev/null; then
echo "Blocked: Write operations not allowed. Use SELECT queries only." >&2
exit 2
fi
exit 0
Make it executable with chmod +x. I ran the script (not inside Claude Code) against four sample hook inputs with jq installed:
| Command in the hook input | Exit code | Result |
|---|---|---|
psql -c "SELECT count(*) FROM orders" | 0 | Allowed |
psql -c "update orders set status = 'void'" | 2 | Blocked |
psql -c "SELECT 1; DROP TABLE orders" | 2 | Blocked |
psql -c "SELECT lo_unlink(oid) FROM pg_largeobject_metadata" | 0 | Allowed, and that function deletes data |
The last row is why a keyword blocklist is a backstop and not a security boundary. The real control is a database role that only has SELECT rights. I did not run this hook inside a live subagent, so after you set it up, ask the subagent to run an UPDATE and confirm it is refused. The docs also say frontmatter hooks in a project-level subagent run only after you trust that folder; until then the subagent runs without them. More on hooks in the hooks guide, and on layering permissions in AI coding agent safety.
Does a subagent really load? A tiny proof
Before building anything elaborate, prove the plumbing. I created .claude/agents/pong.md with tools: Read, model: haiku, maxTurns: 2, and a body saying to reply with exactly PONG-4417. Then I ran Claude Code headless from that folder and asked it to use the pong subagent.
The transcript showed a single Agent tool call with subagent_type: "pong", and the final answer was The pong subagent returned exactly: PONG-4417. The session's startup event listed pong among the available agents next to Explore, Plan and general-purpose. Cost reported for that run: about $0.11. The cheapest test you can do on your own setup is the same: a one-line agent with a fixed reply.
Which model should a subagent use?
The docs say you can control costs by routing tasks to faster, lower-cost models like Haiku. The practical rule: read-only searching and log summarising rarely need your strongest model; debugging a subtle race condition might. Put model: haiku or model: sonnet in the file. model: inherit uses the main conversation's model.
To force one model on every subagent, the docs describe setting CLAUDE_CODE_SUBAGENT_MODEL plus CLAUDE_CODE_SUBAGENT_MODEL_FORCE to 1 in the env block of settings (needs a recent version). Run /tasks while a subagent is working to see which model it is actually on. I have not benchmarked quality across models, so no claims here about which is better for review.
Patterns that work
These come from the docs' "common patterns" and are worth keeping, with an untested prompt template for each:
- Isolate noisy output.
Use a subagent to run the test suite and report only the failing tests with their error messages. - Parallel research.
Research the auth, database and API modules in parallel using separate subagents, then summarise how they depend on each other.The docs warn that many subagents returning detailed results can still eat your main context, and each spends tokens while it runs. - Chain.
Use the pr-reviewer subagent to find problems, then fix only the serious ones.
For heavier parallel work across sessions, see the multi-agent lesson. The three prompt templates above are untested wording; adapt them to your repo.
Why isn't my subagent being used?
| Symptom | Likely cause | Fix |
|---|---|---|
| Claude never delegates | Description too vague | Say when to use it, add "use proactively" if you want automatic delegation |
| New subagent missing from the list right after you created the folder | Folder did not exist at session start | Restart Claude Code once |
| File silently missing | No name or no description, or frontmatter does not parse | Run the checker above |
| Hooks in the file never fire | Project folder not trusted yet | Accept the trust dialog for that folder |
| Startup warning about descriptions | Combined descriptions over 15,000 tokens (per docs) | Shorten descriptions, move detail into the body |
Plugin subagent ignores hooks, mcpServers or permissionMode | Not supported for plugin subagents, for security | Copy the file into .claude/agents/ |
What I could not verify
- I did not run the PR reviewer or the
db-readerhook inside a live subagent. The hook script was tested standalone, and the loading and restriction tests used simpler agents. - The restriction test is one run on one model.
- Version numbers in the docs (for example 2.1.246 for partial output markers) are taken from the docs' text, not tested by me.
- I tested on Linux only. The docs say Windows hooks need PowerShell.



