Skip to content

Custom subagents in Claude Code: scoped specialists you can verify

A custom subagent in Claude Code is a Markdown file in .claude/agents/ (project) or ~/.claude/agents/ (personal). Its YAML frontmatter sets the name, the delegation description, tools, model and permissions, and its body becomes the system prompt. Each subagent runs in its own context window and returns only a summary, so noisy side work stays out of the main conversation.

Forty minutes into a refactor, your session holds three full test runs, two repository-wide grep sweeps and a migration log. The constraint you set at the start (“do not touch the public API”) is buried under all of it, and the review you now want would compete for the same context. This page is for developers who want that side work done by scoped specialists, and for tech leads who commit a shared set of subagents to the repository and need them to behave the same for eight people.

  • Three subagent files you can commit today: a read-only reviewer that returns a verdict, a cheap test runner that reports only failures, and a worker that edits in its own git worktree.
  • A 30-line CI check that catches the frontmatter mistakes Claude Code skips without an error.
  • A rule for choosing each subagent’s model and effort, and a way to confirm what actually ran.
  • A verification routine that treats a subagent’s report as a claim to check, not as evidence.
  • A symptom-to-recovery table for the failures that come up in practice.

When should you use a subagent instead of a skill, a fork or a background session?

Section titled “When should you use a subagent instead of a skill, a fork or a background session?”

A subagent starts with a fresh context and sends back one summary. That suits self-contained work that produces output you will not read again, and does not suit work that needs your conversation history or constant back-and-forth.

You need…UseWhy
A noisy, self-contained side task: a test run, a log triage, a codebase searchA custom subagentOwn context window, own tool limits, only the summary comes back
A reusable procedure that should run with everything you have said so farA skillSkills run in the main conversation’s context
A side task that needs the whole conversation, without re-explaining itA fork: /subtask <task> (v2.1.212+)A fork inherits the conversation and reuses the parent’s prompt cache
Several independent tasks you supervise, each in its own sessionAgent view (claude agents, claude --bg)Separate sessions, separate worktrees, one dashboard
Dozens of agents driven by a script you can rerunDynamic workflowsOrchestration lives in code, not in one conversation
A quick question about something already in the conversation/btwSees your context, has no tools, and the answer is not added to history

Claude Code also ships built-in subagents: Explore and Plan (read-only research) and general-purpose (can edit). A custom subagent earns its place when you keep spawning the same kind of worker with the same instructions and the same limits.

A subagent is one Markdown file. Only name and description are required:

---
name: code-reviewer
description: Reviews the current branch's changes for correctness and security. Use proactively before a commit or pull request.
tools: Read, Grep, Glob, Bash
---
You review code. You never edit files. Report findings with file and line.

The body replaces the Claude Code system prompt for that subagent: it receives your text plus basic environment details such as the working directory. The name is the identity; the file name and any subfolder under agents/ do not matter.

Where subagent files live, and which one wins

Section titled “Where subagent files live, and which one wins”

When two definitions share a name, Claude Code uses the one from the higher-priority location:

PriorityLocationScope
1 (highest)Managed settingsOrganization-wide, set by admins
2--agents '{…}' JSON flagThe current session only
3.claude/agents/This project; commit it
4~/.claude/agents/All your projects
5 (lowest)A plugin’s agents/ directoryWherever the plugin is enabled

Claude Code watches the project and user agents directories, so an edit takes effect at the next delegation without a restart. The exception: if the directory did not exist when the session started, restart once after creating the first file.

A non-fork subagent starts with:

  • Its own system prompt (the file body) plus environment details.
  • The task message Claude writes when it delegates. This is the only channel for anything specific to the current job.
  • Your CLAUDE.md files, every level the main conversation loads, including an AGENTS.md loaded as project instructions. Set omitClaudeMd: true (v2.1.271+) to skip the user, project and local CLAUDE.md files (managed policy files still load) for a subagent that needs nothing but the task.
  • A git status snapshot and the full content of any skill listed in its skills field.

