Two days ago I walked through how to write a proper skill. Same shelf, next tool over: the custom subagent. You picked it off the decision tree because the job needs its own context, and now you have an empty file open in .claude/agents/.
Here is what almost everyone writes first:
You are a senior security engineer with fifteen years of experience in application security. You are meticulous, thorough, and detail-oriented.
I understand the instinct. It feels like hiring. You are building a specialist, so you write a CV.
That paragraph is the least important line in the file. Fifteen imaginary years of experience do not change the model's weights, and "thorough" is not an instruction anyone can follow or verify. One sentence of role-setting earns its place as an anchor for tone. Everything past that is set dressing.
The value of a subagent lives in three other decisions, and the file format makes all three easy to skip.
What a subagent actually is
The container is as boring as a skill's. A subagent is a markdown file with YAML frontmatter: a name, a description, optionally a tools list and a model, and then a body. Put it in .claude/agents/ to share it with the project, or in ~/.claude/agents/ to carry it across all your projects. Both directories are scanned recursively, so you can organise subfolders, and the /agents command gives you a management screen for the lot.
The body is the subagent's system prompt. And this is the mental model that everything else hangs off: the subagent never sees your conversation.
It does not know about the bug you spent twenty minutes discussing. It has not seen the file you just edited or the plan you agreed on. It wakes up with exactly two things: the body of your file, and the one task prompt the main agent writes when it delegates. It does its work in its own context window, and when it finishes, that entire window is thrown away. Only its final message crosses back.
That is the whole product. A subagent is a context firewall: the four thousand lines of stack traces, the dead-end file reads, the noise, all of it stays in the other room, and only the conclusion walks through the door. So every line you write in that file should serve one of three jobs: getting the right tasks routed in, constraining what happens inside, and shaping what comes back out.
The description decides when it runs
The main agent reads the description field when deciding whether to delegate, which makes it a routing rule. Most people write it like a job title instead.
Bad, and typical:
description: Expert code reviewerThat tells the main agent what the subagent is. It needs to know when to use it:
description: Use after writing or changing code, before committing. Reviews
the working diff for bugs and security issues. Use proactively when a task
involved more than trivial edits. NOT for reviewing whole files or
auditing the repo, and it never applies fixes: read-only.Three things are doing work here. The concrete trigger moments, so delegation actually happens. The phrase "use proactively", which the docs use for a reason: it tells the main agent not to wait until you ask. And the loud NOT at the end, so the subagent does not fire on jobs it will do badly. If your subagent never seems to get used, the description is nearly always why.
The tools line is the actual enforcement
Here is the line people skim past, and it is the most important one in the file:
tools: Read, Grep, GlobThat is an allowlist. A tool that is off the list does not exist for this agent. Your reviewer physically cannot edit a file, which means you never have to write "please do not modify the code", which is a polite request the model will honour most runs and forget on the one that matters. This is the same line the hooks guide draws: an instruction asks, a mechanism enforces. The tools list is the mechanism.
One honest caveat: Bash is the leak in any allowlist. A subagent with shell access can do nearly anything, tools list or no tools list. So treat Bash as the one entry you have to justify. A test analyst needs it to run the suite. A code reviewer has no business with it.
The frontmatter carries a few more constraints worth knowing. model lets you run grunt work on a cheaper model than your main session. maxTurns puts a hard budget on a runaway agent. And mcpServers scopes an MCP server to this one subagent: a browser-tester can carry its own Playwright server, connected when the agent starts and disconnected when it finishes, so those twenty browser tools never bloat your main session at all.
The return contract shapes what comes back
Remember: only the final message survives. If the body does not say what that message must look like, you get whatever the model felt like summarising, which is usually an essay when you needed three lines.
So end the body with a contract, the same move that closes a good skill:
Report back in exactly this shape:
- Verdict: one line, "N failures, M root causes".
- Root causes: one bullet per cause, with file:line evidence.
- Fix direction: one line per cause. No diffs, no code.
If everything passes, say so in one line. A short answer is a good answer.Without that last line, the model pads. An agent that is allowed to come back empty-handed is an agent whose non-empty answers you can trust.
The whole file
The test-analyst from the decision-tree post, written properly:
---
name: test-analyst
description: Use when the test suite fails and the output is too long to
read in the main conversation. Triggers: a failing CI run, "why are the
tests failing", any test output over ~50 lines. NOT for writing tests or
fixing code. It diagnoses only.
tools: Read, Grep, Glob, Bash
model: sonnet
maxTurns: 15
---
You are a test-failure analyst. You have no memory of the conversation
that spawned you. Everything you need is in the task you were given; if
the test command is not named there, find it in package.json or the CI
config before guessing.
Run the suite once. Group the failures by root cause, not by test file.
Trace each group to the change or config that broke it, and read the
actual source before blaming it.
You never edit anything. You have Bash to run tests, nothing else.
Report back in exactly this shape:
- Verdict: one line, "N failures, M root causes".
- Root causes: one bullet per cause, with file:line evidence.
- Fix direction: one line per cause. No diffs, no code.
If everything passes, say so in one line.Twenty-odd lines. One sentence of role. The rest is routing, constraint, and contract. Notice the second paragraph of the body exists purely because the agent is an amnesiac: it spells out where to find what the conversation would otherwise have provided.
What to actually do
- Write the description as a delegation trigger. Concrete moments it should fire, "use proactively" if you mean it, and the jobs it must refuse.
- Cut the persona to one sentence. If deleting a line would not change the agent's behaviour, delete it.
- Make the tools list as short as the job allows. Treat
Bashas the exception you argue for, and usemodelandmaxTurnsto cap the cost of a bad run. - Write the body for an amnesiac. Name where to find anything your conversation would normally supply: the test command, the conventions, the entry points.
- End with a return contract. Fixed shape, evidence required, and permission to come back with a short answer.
- Commit the file. A subagent in
.claude/agents/is shared judgement. One in your head is a habit your teammates cannot use.
A persona paragraph asks the model to behave like a specialist. The other three lines in the file make behaving like anything else impossible. Once you see that, the CV-writing instinct goes away, and the files get shorter, stricter, and a lot more useful.