Skip to content

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.

This is the top of a compiled web-developer.md, verbatim:

---
name: web-developer
description: "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, Skill
model: opus
permissionMode: default
hooks: {"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.

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 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 0
out=$(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.

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 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 it
touches and loading those first — a skill carries the conventions this project expects for its
area, 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 the Skill tool is still there, so a skill you add later can be loaded by name.

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.

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.

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 own

eject 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.