It does not see your conversation history, the files Claude already read, the main session’s auto memory, or your output style. So a rule you stated in chat ten minutes ago does not reach the subagent unless it is in CLAUDE.md, in the subagent’s own prompt, or in the delegation message. For where each CLAUDE.md level lives and what belongs in it, see the memory system.

Build three subagents that end in evidence

Section titled “Build three subagents that end in evidence”

The three files below split the work: one judges, one runs checks, one edits in isolation. Each prompt ends in a fixed report format, which is what lets you check the output without reading the transcript.

  1. Create the reviewer. It can read and run commands, but it cannot write or edit files, so it cannot “fix” a test to make its own verdict true.

    ---
    name: code-reviewer
    description: Reviews the diff against main for correctness, security and missing tests. Use proactively before a commit or pull request.
    tools: Read, Grep, Glob, Bash
    disallowedTools: Write, Edit, NotebookEdit
    model: inherit
    ---
    You review the changes between the merge base with main and the working tree.
    You never modify files.
    1. Run `git diff --merge-base main --stat`, then read every changed file in full.
    2. Check behaviour, not style: wrong logic, unhandled errors, injection,
    authorization gaps, and changed behaviour without a test that covers it.
    3. For every finding, run or name the command or test that demonstrates it.
    Reply in exactly this format and nothing else:
    VERDICT: APPROVE | REQUEST_CHANGES
    FINDINGS:
    - [blocker|major|minor] path/to/file.ts:LINE: what is wrong. Evidence: <command or test>
    CHECKED: <each command you ran, with its exit code>
    Skip anything a linter or formatter would catch. If you found nothing,
    say so and list what you checked.

    Bash can still change files through shell commands. For a strictly read-only reviewer, drop Bash and let the delegation message carry the diff, or add permissions.deny rules for the commands you fear.

  2. Create the test runner. Test output is the classic context flood. This subagent runs on the haiku alias because the job is mechanical and high volume.

    ---
    name: test-runner
    description: Runs tests and reports only the failures. Use whenever a test command would print more than a screen of output.
    tools: Bash, Read, Grep, Glob
    model: haiku
    maxTurns: 15
    ---
    Run the test command given in the task. If none is given, use the project's
    default test script from package.json, pyproject.toml or the Makefile.
    Never edit a file.
    Reply in exactly this format:
    COMMAND: <the exact command>
    EXIT: <exit code>
    RESULT: <passed> passed, <failed> failed, <skipped> skipped
    FAILURES:
    - <test name> at path:LINE: <assertion message, first 3 lines>
    Before you call a failure flaky, re-run that one test once and report both runs.
  3. Create an isolated worker for fan-out. isolation: worktree gives every run its own temporary git worktree, so parallel workers cannot overwrite each other’s edits.

    ---
    name: migration-worker
    description: Applies one mechanical change (a rename, an API migration) to the files named in the task. Use for parallel fan-out across modules.
    tools: Read, Edit, Write, Grep, Glob, Bash
    isolation: worktree
    model: sonnet
    effort: medium
    ---
    Make only the change the task describes, only in the files it names.
    Then run the type check and the tests that cover the changed files.
    Commit on your worktree branch with a message that names the change.
    Reply with: BRANCH, FILES CHANGED, and every command you ran with its exit code.
    If a check fails and the fix is outside your files, stop and report it.

    A subagent worktree branches from your repository’s default branch, not from your current HEAD. When workers must build on unpushed commits of your feature branch, set this in .claude/settings.json:

    {
    "worktree": { "baseRef": "head" }
    }
  4. Add a lint check for the files. Claude Code skips a subagent file without an error in the session when the frontmatter does not parse, when --- is not on line 1, or when name or description is missing. It also ignores unknown fields silently, so disallowed_tools instead of disallowedTools removes nothing. Save this as scripts/lint_agents.py (it needs pyyaml):

    #!/usr/bin/env python3
    """Fail CI when a subagent file would be skipped or would silently ignore a field."""
    import pathlib, sys, yaml
    KNOWN = {"name", "description", "tools", "disallowedTools", "model", "permissionMode",
    "maxTurns", "skills", "mcpServers", "hooks", "memory", "background",
    "omitClaudeMd", "effort", "isolation", "color", "initialPrompt", "experimental"}
    errors = []
    for path in sorted(pathlib.Path(".claude/agents").rglob("*.md")):
    text = path.read_text(encoding="utf-8").replace("\r\n", "\n")
    if not text.startswith("---\n"):
    print(f"warning: {path}: no frontmatter, so Claude Code treats it as a doc")
    continue
    try:
    meta = yaml.safe_load(text.split("---\n", 2)[1]) or {}
    except yaml.YAMLError as exc:
    errors.append(f"{path}: YAML does not parse: {exc}")
    continue
    if not isinstance(meta, dict):
    errors.append(f"{path}: frontmatter must be a YAML mapping")
    continue
    name = str(meta.get("name", ""))
    if not name or not meta.get("description"):
    errors.append(f"{path}: needs both name and description")
    if name.startswith("-") or ":" in name:
    errors.append(f"{path}: name must not start with '-' or contain ':'")
    for key in sorted(set(meta) - KNOWN):
    errors.append(f"{path}: unknown field '{key}' (ignored at runtime)")
    print("\n".join(errors) or "agents OK")
    sys.exit(1 if errors else 0)

    Run it in the same CI job as your linter. The job needs no secrets and only read access:

    .github/workflows/agent-config.yml
    name: agent-config
    on: [pull_request]
    permissions:
    contents: read
    jobs:
    lint-agents:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v7
    with:
    persist-credentials: false
    - run: pip install pyyaml && python3 scripts/lint_agents.py

    On pull_request, GitHub runs this workflow and scripts/lint_agents.py from the pull request itself, so a pull request can edit the check in the same diff as the agent file. Put .github/ and scripts/lint_agents.py under CODEOWNERS together with .claude/agents/ and require code-owner review in branch protection.

    The vendor docs suggest claude plugin validate .claude/agents for the same job. On v2.1.283 it printed “Validation passed” for a directory holding a file with broken YAML, a file with a misspelled field and a file with no name, even with --strict (tested 2026-09-26), so do not rely on it for project agent files yet.

  5. Try it. Commit the three files and the script, then paste the first prompt from the copy-paste section below. In the transcript, a delegation shows as a row such as code-reviewer(Review the diff). Run /tasks while a subagent is running to see its row and the model it runs on.

