Appearance
Custom Subagents
Built-in agents handle generic patterns, Explore for research, Plan for planning, General-purpose for unbounded delegation. Custom subagents are where you encode domain-specific knowledge into a reusable worker. A custom subagent is a markdown file with YAML frontmatter that defines the agent's name, description, allowed tools, model, and hooks. Drop it in .claude/agents/ and Claude can spawn it like any built-in agent, but this one knows your codebase's conventions, uses only the tools you permit, and can even run on a cheaper model to save credits.

What you'll learn
- Custom subagents live in
.claude/agents/<name>.md(project-scoped) or~/.claude/agents/<name>.md(personal, available across all projects) - YAML frontmatter defines
name,description,tools,model, and optionalhooks. The description is how Claude decides when to spawn this agent, write it as if you're telling Claude what tasks this agent handles - Tools are deny-by-default. If you omit the
toolsfield, the subagent gets zero tools. List every tool you want to allow, including specific command patterns likeBash(git *)orBash(npm run lint:*) - Set
tools: - Agenttodenyto prevent a subagent from delegating to other subagents, critical for leaf workers that should never spawn children - Agent location priority: managed settings >
--agentsCLI flag >.claude/agents/>~/.claude/agents/> plugin agents. Project agents take precedence over personal ones, so a project can override a personal agent with the same name
The problem
You're working on a Python project with a specific linting and testing setup. Every time you ask Claude to write code, it runs pytest when your project uses tox, and it tries black when you use ruff format. You write this in CLAUDE.md, but it still gets it wrong 30% of the time because the instructions get diluted across a long context.
Or: you want to delegate PR review to a subagent, but you don't want that subagent to be able to push commits, run deployments, or access your production database. General-purpose has every tool the parent has, if you delegate to General-purpose, it can do all of those things. You need a subagent with a restricted toolset.
Custom subagents solve both problems. The linting agent always runs tox and ruff because that's what its markdown file says. The PR reviewer can read code and comment on PRs but can't touch git. And both use smaller models than the parent, saving you credits on repetitive work.
The anatomy of a custom subagent
A custom subagent is a markdown file with YAML frontmatter. The frontmatter is the configuration, the body is the agent's system prompt.
markdown
---
name: pr-reviewer
description: Reviews pull requests for code quality, security issues, and adherence to project conventions. Use me for any PR review task.
tools:
- Read
- Glob
- Grep
- Bash(git diff *)
- Bash(git log *)
- Bash(gh pr view *)
- Bash(gh pr comment *)
model: claude-sonnet-4-20250514
---
# PR Reviewer
You review pull requests. Your job is to find issues, not to fix them.
## What you check
1. **Security**: hard-coded secrets, injection vulnerabilities, unsafe deserialization
2. **Correctness**: logic errors, off-by-one, null/undefined handling
3. **Conventions**: does the code follow this project's patterns?
4. **Tests**: are new paths covered? are edge cases tested?
## What you never do
- Commit or push code
- Merge pull requests
- Run deployment commands
- Access production systems
## Output format
For each issue: file, line, severity (critical/high/medium/low), description, and suggested fix.The frontmatter fields
| Field | Required | What it does |
|---|---|---|
name | Yes | Identifier used in logs, the agent view, and --agents registration. Lowercase, hyphens. Must be unique within a scope |
description | Yes | The trigger. Claude reads this to decide whether to spawn this agent. Write it in the second person: "Use me when..." or "I handle..." |
tools | Recommended | The allowlist. If omitted, the agent gets zero tools. List tool names exactly as they appear in Claude Code's tool registry: Read, Write, Edit, Glob, Grep, Bash(...), Task, Agent, mcp__<server>__<tool> |
model | Optional | Override the model for this agent. Use cheaper models for simple tasks: claude-haiku-4-5-20251001 for linting, claude-sonnet-4-20250514 for review |
hooks | Optional | PreToolUse, PostToolUse, and other hook triggers specific to this agent. See Day 2 for hook syntax |
Tool patterns
The Bash() tool takes a command pattern in parentheses. The pattern is a glob , * matches anything, specific strings match exactly:
Bash(git *) # Any git command
Bash(git diff *) # git diff with any arguments
Bash(git commit *) # git commit, dangerous, be intentional about allowing this
Bash(npm run lint:*) # Any npm script starting with "lint:"
Bash(npm test) # Exactly "npm test", no argumentsThe mcp__<server>__<tool> syntax gates access to MCP server tools. If you've connected a database MCP server called postgres, you might allow mcp__postgres__query_readonly but deny mcp__postgres__query_write.
To explicitly deny a tool (even if a broader allowlist would match it), prefix with the deny syntax in the parent's permissions, see the Permissions & Security lesson.
Agent location and priority
Claude Code resolves custom subagents in this order, with the first match winning:
- Managed settings , agents defined in
.claude/settings.jsonunder theagentskey --agentsCLI flag ,claude --agents ./my-agents/points to a directory of agent definitions.claude/agents/, project-scoped agents, committed to version control~/.claude/agents/, personal agents, available across all projects- Plugin agents , agents shipped with Claude Code plugins
Project agents (.claude/agents/) take priority over personal agents (~/.claude/agents/). If both define an agent called pr-reviewer, the project version wins when you're in that project. This means teams can enforce specific agent behavior via version control while individuals maintain their own variants for personal projects.
Build it
Step 1: Create a simple linting agent
Create .claude/agents/linter.md:
markdown
---
name: linter
description: Runs project linters and reports results. Use me when you need to lint code or check formatting. I never modify files.
tools:
- Read
- Glob
- Bash(npm run lint)
- Bash(npm run lint:fix)
- Bash(npx eslint *)
- Bash(npx prettier --check *)
model: claude-haiku-4-5-20251001
---
# Linter Agent
You run linting checks. You do not fix code, you only report what's wrong.
## Process
1. Check which linters the project uses (look at package.json scripts)
2. Run the linting commands
3. Parse the output into a clean summary: file, line, rule, message
4. Return the summary. Do not suggest fixes, that's a different agent's job.Step 2: Check that Claude can see it
In a Claude Code session, ask:
What custom subagents are available in this project?Claude will read .claude/agents/ and list the agents it found. If linter doesn't appear, check that the file is named .md and the YAML frontmatter is valid (three dashes --- on the first line, name and description fields present).
Step 3: Trigger the custom agent
Give Claude a task that matches the agent's description:
Lint all the TypeScript files that were changed in the last commit.Claude should recognize the pattern, see the linter agent in its registry, and spawn it. You'll see the agent name in the output. The linter agent runs eslint or whatever the project uses, formats the results, and returns them to the parent.
Step 4: Create a restricted review agent
The most common custom subagent is a reviewer that can read but not write:
markdown
---
name: code-reviewer
description: Reviews code for quality, security, and convention issues. Use me for code review, PR review, or pre-commit checks. I never modify code.
tools:
- Read
- Glob
- Grep
- Bash(git diff *)
- Bash(git log *)
- Bash(git show *)
- Bash(gh pr view *)
- Bash(gh pr diff *)
model: claude-sonnet-4-20250514
---
# Code Reviewer
Review code changes and report issues. You are read-only by design.
## Review checklist
- Security: injection, secrets, unsafe operations
- Correctness: logic errors, edge cases, type issues
- Performance: N+1 queries, unnecessary allocations, blocking calls
- Maintainability: naming, structure, duplication
## Output
For each issue found: file, line range, severity, category, description.
End with a summary: total issues by severity, overall assessment.Step 5: Prevent a custom agent from delegating
If an agent should never spawn subagents (a leaf worker), deny the Agent tool:
markdown
---
name: leaf-worker
description: Performs a single, well-defined task and returns. Use me for focused, non-delegatable work.
tools:
- Read
- Write
- Edit
- Bash(git *)
- Agent: deny
model: claude-haiku-4-5-20251001
---The Agent: deny line explicitly blocks the agent from using Task (subagent delegation). Even if the model decides delegation would help, the tool restriction prevents it.
What goes wrong
| Mistake | How you notice it | The fix |
|---|---|---|
| YAML indentation error in frontmatter | The agent doesn't appear in "What custom subagents are available?" | YAML frontmatter is strict about indentation. Use two spaces, not tabs. The frontmatter must start and end with --- on its own lines |
Missing tools field, agent gets zero tools | Claude spawns the agent, the agent immediately fails with "no tools available" | Add a tools field. Every agent needs at least Read to function. Tools are deny-by-default, omitting the field means zero tools |
| Bash pattern too broad | The agent runs commands you didn't intend, like git push --force when you only wanted git diff | Be specific in Bash patterns. Bash(git *) is dangerous. Use Bash(git diff *), Bash(git log *), and explicitly list only the commands the agent needs |
| Agent description doesn't match what Claude looks for | Claude delegates to General-purpose instead of your custom agent | The description is the dispatch key. Write it as if you're telling Claude "use this agent when...". Be explicit about the trigger conditions: "Use me for Python linting tasks" rather than "I lint code" |
| Personal agent overridden by project agent with same name | Your personal pr-reviewer agent doesn't load in a project that has its own | This is by design, project agents win. If you want your personal variant, use a different name, or add --agents ~/.claude/agents/ to override (but this replaces all project agents) |
| Agent runs on an expensive model by default | Your linting agent burns Opus credits on style checking | Set model: claude-haiku-4-5-20251001 in the frontmatter. Simple tasks don't need frontier models |
Confirm it worked
Execute the following commands to ensure your custom agent correctly applies its specialized tool restrictions.
bash
# 1. Create the linter agent from Step 1
mkdir -p .claude/agents
# Write the linter agent file as shown above
# 2. Start Claude Code and verify discovery
claude
# "What custom subagents are available in this project?"
# Verify: linter appears in the list
# 3. Trigger the agent
# "Lint all files changed in the last commit."
# Verify: Claude spawns the linter agent, you see its name in the output
# 4. Verify tool restrictions
# "Using the linter agent, lint the code and also reformat it."
# Verify: linter reports lint issues but does not reformat ,
# reformatting is not in its tool allowlist
# If all checks pass, custom subagents are working.Next: Agent Teams , coordinating multiple peers with --teammate-mode.