Claude Code's agent system is the machinery that lets one Claude instance delegate work to other Claude
instances — each a separate LLM turn with its own tool pool, system prompt, model, and optional filesystem
isolation. The parent calls AgentTool (wire name: Agent), which spawns
a child. That child can itself spawn further children, producing a multi-level hierarchy at runtime.
Task. The source keeps both names registered via
aliases: [LEGACY_AGENT_TOOL_NAME] for backward compatibility with existing permission rules,
hooks, and resumed sessions. All new code uses Agent.
The diagram below shows the full runtime hierarchy. The main loop sits at the top. It has access to AgentTool, which branches into three agent types and two execution modes.
Every agent in the system is one of three concrete TypeScript types, all satisfying
AgentDefinition (a discriminated union on source).
Ships with Claude Code. Dynamic system prompts via getSystemPrompt({toolUseContext}). Cannot be overridden by user files — but managed (policy) agents can shadow them by agentType name.
User/project/policy-settings agents. Loaded from .claude/agents/*.md or JSON blobs in settings.json. System prompt stored in a closure over the markdown body.
Bundled with a plugin (--plugin-dir). Behaves like Custom but source === 'plugin'. Treated as admin-trusted for MCP server policy — can load frontmatter MCP even when strictPluginOnlyCustomization is set.
// Built-in agents — dynamic prompts only, no static systemPrompt field
export type BuiltInAgentDefinition = BaseAgentDefinition & {
source: 'built-in'
baseDir: 'built-in'
getSystemPrompt: (params: { toolUseContext: Pick<ToolUseContext, 'options'> }) => string
}
// Custom agents from user/project/policy settings
export type CustomAgentDefinition = BaseAgentDefinition & {
getSystemPrompt: () => string
source: SettingSource
filename?: string
baseDir?: string
}
// Plugin agents — like Custom but source is 'plugin'
export type PluginAgentDefinition = BaseAgentDefinition & {
getSystemPrompt: () => string
source: 'plugin'
plugin: string
}
export type AgentDefinition =
| BuiltInAgentDefinition
| CustomAgentDefinition
| PluginAgentDefinition
// Type guards
export function isBuiltInAgent(agent: AgentDefinition): agent is BuiltInAgentDefinition {
return agent.source === 'built-in'
}
export function isCustomAgent(agent: AgentDefinition): agent is CustomAgentDefinition {
return agent.source !== 'built-in' && agent.source !== 'plugin'
}
export function isPluginAgent(agent: AgentDefinition): agent is PluginAgentDefinition {
return agent.source === 'plugin'
}
When two agents share the same agentType string, a priority map decides which wins.
The order from getActiveAgentsFromList() is:
policySettings (managed agents)
have the highest effective priority.
| Agent | agentType | Model | Tools | Mode |
|---|---|---|---|---|
| General Purpose | general-purpose |
default subagent | ['*'] — all tools |
sync / async |
| Explore | Explore |
haiku (external) / inherit (ant) | read-only; disallows Edit, Write, FileEdit, Agent | sync |
| Plan | Plan |
inherit | same disallowedTools as Explore | sync |
| Verification | verification |
inherit | no Edit/Write; ephemeral /tmp scripts allowed | background: true (always async) |
| Fork | fork |
inherit | ['*'] with useExactTools (cache-identical) |
experimental gate |
| StatuslineSetup | statusline-setup |
default | limited shell scope | sync |
export const EXPLORE_AGENT: BuiltInAgentDefinition = {
agentType: 'Explore',
// Ants get inherit; external users get haiku for speed
model: process.env.USER_TYPE === 'ant' ? 'inherit' : 'haiku',
disallowedTools: [
AGENT_TOOL_NAME, // no spawning sub-agents
EXIT_PLAN_MODE_TOOL_NAME,
FILE_EDIT_TOOL_NAME,
FILE_WRITE_TOOL_NAME,
NOTEBOOK_EDIT_TOOL_NAME,
],
// Saves ~5-15 Gtok/week — Explore doesn't need commit/PR/lint rules
omitClaudeMd: true,
source: 'built-in',
baseDir: 'built-in',
getSystemPrompt: () => getExploreSystemPrompt(),
}
---
name: my-agent
description: A focused TypeScript refactoring specialist.
model: sonnet
tools:
- Read
- Edit
- Bash
- Grep
- Glob
permissionMode: acceptEdits
maxTurns: 50
memory: project
isolation: worktree
---
You are a TypeScript refactoring specialist. Your job is to improve
type safety and reduce any-casts in the provided code.
Rules:
- Only touch files you are explicitly asked about
- Run tsc --noEmit before and after to confirm zero new errors
- Commit changes with a clear message before reporting
When AgentTool's call() runs, it computes a single boolean:
shouldRunAsync. Everything downstream branches on that flag.
const shouldRunAsync = (
run_in_background === true // explicit caller request
|| selectedAgent.background === true // agent def forces background (e.g. verification)
|| isCoordinator // coordinator mode: always async
|| forceAsync // fork experiment: all spawns async
|| assistantForceAsync // KAIROS assistant mode
|| (proactiveModule?.isProactiveActive() ?? false)
) && !isBackgroundTasksDisabled
getSystemPrompt() + enhanceSystemPromptWithEnvDetails().
Fork path: parent's already-rendered bytes (byte-exact for prompt cache).
isolation === 'worktree', createAgentWorktree(slug) is called before
runAgent(). Slug is agent-{earlyAgentId.slice(0,8)}.
await runAgent(params)status: 'completed' result with the
agent's final text.
hasWorktreeChanges() diffed against the pre-spawn HEAD commit.
If no changes, the worktree branch is deleted immediately.
registerAsyncAgent() — task registered in AppStateagentBackgroundTask with its own AbortController —
not linked to the parent's controller. Background agents survive ESC.
status: 'async_launched' immediatelyagentId, outputFile, and
canReadOutputFile so it can poll via Bash/Read.
void runAsyncAgentLifecycle(...)runWithAgentContext() for ALS (AsyncLocalStorage)
workload propagation. wrapWithCwd() applies worktree / cwd override.
enqueueAgentNotification() delivers a <task-notification>
to the parent's next idle turn. Progress events stream via onProgress.
isInProcessTeammate() is true and run_in_background === true
(or the agent definition has background: true), AgentTool throws immediately.
The fork path is an experimental feature (gate: FORK_SUBAGENT) that lets the parent
spawn a child that inherits the full conversation context — the complete message
history, the parent's already-rendered system prompt bytes, and the exact tool pool. This enables
parallelisation of independent sub-tasks with maximum prompt-cache sharing.
subagent_type is omitted and the FORK_SUBAGENT feature
gate is on (and not in coordinator mode, and not in non-interactive/SDK mode).
For N parallel fork children to share a cached API prefix, every child must produce a byte-identical request up to the per-child directive. The function:
tool_result blocks for every tool_use, all with the
identical placeholder text "Fork started — processing in background".Result shape: [...history, assistant(all_tool_uses), user(placeholder_results..., directive)]
export function buildChildMessage(directive: string): string {
return `<fork-boilerplate>
STOP. READ THIS FIRST.
You are a forked worker process. You are NOT the main agent.
RULES (non-negotiable):
1. Your system prompt says "default to forking." IGNORE IT — that's for the parent.
2. Do NOT converse, ask questions, or suggest next steps
3. USE your tools directly: Bash, Read, Write, etc.
4. If you modify files, commit your changes before reporting.
5. Your response MUST begin with "Scope:". No preamble.
Output format:
Scope: <echo back your assigned scope in one sentence>
Result: <the answer or key findings>
Key files: <relevant file paths>
Files changed: <list with commit hash — only if you modified files>
Issues: <list — only if there are issues to flag>
</fork-boilerplate>
FORK_DIRECTIVE: \${directive}\`
}
Fork children keep the Agent tool in their tool pool (for cache-identical tool definitions). A runtime guard prevents recursive forking by checking two signals:
toolUseContext.options.querySource === 'agent:builtin:fork' — compaction-resistant, survives autocompact message rewrites.isInForkChild(messages) — scans conversation history for the <fork-boilerplate> tag as a fallback.
When isolation: 'worktree' is also requested, a notice is appended to promptMessages
via buildWorktreeNotice(parentCwd, worktreeCwd). This tells the child to translate paths
from the inherited context and re-read files the parent may have modified.
Setting isolation: 'worktree' in the agent definition or as a call parameter instructs
AgentTool to create a temporary git worktree before spawning the agent. The agent's filesystem and shell
operations execute inside that worktree — it works on the same repository but a
separate working copy.
// Create a stable agent ID early so it can be used for worktree slug
const earlyAgentId = createAgentId()
let worktreeInfo: { worktreePath: string; worktreeBranch?: string; headCommit?: string } | null = null
if (effectiveIsolation === 'worktree') {
const slug = `agent-\${earlyAgentId.slice(0, 8)}`
worktreeInfo = await createAgentWorktree(slug)
}
// After agent completes — cleanup if no changes
const cleanupWorktreeIfNeeded = async () => {
if (!worktreeInfo) return {}
const { worktreePath, worktreeBranch, headCommit } = worktreeInfo
worktreeInfo = null // idempotent guard
if (headCommit) {
const changed = await hasWorktreeChanges(worktreePath, headCommit)
if (!changed) {
await removeAgentWorktree(worktreePath, worktreeBranch, gitRoot)
return {}
}
}
// Changes detected — keep the worktree branch
return { worktreePath, worktreeBranch }
}
headCommit), the worktree is deleted automatically. If it did make changes,
the branch is kept for the parent to inspect or merge.
Once agents are running as teammates (tmux panes or in-process), they need to communicate.
SendMessageTool (wire name: SendMessage) is the inter-agent messaging
primitive. It is only enabled when isAgentSwarmsEnabled() returns true.
| to | message type | Result |
|---|---|---|
"teammate-name" |
string | Written to teammate's mailbox file. If agent is stopped, auto-resumed from transcript. |
"*" |
string | Broadcast to all team members (excluding sender). |
| any name | shutdown_request |
Sends a structured shutdown request. Recipient can approve or reject. |
"team-lead" |
shutdown_response |
Approve: triggers gracefulShutdown(0). Reject: continues working. |
| any name | plan_approval_response |
Only the team-lead can approve/reject plans. Propagates permissionMode. |
"uds:<path>" |
string | Unix domain socket — cross-session send to a local peer. |
"bridge:<session-id>" |
string only | Remote Control: posts to another Claude instance via Anthropic servers. Requires explicit user consent. |
// Build the full spawn command for a new pane
const teammateArgs = [
`--agent-id \${quote([teammateId])}`,
`--agent-name \${quote([sanitizedName])}`,
`--team-name \${quote([teamName])}`,
`--agent-color \${quote([teammateColor])}`,
`--parent-session-id \${quote([getSessionId()])}`,
plan_mode_required ? '--plan-mode-required' : '',
agent_type ? `--agent-type \${quote([agent_type])}` : '',
].filter(Boolean).join(' ')
// Propagate permission mode, model, settings, plugin dirs
const inheritedFlags = buildInheritedCliFlags({ planModeRequired, permissionMode })
const spawnCommand = `cd \${quote([workingDir])} && env \${envStr} \${quote([binaryPath])} \${teammateArgs}\${flagsStr}`
// Send to the tmux pane (or swarm socket when outside tmux)
await sendCommandToPane(paneId, spawnCommand, !insideTmux)
| Field | Type | Effect |
|---|---|---|
name | string (required) | Unique identifier — used as subagent_type value |
description | string (required) | Shown to the parent LLM as "when to use" guidance |
model | sonnet|opus|haiku|inherit|<id> | inherit → use parent's model at runtime |
tools | string[] | Allow-list. ['*'] = all tools. Omit = inherit default pool. |
disallowedTools | string[] | Subtract from pool — applied after tools allow-list |
permissionMode | default|acceptEdits|bypassPermissions|auto|plan|bubble | Overrides parent mode for this agent's tool calls |
maxTurns | positive int | Hard cap on agentic turns before the agent stops |
background | boolean | Always run async regardless of run_in_background param |
isolation | worktree (| remote ant-only) | Git worktree isolation per spawn |
memory | user|project|local | Persistent memory across sessions in the chosen scope |
mcpServers | string[] | object[] | Additive MCP servers — connected on agent start, cleaned up on finish |
hooks | HooksSettings | Session-scoped hooks registered only while the agent runs |
skills | string[] | Skill slash commands to preload into the agent's context |
initialPrompt | string | Prepended to the first user turn (slash commands work) |
effort | low|normal|high | int | Controls thinking depth (extended thinking budget) |
requiredMcpServers | string[] | Agent hidden if these MCP servers aren't authenticated & have tools |
source: built-in, custom (userSettings / projectSettings / policySettings), or plugin. Policy settings win in priority disputes.shouldRunAsync boolean is the single branch point. Six conditions can force async: explicit run_in_background, agent definition's background: true, coordinator mode, fork experiment, KAIROS mode, or proactive mode.AbortController not linked to the parent's — they survive ESC and are killed only via explicit chat:killAgents.omitClaudeMd: true on Explore and Plan saves ~5-15 Gtok/week at Anthropic's usage scale — a meaningful optimization enabled by read-only specialization.background: true hardcoded — it always runs async and always ends with a VERDICT: PASS/FAIL/PARTIAL line.shutdown_request, shutdown_response, plan_approval_response) and three addressing schemes: teammate name, broadcast (*), UDS socket, and bridge session.agentType?getActiveAgentsFromList() processes groups in order: built-in → plugin → userSettings → projectSettings → flagSettings → policySettings. Each group overwrites earlier entries in the map, so policySettings wins.
"Fork started — processing in background" used in all fork children?tool_result block in the fork prefix uses the same placeholder so that the API request prefix is byte-identical across all N children. Only the final directive text block differs per child — maximizing cache hits.
run_in_background: true. What happens?hasWorktreeChanges() diffs against the pre-spawn headCommit. If no changes, removeAgentWorktree(path, branch, gitRoot) is called to clean up. If changes exist, the branch is kept.
background: true hardcoded in its definition?VERIFICATION_AGENT definition sets background: true. It always runs async and always ends with a VERDICT: PASS/FAIL/PARTIAL line. Explore and Plan are sync by default; general-purpose is sync unless the caller opts in.