How do you make Claude pick the right subagent?

Section titled “How do you make Claude pick the right subagent?”

Claude chooses a subagent from its description, so write the description as a routing rule: what the subagent does, and the situation that should trigger it. “Use proactively after code changes” encourages automatic delegation. Keep descriptions short: when the combined descriptions of your custom subagents pass 15,000 tokens, Claude Code warns at startup, and every token of them sits in the main context.

When routing matters, stop relying on it. There are three levels of control:

HowWhat it guarantees
Name it in the prompt: “Use the test-runner subagent…”Nothing: Claude usually delegates, but decides
@-mention: type @ and pick it, or write @agent-test-runnerThat subagent runs for this task; Claude still writes its task message
Session-wide: claude --agent code-reviewer, or "agent": "code-reviewer" in .claude/settings.jsonThe main thread itself takes the subagent’s prompt, tools and model

--agent combines with background sessions: claude --agent code-reviewer --bg "review the open changes on this branch" starts the reviewer as its own session, which you then manage in agent view. In the agent-view dispatch box, a prompt whose first word matches a subagent name dispatches that subagent, so a prompt that starts with “test-runner” is not plain text.

To stop Claude from using a subagent at all, deny it in settings with its exact name:

{
"permissions": {
"deny": ["Agent(Explore)", "Agent(migration-worker)"]
}
}

