{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "anthropic:claude-subagent:2.1",
  "title": "Claude Code subagent definition front matter v2.1",
  "description": "Front matter accepted by Claude Code 2.1 in an agent definition — a markdown file under `.claude/agents/`, `~/.claude/agents/`, or a plugin's `agents/` directory, whose body is the subagent's system prompt. Eighteen fields, of which `name` and `description` are required: a file with no `name` is skipped as documentation and one with no `description` is refused with an error, so neither is optional the way a `SKILL.md` field is. Unknown keys are tolerated, because Claude Code records an unrecognised agent key as telemetry and loads the file anyway. The enumerated sets here are the ones the loader checks rather than the ones the documentation lists — `isolation` takes `remote` as well as `worktree`, and `permissionMode` takes `manual` as an alias for `default`. Three fields carry a plugin caveat: `permissionMode`, `hooks` and `mcpServers` are read for a project or user agent and ignored for a plugin one. Do not reach for this schema to check a `SKILL.md`. The two contracts look alike and are not, starting with tool grants spelled `tools` and `disallowedTools` here and `allowed-tools` and `disallowed-tools` there — see `anthropic:claude-skill:2.1`. See https://code.claude.com/docs/en/sub-agents",
  "type": "object",
  "additionalProperties": true,
  "required": ["name", "description"],
  "$defs": {
    "wordsOrList": {
      "description": "A field Claude Code accepts either as one delimited string or as a YAML list of entries. The string form is split on commas and spaces falling outside parentheses, so a parenthesised rule such as `Bash(npm run test:*)` survives as a single entry. `items` applies only to the array form, so no branch is needed and a bad entry is reported once, against the item that is wrong.",
      "type": ["string", "array"],
      "items": { "type": "string", "minLength": 1 }
    }
  },
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "pattern": "^[^-]",
      "description": "The agent's identity: how the Agent tool, the `--agent` flag and an `Agent(...)` tool grant address it, and what a hook sees as `agent_type`. Required — a file without one is skipped silently as documentation rather than reported. Independent of the filename, which is only where the agent was found, and of the directory it sits in. Constrained here to the one rule the loader enforces, that a name may not begin with `-`. The documented lowercase-and-hyphens convention is not enforced, and agent types with capitals in them ship today, so a schema holding this to `^[a-z-]+$` would fail agents that work. Avoid `:` all the same: it is the separator in a plugin-scoped identifier such as `my-plugin:reviewer`."
    },
    "description": {
      "type": "string",
      "minLength": 1,
      "description": "When to delegate to this agent, which is how Claude decides whether to call it and what the Agent tool listing shows. Required — an agent file without one is refused with `missing required 'description' in frontmatter`. Saying \"use proactively\" encourages automatic delegation. The descriptions of all custom agents share one budget, so keep this short; that budget is a truncation rather than an error, and is not enforced here."
    },
    "model": {
      "type": "string",
      "minLength": 1,
      "description": "Model the agent runs on, taking the same values as `/model` plus `inherit`, which matches the spawning conversation and is also what omitting the field does. Deliberately not enumerated: the accepted set moves with the product and is narrowed further by an organisation's `availableModels` allowlist, so a list assembled here would reject working agents."
    },
    "tools": {
      "$ref": "#/$defs/wordsOrList",
      "description": "Allowlist of tools the agent may call, replacing the default inherited set. Entries are exact tool names, scoped rules such as `Bash(npm run test:*)`, `Agent(worker, researcher)` to bound which agents it may spawn, or an `mcp__<server>` pattern. A list containing `*` imposes no restriction at all, which is what omitting the field already does."
    },
    "disallowedTools": {
      "$ref": "#/$defs/wordsOrList",
      "description": "Denylist of tools removed from whatever pool the agent ends up with, in the same entry format as `tools`. Useful for narrowing the inherited set without enumerating everything kept — an agent that may run commands but must not edit files."
    },
    "color": {
      "enum": [
        "red",
        "blue",
        "green",
        "yellow",
        "purple",
        "orange",
        "pink",
        "cyan"
      ],
      "description": "Display colour for the agent in the task list and transcript. A closed, published set of eight. Worth checking even though it is cosmetic: a colour outside the set is dropped without a warning, so the only symptom is an agent that never takes the colour you gave it."
    },
    "effort": {
      "type": ["string", "integer"],
      "pattern": "^(?:low|medium|high|xhigh|max|-?[0-9]+)$",
      "description": "Reasoning effort while the agent runs, overriding the session level. Either one of the five published levels or an integer, which is why this is typed as two: `pattern` constrains only the string form and leaves the integer one alone, so a wrong value is reported once, against the key. Which levels a model actually offers varies. The loader additionally folds case and resolves two internal aliases; neither is part of the published contract, and both are left out here so the accepted set matches `anthropic:claude-skill:2.1`. The integer form carries **no lower bound**, deliberately, and that asymmetry with `maxTurns` is a fact about the loader rather than an oversight: `maxTurns` is checked for a positive integer and warns otherwise, while `effort` is checked with a bare integer test, so `0` and `-3` are both taken and neither is reported. A `minimum` here would fail an agent that runs. The integer may also be written quoted, which is why the pattern carries a digit branch as well as the five names — the loader reaches the numeric form through `parseInt` on the string, so `\"8\"` and `8` are the same value to it, exactly as they are for `maxTurns`."
    },
    "permissionMode": {
      "enum": [
        "acceptEdits",
        "auto",
        "bypassPermissions",
        "default",
        "dontAsk",
        "manual",
        "plan"
      ],
      "description": "Permission mode the agent runs in, defaulting to the spawning conversation's. `manual` is accepted as an alias for `default` and normalised to it before the mode is checked, so both spellings are legal. Read for a project or user agent; ignored for a plugin agent, which is warned about rather than failed."
    },
    "maxTurns": {
      "type": ["integer", "string"],
      "minimum": 1,
      "pattern": "^[1-9][0-9]*$",
      "description": "Cap on agentic turns before the agent stops; its output is then marked partial and it can be resumed. A positive integer, written either as a YAML number or as a quoted string — typed as both, with `minimum` constraining only the number and `pattern` only the string, so a wrong value is reported once. `null` is rejected on purpose: the loader treats it as an invalid value rather than as an absent one."
    },
    "skills": {
      "$ref": "#/$defs/wordsOrList",
      "description": "Skills preloaded into the agent's context when it starts, by name. The full skill content is injected, so this is a budget as well as a convenience; the agent can still reach an unlisted skill through the Skill tool."
    },
    "mcpServers": {
      "type": "array",
      "description": "MCP servers available to the agent. Each entry is either the name of a server the parent session already runs, reused by reference, or a one-key map defining a server inline and scoped to this agent alone. The inner shape of an inline definition is left unconstrained here: it is the MCP server contract rather than the agent contract, and it is versioned separately. Read for a project or user agent; ignored for a plugin agent.",
      "items": { "type": ["string", "object"] }
    },
    "hooks": {
      "type": "object",
      "description": "Hooks registered for the lifetime of this agent, keyed by hook event, in the same shape as the hooks configuration. `Stop` is registered as `SubagentStop`. The inner shape is left unconstrained here for the same reason as `mcpServers`: it is the hooks contract, versioned separately. Read for a project or user agent; ignored for a plugin agent, and it needs a trusted workspace."
    },
    "memory": {
      "enum": ["user", "project", "local"],
      "description": "Scope of the agent's persistent memory directory: `user` for `~/.claude/agent-memory/<name>/`, `project` for `.claude/agent-memory/<name>/` — version-controlled and shared — and `local` for `.claude/agent-memory-local/<name>/`. Needs auto memory enabled. Setting this also forces the memory tools into the agent's tool set, even when `tools` did not list them."
    },
    "background": {
      "type": ["boolean", "string"],
      "pattern": "^(?:true|false)$",
      "description": "`true` keeps the agent in the background even when Claude asks for its result in the foreground. Accepts a YAML boolean, or the exact lowercase strings `true` and `false`. The two branches are not the same set and the difference is invisible in the file: YAML 1.2 resolves `true`, `True` and `TRUE` to booleans, so all three reach the validator on the boolean branch and pass, while a *quoted* `\"True\"` stays a string and fails, because the loader compares the string form against `'true'` and `'false'` exactly. This is the trap in moving between the two Claude Code contracts: a `SKILL.md` flag takes `yes`, `no`, `on`, `off`, `1` and `0` as well, while the agent loader answers every one of those with `Must be 'true', 'false', or omitted.` Written as two type-scoped keywords rather than a branch, so a wrong value is reported once, against the key."
    },
    "isolation": {
      "enum": ["worktree", "remote"],
      "description": "Where the agent's file changes land. `worktree` runs it in a temporary git worktree cut from the repository's default branch, cleaned up automatically when nothing changed; `remote` is the loader's other accepted value. The published documentation names only `worktree`, and the schema follows the loader, which warns on anything outside these two."
    },
    "initialPrompt": {
      "type": "string",
      "minLength": 1,
      "pattern": "\\S",
      "description": "First user turn, submitted automatically when this definition runs as the *main* session — through `--agent` or the `agent` setting — and prepended to any prompt the user supplies. Commands and skills in it are expanded. Not read when the definition is spawned as a subagent, which is the usual case, so an agent that only ever gets delegated to has no use for it. A value that is only whitespace is discarded silently by the loader, so the pattern requires one non-space character rather than leaving `minLength` to pass it."
    },
    "observer": {
      "type": "string",
      "minLength": 1,
      "pattern": "\\S",
      "description": "Agent type spawned automatically as a background observer whenever this agent runs, to watch how it works rather than to do the work. Names an agent type, so it is not enumerated: the set includes whatever the project, its plugins and the user define. Blank or whitespace-only is dropped silently, which is why the pattern demands a non-space character instead of letting `minLength` wave a run of spaces through."
    },
    "observerMessage": {
      "type": "string",
      "minLength": 1,
      "pattern": "\\S",
      "description": "Extra instruction appended, after the harness-owned default, to each activity digest sent to the `observer`. Only meaningful alongside `observer`; that pairing is a cross-field rule the schema does not express. Whitespace alone is discarded silently, so the pattern reports it."
    }
  }
}
