Claude Code subagent schema
A subagent is a markdown file under .claude/agents/: YAML front matter that
configures a delegate, then a body that becomes its system prompt. The front
matter has one contract, and anthropic:claude-subagent:2.1 is it.
docmeta validate .claude/agents/**/*.md -s anthropic:claude-subagent:2.1Eighteen fields. Two of them, name and description, are required. That makes
this the second built-in that demands anything of an agent file. It is the first
that demands it because the runtime refuses to load without it.
Why it requires two fields when the skill schema requires none
Section titled “Why it requires two fields when the skill schema requires none”anthropic:claude-skill:2.1 requires nothing, because Claude Code marks every
SKILL.md field optional. A skill with no front matter at all still loads, and
takes its description from the first paragraph of the body.
An agent definition has no such fallback. Drop name and the file is skipped.
It is treated as documentation and not reported, so the agent you wrote simply
does not appear in the Agent tool listing. Drop description and it is refused
outright:
Agent file .claude/agents/reviewer.md is missing required 'description' in frontmatterBoth are load failures. The difference is that only one of them says so, which is exactly the kind of silence a metadata check is for.
The fields
Section titled “The fields”| Field | Type | Notes |
|---|---|---|
name |
string | Required. The agent’s identity, which is what the Agent tool, --agent and Agent(...) grants address, and what a hook sees as agent_type. Independent of the filename. |
description |
string | Required. When to delegate. “Use proactively” encourages automatic delegation. |
model |
string | Same values as /model, plus inherit, which is also the default. |
tools |
string | list | Allowlist replacing the inherited set. Exact names, Bash(npm run test:*), Agent(worker, researcher), or mcp__<server>. |
disallowedTools |
string | list | Denylist removed from whatever pool the agent ends up with. |
color |
enum | red, blue, green, yellow, purple, orange, pink, cyan. |
effort |
enum | integer | low, medium, high, xhigh, max, or an integer. |
permissionMode |
enum | acceptEdits, auto, bypassPermissions, default, dontAsk, manual, plan. |
maxTurns |
positive integer | Turn cap; the result is marked partial and the agent can be resumed. |
skills |
string | list | Skills preloaded into the agent’s context at startup. |
mcpServers |
list | Server names to reuse from the parent session, or one-key maps defining a server inline. |
hooks |
object | Hooks scoped to this agent, keyed by event. Stop registers as SubagentStop. |
memory |
enum | user, project, or local, naming which agent-memory directory persists. |
background |
boolean | Keep the agent in the background even when Claude wants the result. |
isolation |
enum | worktree or remote. |
initialPrompt |
string | First turn when the definition runs as the main session. Not read when it is spawned as a subagent. |
observer |
string | Agent type auto-spawned to watch this one work. |
observerMessage |
string | Extra instruction appended to each digest sent to the observer. |
permissionMode, hooks and mcpServers are read for a project or user agent
and ignored for a plugin agent. Claude Code warns about that rather than
failing. The schema validates them wherever they appear, because a field that is
ignored is still a field that was meant to do something.
background is not the flag a skill uses
Section titled “background is not the flag a skill uses”This is the one difference worth memorising, because both spellings are boolean
and only one file type takes six of them. A SKILL.md flag accepts yes, no,
on, off, 1 and 0 alongside true and false. The agent loader accepts a
YAML boolean or the exact strings true and false, and answers everything else
with:
Agent file .claude/agents/watcher.md has invalid background value 'yes'.Must be 'true', 'false', or omitted.So the same-looking key is typed differently in the two schemas on purpose:
background: yes # fine in a SKILL.md, ignored in an agentbackground: true # the only form both acceptname is checked for one rule, not the documented three
Section titled “name is checked for one rule, not the documented three”The loader enforces exactly one character rule. A name may not begin with -,
and a file that breaks it is dropped with an error. Nothing else about the name
is checked at load time: capitals work, underscores work, digits work.
The documentation describes a lowercase-and-hyphens convention, and it is a good
convention. It is not what the runtime refuses, and agent types with capitals in
them ship today. A schema holding name to ^[a-z-]+$ would fail agents that
run. What docmeta checks is the rule that fails the load; the convention is left
to you. That is the same call the platform
schemas make about requiring exactly what
the tool refuses to build without.
One thing to avoid anyway is a : in a name. It is the separator in a
plugin-scoped identifier such as my-plugin:reviewer, so a name containing one
is asking for an ambiguous address.
What is enumerated, and what deliberately is not
Section titled “What is enumerated, and what deliberately is not”Five fields have closed, published value sets, so a typo in one is an error:
✗ .claude/agents/db-reader.md /permissionMode must be equal to one of the allowed values (line 5) [anthropic:claude-subagent:2.1]color is worth checking even though it is cosmetic. An unknown colour is
dropped without a warning, so the only symptom is an agent that never takes
the colour you gave it. magenta is the one that catches people. It is a
perfectly ordinary terminal colour, it is not one of the eight, and it appears
in shipped plugins today. permissionMode includes manual, which Claude Code
normalises to default before checking, so both spellings are legal.
isolation lists worktree and remote; the published documentation names
only the first, and the schema follows the loader.
model is not enumerated. Its accepted values move with the product, and an
organisation’s availableModels allowlist narrows them further. A list written
here would reject agents that work. observer is not enumerated either, for the
matching reason. It names an agent type, and the set includes whatever the
project, its plugins and you define.
hooks and mcpServers are checked for shape and no further. A hooks block is
an object and a server list is a list. What goes inside either one is a
separate contract, versioned separately from the agent file. Pinning it here
would date this schema to whichever week it was written.
initialPrompt, observer and observerMessage are checked for one thing
beyond being strings: they have to contain a non-space character. That is the
color rule again rather than tidiness. The loader keeps each of them only when
the value survives a .trim(), and discards it silently otherwise. So
observer: " " is an observer that never spawns and never says why.
Two fields that take a YAML number or a quoted string
Section titled “Two fields that take a YAML number or a quoted string”maxTurns and effort both reach the validator as either type, depending on
how the value was written. Both are typed as two rather than one:
maxTurns: 15 # integermaxTurns: "15" # string — also acceptedeffort: high # stringeffort: 8 # integer — also acceptedeffort: "8" # quoted integer — also acceptedThe quoted integer matters more than it looks. A templating tool that quotes
every scalar produces it by accident. The loader reaches the numeric form
through parseInt on the string, so "8" and 8 are the same value to it.
The constraint keywords are type-scoped, so each applies to only one of the two
forms. A wrong value is reported once, against the key, rather than once per
failed branch. maxTurns: 0, maxTurns: -3 and maxTurns: null all fail; the
loader treats null as an invalid value rather than as an absent one.
effort has no such lower bound, and that asymmetry is deliberate.
maxTurns is checked for a positive integer and warns otherwise. effort is
checked with a bare integer test, so effort: 0 and effort: -3 are both
taken, and neither is reported. A minimum here would read as symmetry and
would fail an agent that runs.
One parse failure docmeta reports and Claude Code recovers from
Section titled “One parse failure docmeta reports and Claude Code recovers from”An agent description is usually long, and a long sentence tends to contain a
colon followed by a space. Unquoted, that is a nested mapping as far as YAML is
concerned, and the front matter does not parse:
description: Review a PR. Examples: a refactor, a new endpoint.✗ .claude/agents/reviewer.md (root) Invalid YAML frontmatter: Nested mappings are not allowed in compact mappings [(parse)]Claude Code recovers from this. When the first parse throws, it retries once.
That retry re-emits every key: value line whose value contains : or a YAML
indicator character as a quoted string. Then it parses that instead. So the
agent loads, and docmeta reports a file that works.
That is worth knowing rather than working around. The repair is a fallback, and it applies to a single-line value and nothing else. Anything it cannot rescue is skipped, with only a debug-log line to say so. Quoting the value fixes the report and removes the dependence on the fallback:
description: "Review a PR. Examples: a refactor, a new endpoint."Validate agents and skills in one run
Section titled “Validate agents and skills in one run”They are different files, so this is two runs rather than a stacked schema set. Nothing stops you doing both from one script:
docmeta validate .claude/agents/**/*.md -s anthropic:claude-subagent:2.1docmeta validate .claude/skills/**/SKILL.md -s anthropic:claude-skill:2.1Point them at a config rule instead and CI checks both on every push:
overrides: - files: ".claude/agents/**/*.md" schemas: - anthropic:claude-subagent:2.1 - files: ".claude/skills/**/SKILL.md" schemas: - anthropic:claude-skill:2.1The first matching override wins, so keep the two globs disjoint. agents/ and
skills/ already are.