The tools field is an allowlist and disallowedTools is a denylist; when both are set, the denylist applies first. Four rules decide whether a restriction holds:

  • A specifier removes the whole tool. disallowedTools: Bash(git push *) takes away all of Bash, not only git push. To keep Bash and block one command, add Bash(git push *) to permissions.deny in settings; that rule covers the main conversation and every subagent.
  • permissionMode is often ignored. When the main conversation runs in auto mode, acceptEdits or bypassPermissions, the subagent runs in that same mode whatever its file says. Auto mode is the starting mode for interactive sessions from v2.1.283 (the latest channel), unless a settings file sets another default mode, so in practice the tools list and deny rules are your real limits. A subagent that declares bypassPermissions never gets more than the main conversation has.
  • Background subagents get fewer tools. In an interactive session, fork mode is on by default and Claude Code runs the subagents Claude spawns in the background. A background subagent keeps every MCP tool but only a fixed set of built-in tools (such as Read, Grep, Glob, Bash, Edit, Write, WebFetch and Skill). The same file can therefore work in the foreground and miss a tool in the background. Its permission prompts appear in your main session, and a “for the rest of the session” answer there applies to the whole session.
  • Plugin subagents drop three fields. A subagent that comes from a plugin ignores hooks, mcpServers and permissionMode. Copy the file into .claude/agents/ if you need them.

Two fields add capability without adding context to the main session. mcpServers with an inline server definition connects that server only while the subagent runs, so its tool descriptions never load into the main conversation. hooks in frontmatter run only while that subagent is active, for example a PreToolUse hook that blocks SQL writes for a database reader. Both load from a project .claude/agents/ file only after you accept the workspace trust dialog for that folder; a claude -p run does not count as trusted.

Choose the model and effort for each subagent

Section titled “Choose the model and effort for each subagent”

Claude Code resolves a subagent’s model in this order: a model Claude passes for that one call, then the file’s model field, then the CLAUDE_CODE_SUBAGENT_MODEL environment variable, then the main conversation’s model. The field takes inherit, an alias (opus, sonnet, haiku, fable) or a full model ID.

Follow the same rule as the rest of this site: start on the default model, tune effort before switching model, and switch only when your own results say so.

  • Leave judgement work (review, design critique, debugging) on inherit. On the latest channel that is Claude Opus 5.5 by default; on stable (v2.1.274) the default depends on your plan.
  • Move mechanical, high-volume work (test runs, log triage, bulk renames) to sonnet or haiku, and set effort per subagent before you change the model.
  • Reserve fable for the longest-horizon work, chosen deliberately.

Prices and context windows for each model are on the model comparison hub.

Two surprises to know about. First, an alias from the same family as the main conversation’s model resolves to the main model: with the session on Opus 5.5, model: opus runs Opus 5.5, not whatever the alias points to elsewhere. Second, CLAUDE_CODE_SUBAGENT_MODEL is only a default; to force one model onto every subagent, also set CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1 (v2.1.257+). Confirm the result in /tasks, which shows the model on each running subagent’s row.

Every subagent sends its own requests against the same usage limits as your main conversation, and every returned summary lands in your main context; the fixed, short report formats above keep the second cost down. See cost control for Claude Code for budgets across a team.

How do you verify what a subagent reports?

Section titled “How do you verify what a subagent reports?”

