Anatomy of a sub-agent
Other pages here describe compiling a sub-agent. This one shows the result. A compiled sub-agent is one markdown file — YAML frontmatter that Claude Code reads, then a body assembled from a shared Liquid template and the agent’s own markdown partials. None of it is hand-written, and npx agents-inc compile replaces the whole file each time it runs.
Quick start
Section titled “Quick start”This is the top of a compiled web-developer.md, verbatim:
---name: web-developerdescription: "Implements frontend features from detailed specs - UI components, TypeScript, styling, client state - surgical execution following existing patterns - invoke AFTER the pm creates the spec"tools: Read, Write, Edit, Grep, Glob, Bash, Skillmodel: opuspermissionMode: defaulthooks: {"Stop":[{"hooks":[{"type":"command","command":"command -v npm >/dev/null 2>&1 && [ -f package.json ] || exit 0; out=$(npm run --if-present --silent typecheck 2>&1) || { printf '%s\\n' \"$out\" >&2; exit 2; }"}]}]}skills: - web-framework-react:web-framework-react - web-styling-cva:web-styling-cva - web-styling-design-tokens:web-styling-design-tokens - web-styling-tailwind:web-styling-tailwind - web-ui-base-ui:web-ui-base-ui---<!-- Generated by agents-inc — do not edit; compile rewrites this file -->
# Web Developer Agent
<role>You are an expert frontend developer implementing UI features based on detailed specifications while strictly following existing codebase conventions.
**When implementing features, be thorough on what the spec needs and silent on the rest. Cover the edge cases, error states, and accessibility affordances the feature actually has. An implementation's size follows the spec's size, not the template's.**
Your job is **surgical implementation**: read the spec, examine the patterns, implement exactly what's requested, test it, verify success criteria. Nothing more, nothing less.
<domain_scope>
## Domain Scope
**You handle:**
[… trimmed …]The frontmatter is the entire contract with Claude Code: what the sub-agent is called, what it’s for, which tools it may use, which model runs it, how it’s isolated, what has to happen before it may stop, and which skills load into its context the moment it spawns. Everything below the marker is prompt — an XML-tagged body, segmented so the model can be pointed at one section at a time. The file lands in ~/.claude/agents/ at global scope and .claude/agents/ at project scope.
The frontmatter
Section titled “The frontmatter”| Key | Where the value comes from | What Claude Code does with it |
|---|---|---|
name |
the agent’s key at compile time — it matches the filename stem | the address the Task tool dispatches to |
description |
description: in the agent’s metadata.yaml, copied verbatim |
shown to the dispatching model; it’s how an agent gets picked |
tools |
the tools: list in metadata.yaml, plus Skill, which the compile step always appends |
the allowlist the sub-agent spawns with |
disallowedTools |
disallowedTools: in metadata.yaml — omitted when it isn’t set |
tools the sub-agent may not use |
model |
model: for the agent in config.ts, falling back to metadata.yaml, then to inherit |
which model runs the sub-agent |
effort |
effort: in config.ts, falling back to metadata.yaml — omitted when neither sets it |
reasoning effort for the run |
permissionMode |
permissionMode: in metadata.yaml, falling back to default |
the sub-agent’s permission posture |
isolation |
isolation: in metadata.yaml — omitted when it isn’t set |
runs the sub-agent in a git worktree of its own |
experimental |
experimental: in metadata.yaml — omitted when it isn’t set |
an escape hatch for frontmatter options outside the stable set — cacheTtl is the only one documented |
hooks |
hooks: in metadata.yaml, merged with a completion gate the compile step adds for any agent holding Write or Edit |
processes Claude Code runs around the sub-agent’s lifecycle |
skills |
the preloaded subset of the agent’s skills — omitted when there are none | each named skill is loaded into the sub-agent’s context at spawn |
Five of the eleven are on every compiled file: name, description, tools, model and permissionMode. The last two always render because the template supplies a fallback — inherit and default — so their presence says nothing about whether anyone chose them. The other six appear only when something puts them there.
model is a real per-agent choice. Accepted values are sonnet, opus, haiku, fable and inherit, and every shipped agent names opus — see Tune a sub-agent’s model for setting one differently.
permissionMode: default is usually not a choice anyone made. No shipped agent’s metadata.yaml contains the key, so the line you see in those files is the template’s fallback. Set it in a metadata.yaml and it reaches the compiled file: the accepted values are default, acceptEdits, dontAsk, bypassPermissions, plan and delegate.
effort and disallowedTools are absent from every shipped agent, because no shipped metadata.yaml declares either. Both are carried through if you declare them — effort from config.ts as well, which is where you’d normally set it.
isolation has one documented value, worktree. It runs the sub-agent in a fresh git worktree instead of the tree the session has been editing. No shipped agent sets it; the key is there for an agent you author. The role it suits is a reviewing one — reading a diff in a tree of its own keeps the reading uncoloured by whatever the session was doing to that tree — and it suits no agent that has to write into the session’s tree.
experimental is an escape hatch, not a tuning knob you’re likely to reach for. Claude Code documents exactly one option under it today, cacheTtl, accepting 5m or 1h. No shipped agent sets it.
The doubled skill reference is plugin:skill. web-framework-react:web-framework-react names the plugin on the left and the skill inside it on the right; the halves are identical because each skill ships as its own single-skill plugin. A skill you’ve ejected renders as a bare id instead, with no colon — see Install modes.
The completion gate
Section titled “The completion gate”The long hooks: line is a Stop hook — Claude Code converts it to SubagentStop when it registers a sub-agent’s hooks, which is the name the logs show — and every agent that can write files gets the same one. When the sub-agent tries to finish, Claude Code runs the command; exiting 2 blocks the stop and hands the command’s output back to the sub-agent, so a failing typecheck comes back as the errors themselves and the agent keeps working instead of reporting done.
It is a hook rather than a sentence in the prompt because those are different kinds of claim. “Check your work before stopping” is advice a model may decline to take. This runs whether it does or not.
The command reads, unwrapped:
command -v npm >/dev/null 2>&1 && [ -f package.json ] || exit 0out=$(npm run --if-present --silent typecheck 2>&1) \ || { printf '%s\n' "$out" >&2; exit 2; }It is inert wherever it can’t be right. Your compiled agents get installed into whatever repository you point them at, and a gate that hard-blocks in a project with no npm would make every sub-agent unstoppable. So the first line exits 0 unless there is both an npm and a package.json, and --if-present exits 0 for a script the project doesn’t define. What’s left is exactly the case the gate is for: a project that declares these checks, failing one of them.
Who gets it. Any agent whose tools include Write or Edit. A hook the agent declares for another event — a PostToolUse formatter, say — is added beside the gate; declaring a stop hook of your own (Stop, or the SubagentStop spelling Claude Code converts it to) replaces the gate, because a project stating its own completion check keeps it. Of the eighteen shipped agents, thirteen qualify; the four researchers and the reviewer read without writing, so their compiled files carry no hooks key at all.
The body
Section titled “The body”Sections come out in a fixed order, separated by --- rules, and the XML tags nest.
| Section | Tag | What’s in it |
|---|---|---|
| Title | # heading |
title: from metadata.yaml |
| Role | <role> |
the agent’s identity, which opens a nested <domain_scope> |
| Operating principles | <operating_principles> |
one shared block, identical in every agent — how to investigate, what to change, what to verify, what to report |
| Critical requirements | <critical_requirements> |
hard rules for this agent; the section is skipped when the file behind it is empty |
| Playbook | its own top-level tags, which vary by agent | the step-by-step process this agent follows |
| Output format | <output_format> |
the shape of the agent’s report, one nested tag per section it must carry |
| Critical reminders | <critical_reminders> |
repeated emphasis; skipped when empty |
| Compile-time block | <system-reminder> |
everything assembled from your configuration rather than from the agent’s role — see below |
The tags aren’t decoration. They’re how the prompt is segmented, and the nesting is real — <role> wraps <domain_scope>, and <output_format> wraps one nested tag per section the agent’s report has to carry, which differs from agent to agent. The playbook is the one section that brings tags of its own choosing: <mandatory_investigation> for the developers and most researchers, <test_planning> for the testers, <retrieval_strategy> for the reviewer and the pm.
The trailing block
Section titled “The trailing block”The last thing in the file is a <system-reminder>, and its position is the point:
<system-reminder>Compiled by 0.162.0.
<skill_activation_protocol>The skills below are available through the Skill tool. Open a task by naming which of them ittouches and loading those first — a skill carries the conventions this project expects for itsarea, and its content reaches you only once you invoke it.
### web-routing-react-router- Description: Client-side routing with data APIs — loaders, actions, error boundaries, search params, nested layouts, and code splitting- Invoke: `skill: "web-routing-react-router:web-routing-react-router"`- Use when building React apps with client-side routing — nested routes, loaders, actions, error boundaries, lazy loading, and URL-based state.
</skill_activation_protocol></system-reminder>A compiled sub-agent is a system prompt, so the file is read front to back on every invocation of it. Everything volatile is collected here, at the end, where changing it leaves the whole prefix above untouched — which is what keeps that prefix cacheable across a version bump or a change to your skill selection. The two things that move are the generator version and the skills, and both live inside this block for that reason.
The block ends in one of three ways, depending on how the agent’s skills load:
- Dynamic skills produce
<skill_activation_protocol>, with a four-line entry per skill. - All skills preloaded produces
<skills_note>saying so — their content is already in context, so there is nothing to invoke. - No skills at all produces a
<skills_note>pointing out that theSkilltool is still there, so a skill you add later can be loaded by name.
Where each section comes from
Section titled “Where each section comes from”Three inputs produce the file: one shared template, one shared partial, and five markdown files belonging to the agent itself.
The template is agent.liquid, and there’s only one of it. It emits the frontmatter shape, the completion gate, the trailing block and the rules between sections, and it decides where everything else lands.
The shared partial is methodologies/operating-principles.liquid, which emits <operating_principles>. It’s the one block every agent carries identically, so a working principle written once applies to all of them.
The per-agent partials are five markdown files in the agent’s own directory, alongside its metadata.yaml.
| File | Where it lands | Required? |
|---|---|---|
identity.md |
inside <role> |
yes |
playbook.md |
after the critical requirements, bringing its own top-level tags | yes |
critical-requirements.md |
inside <critical_requirements> |
no — the section is skipped when it’s empty |
critical-reminders.md |
inside <critical_reminders> |
no — skipped when empty |
output.md |
after the playbook, bringing its own top-level tags | no — falls back to the agent’s category directory |
playbook.md and output.md are the two big ones — for every shipped agent they’re the largest two of the five. Customizing sub-agents covers taking ownership of all five.
Preloaded and dynamic skills in the file
Section titled “Preloaded and dynamic skills in the file”The two load modes look nothing alike once emitted.
A preloaded skill costs one line. It appears in the skills: frontmatter list and nowhere else in the file — no skill content is copied in. The compiler emits a reference and Claude Code resolves it at spawn.
A dynamic skill costs a four-line block inside <skill_activation_protocol>, in the trailing system-reminder:
### web-routing-react-router- Description: Client-side routing with data APIs — loaders, actions, error boundaries, search params, nested layouts, and code splitting- Invoke: `skill: "web-routing-react-router:web-routing-react-router"`- Use when building React apps with client-side routing — nested routes, loaders, actions, error boundaries, lazy loading, and URL-based state.An agent with a long dynamic list therefore carries hundreds of lines it wouldn’t carry if the same skills were preloaded. Which mode a skill uses is a preloaded flag on the skill’s own entry in the stack map in config.ts, not on the agent’s entry in agents — see Config reference.
The fourth line is the skill’s own usageGuidance, copied out of its metadata.yaml and rendered verbatim. It’s the sentence the skill’s author wrote about when to reach for it, and it’s the only thing in the block that tells the sub-agent that — nothing about where you filed the skill in your config reaches this line. A skill whose metadata states none falls back to Use when working with <category>.
tools: is an allowlist, and Skill is always on it. A sub-agent that names tools: at all gets only what it names — omitting the key inherits everything instead — so enumerating is what would otherwise strip the Skill tool the trailing block’s own instructions depend on. The compile step appends it unconditionally, including for the read-only researchers, since loading a skill grants no write access. Declaring skills: does not grant the tool: that key preloads content, and the tool is what invokes a skill at runtime.
What a recompile does to your edits
Section titled “What a recompile does to your edits”A recompile replaces the file. There’s no merge, no backup and no prompt. npx agents-inc compile renders fresh output and writes it whenever the result differs from what’s on disk — and any hand edit makes it differ, so hand edits are lost.
That’s what the marker on the first line after the frontmatter is for:
<!-- Generated by agents-inc — do not edit; compile rewrites this file -->Its bytes don’t move between releases, which is deliberate: it’s the first line of a system prompt, and a marker carrying a version number would rewrite the top of every compiled agent on every release. The version lives in the trailing block instead, where changing it costs nothing. Stamping replaces rather than inserts, so recompiling rewrites that one line rather than stacking a second beside it — which is also what upgrades a file stamped by an older release that still spelled the version into it.
Position is part of the claim — only the first line after the frontmatter counts, so an agent that merely quotes the marker further down is still read as yours. npx agents-inc uninstall uses it to tell this CLI’s own output from an agent you wrote by hand, which is also why it’s a body comment and not a frontmatter key: Claude Code documents a fixed set of frontmatter keys and says nothing about how it treats an unknown one, while the body is free-form.
Want a sub-agent to say something different? Take ownership of the inputs rather than the output.
npx agents-inc eject agent-partials # every shipped agent's partials, into .claude-src/agents/npx agents-inc eject templates # the Liquid templates, into .claude-src/agents/_templates/npx agents-inc compile # rebuild from what you now owneject agent-partials copies the whole shipped agent tree, not just the agent you’re changing — and it brings _templates/ with it unless you already have those locally.
Ejected templates win. Compile resolves each template by looking in .claude-src/agents/_templates/ first, then .claude/templates/, then its own built-in copy.
Related
Section titled “Related”- Sub-agents — what a sub-agent is, before the file exists.
- Customizing sub-agents — the three layers you can eject, and what each one changes.
- Config reference — the
config.tsfields that decide what goes into the file. - Architecture — where the compiler sits in the CLI.