A subagent’s report is a claim written by a model that you did not watch. Treat it the way you treat a colleague’s “tests pass” in chat: useful, and checked before it counts.

  • Demand evidence in the format. Every finding must carry a path:line and the command or test that shows it. A finding without evidence is a question, not a blocker.
  • Re-run the decisive check yourself. When the test runner says “0 failed”, have the main session run the same command once and compare exit codes. It is one command, not a full re-read.
  • Separate the writer from the judge. The session that wrote the code should not grade it. The reviewer above has no Write or Edit, so it cannot edit a test to make its own verdict true. For tests the implementing agent must not weaken, see protecting the oracle.
  • Keep the gate outside the agent. CI runs the type check, lint and tests on the pull request. A subagent’s APPROVE informs the merge decision; it never replaces CI. The developer who opens the pull request signs off on the merge, and the tech lead owns .claude/agents/ through CODEOWNERS.
  • Read the scan marker. From v2.1.210, Claude Code scans each subagent report and prepends a marker line when the text imitates Claude Code’s own tags or mentions permission settings such as bypassPermissions. A review that asks you to loosen permissions is a finding about the input it read, not advice.
  • Keep the audit trail. Subagent transcripts are stored as ~/.claude/projects/<project>/<session-id>/subagents/agent-<agent-id>.jsonl and deleted after cleanupPeriodDays (30 days by default). A SubagentStop hook in settings.json can copy the final report somewhere durable.

For the method behind this (explicit inputs, a bounded tool set, a fixed output and a stop condition), see scoped subagents: delegate bounded work, not responsibility.

  • One repository: commit .claude/agents/ and add a CODEOWNERS line for it, so a change to a shared reviewer’s rules gets the same review as a change to CI.
  • Personal helpers: keep them in ~/.claude/agents/. A project file with the same name overrides a personal one.
  • Many repositories: package the subagents as a plugin and install it from a team marketplace; see Claude Code plugins. Plugin subagents appear in the @-mention list under a scoped name such as my-plugin:code-reviewer. claude plugin eval measures how reliably Claude delegates to a plugin’s subagents across a set of prompts.
  • Organization-wide: admins deploy files to .claude/agents/ in the managed settings directory; those override project and user definitions with the same name.
  • Coming from another tool: /import codex, /import cursor or /import gemini brings in that tool’s subagents along with its instruction files, commands and MCP servers.

Codex and Cursor have their own subagent features. For the Codex side, see orchestrating multiple Codex agents; the scoped subagents guide compares all three tools.

SymptomLikely causeRecovery
The subagent is never chosenThe description is vague, overlaps another one, or is lost in a large description budgetRewrite it as a routing rule; @-mention the subagent when it must run
The subagent does not exist in the sessionFrontmatter does not parse, --- is not on line 1, name or description is missing, or name contains :Run claude --debug and read the log; add the lint script to CI
A field has no effectThe key is misspelled or in the wrong case (disallowed_tools, permission_mode)Use the camelCase names; the lint script flags unknown keys
“Agent would be spawned with zero tools”Every entry in tools is misspelled or unavailable to subagentsFix the names; check against the tools the main session actually has
It works in the foreground but a tool is missing in the backgroundBackground subagents keep only a fixed set of built-in toolsUse tools from that set, or set CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 for that run
permissionMode: plan did not stop an editThe main session was in auto mode, acceptEdits or bypassPermissions, which override the fieldRemove Write and Edit from tools; add deny rules in settings
A worktree worker cannot see your latest commitsSubagent worktrees branch from the default branchSet worktree.baseRef to "head", or push the base branch first
Frontmatter hooks or inline MCP servers never runThe folder holding the agent file is not trusted; -p sessions do not countOpen an interactive session in that folder and accept the trust dialog
A new subagent is “not found”The agents directory was created after the session startedRestart Claude Code once
The subagent runs a different model than the file saysSame-family alias resolution, CLAUDE_CODE_SUBAGENT_MODEL_FORCE, or an availableModels allowlist substitutionCheck the model in /tasks; use a full model ID if you need an exact model
“Concurrent subagent limit reached”20 subagents are already running in the session (the default)Let some finish, batch the work, or raise CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS
The main context fills up anywayEach subagent returns a long free-form reportPut a fixed, short reply format at the end of every subagent prompt