Skip to content

Multi-Agent System

Claude Code doesn’t just run one agent. It can spawn specialized agents, coordinate teams, and isolate parallel work. Understanding these patterns helps you choose the right approach for complex tasks.

3 Coordination Patterns

graph TB subgraph A["Pattern A — Subagent (Sequential)"] P1[Parent Agent] -->|"spawn"| C1[Child Agent] C1 -->|"result"| P1 end subgraph B["Pattern B — Coordinator (Parallel)"] CO[Coordinator Agent] -->|"SendMessage"| W1[Worker 1] CO -->|"SendMessage"| W2[Worker 2] CO -->|"SendMessage"| W3[Worker 3] W1 -->|"SendMessage"| CO W2 -->|"SendMessage"| CO W3 -->|"SendMessage"| CO end subgraph C["Pattern C — Fork (Parallel + Isolated)"] ROOT[Root Agent] -->|"fork + worktree"| F1[Fork A\nbranch: agent/feat-a] ROOT -->|"fork + worktree"| F2[Fork B\nbranch: agent/feat-b] F1 -->|"merge"| ROOT F2 -->|"merge"| ROOT end
Pattern A — SubagentPattern B — CoordinatorPattern C — Fork
ExecutionSequential (blocking)ParallelParallel
Git isolationNoNoYes (per-agent worktree)
Coordinator toolsFull4 onlyFull (each fork)
ContextIsolatedIsolated per workerCloned from parent
Best forFocused subtasksLarge parallel projectsConflicting file changes
Deep Dive: AgentTool Input Schema

When the main agent decides to spawn a subagent, it calls AgentTool with this schema:

AgentTool input:
task: string // Description of what the subagent should do
subagent_type: string // One of: "explore", "verification", "general-purpose",
// "plan", "claude-code-guide", or a custom agent name

The system resolves the agent type to a full definition containing: system prompt, allowed tools, model, memory scope, permission mode, and max turns. Custom agents defined in .claude/agents/ are discovered by matching the subagent_type against the frontmatter name field.

Why Coordinators Can’t Code

The coordinator in Pattern B is restricted to exactly 4 tools: TeamCreate, TeamDelete, SendMessage, and SyntheticOutput. No Bash. No FileEdit. No FileRead. No Grep.

This is a code-level constraint, not an instruction. The tools simply aren’t registered in the coordinator’s tool manifest.

Why? Because LLMs take the shortest path. If a coordinator has FileEdit, it will edit files directly instead of delegating. Removing execution tools forces the coordinator to divide work, assign it, and wait for results. The constraint creates the behavior:

# Without constraint (bad)
Coordinator thinks: "I could just edit this file myself... done."
Result: sequential, no parallelism
# With constraint (good)
Coordinator thinks: "I must delegate this to a worker."
Result: natural parallelism, natural specialization
Deep Dive: Recursive Architecture — runAgent()

The most important architectural insight: subagents run the exact same query() function as the main agent. There is no separate “subagent loop” — it’s recursive.

FUNCTION runAgent(agentDefinition, task, forkContext, worktreePath):
// 1. Resolve model (agent-specific or inherit parent)
model = getAgentModel(agentDefinition, parentModel)
// 2. Build context messages
IF forkContext:
// Fork: clone parent context, replace tool_results with placeholder
messages = filterIncompleteToolCalls(forkContext)
FOR EACH toolResult IN messages:
toolResult.content = "Fork started — processing in background"
ELSE:
// Fresh subagent: empty context
messages = []
// 3. Resolve permission mode
IF agentDefinition.permissionMode == "bubble":
// Permission requests float up to parent terminal
permissionHandler = parentTerminalHandler
ELSE:
permissionHandler = defaultHandler
// 4. Build system prompt from agent definition
systemPrompt = agentDefinition.markdownBody
// 5. Run the SAME query loop as the main agent
FOR AWAIT message OF query({model, messages, systemPrompt, tools: agentDefinition.tools}):
YIELD message // Stream results back to parent

This means every agent — main, subagent, fork — gets the same capabilities: 4-phase loop, escalating recovery, context defense, permission pipeline. The difference is only in configuration (which tools, which prompt, which permission mode).

6 Built-in Agents

AgentTool AccessRoleKey Detail
ExploreFileRead, Grep, Glob, Bash (read-only)Codebase discoverySystem prompt opens with CRITICAL: READ-ONLY MODE — defense at both prompt AND tool level
VerificationBash, FileRead, WebFetch, Agent (recursive)Break things”Your job is not to confirm — it’s to try to break it.” Detects two failure patterns: verification avoidance (describing tests without running them) and being seduced by the first 80%
General-purposeAll toolsVersatile worker”Complete the task fully — don’t gold-plate, but don’t leave it half-done”
PlanFileRead, Grep, Glob, Bash (read-only), AgentArchitectRead-only. Analyzes codebase and designs implementation plans
Claude Code GuideFileRead, Grep, Glob, Bash, WebFetch, WebSearchHelp deskFetches docs from code.claude.com
Statusline SetupFileRead, Bash, ConfigShell specialistReads shell config, converts PS1 prompts for statusline integration

Custom Agents

Define custom agents in .claude/agents/ as markdown files. Each file is a complete agent spec:

---
name: migration-specialist
description: Handles database migrations safely
model: claude-opus-4-5
tools: [FileRead, Bash, Grep]
whenToUse: "When working with files in migrations/ directory"
memory: project # user | project | local
isolation: worktree # run in separate git worktree
permissionMode: default # cannot be elevated by plugin agents
maxTurns: 20
hooks:
PostToolUse: "npm run test:migrations"
---
You are a database migration specialist. Before making any change,
read the existing migration files to understand naming conventions
and the current schema state.

Plugin agents are restricted. They cannot set permissionMode, hooks, or mcpServers per-agent. These fields would escalate privileges beyond what the user approved at install time. Plugin-level hooks (set at the plugin manifest) are trusted — per-agent escalation is not.

Deep Dive: Fork Agent Internals

Fork agents optimize for parallel execution with shared prompt cache:

FORK_PLACEHOLDER_RESULT: When forking, all tool_result blocks in the shared context are replaced with an identical placeholder string: "Fork started — processing in background". This ensures all fork children send byte-identical API request prefixes (system prompt + shared history + placeholder results). The API server caches this prefix — 3 fork children sharing cache = 3x token savings on input.

isInForkChild() anti-recursive guard:

FUNCTION isInForkChild(messages):
// Check if conversation contains the fork boilerplate tag
FOR message IN messages:
IF message.type == "user" AND message.content.includes("<fork-context>"):
RETURN true
RETURN false
// Usage: AgentTool checks before allowing fork
IF isInForkChild(currentMessages):
REJECT "Cannot fork from inside a fork child"

Without this guard: fork A spawns fork B spawns fork C → exponential explosion. The guard detects the boilerplate tag injected when the fork was created and prevents recursion.

buildWorktreeNotice(): Injected into the fork agent’s context:

"You are working in an isolated git worktree at ${worktreePath}.
Your changes are on branch ${branchName}.
Changes will be merged back to the parent branch when you complete your task."

The agent knows it’s on a temporary branch and can commit freely without affecting the main branch.

Session mode matching: When spawning a subagent, the child’s permission mode must be equal to or more restrictive than the parent’s. A default parent can spawn a plan child (more restrictive) but a plan parent cannot spawn a bypassPermissions child (less restrictive). This prevents privilege escalation through the agent chain.

Agent Memory

Agents have 3 memory scopes:

ScopeStorage LocationShared Across
user~/.claude/memory/All projects, all sessions
project.claude/memory/All agents in same project
localIn-session onlyCurrent session only

Agents can save state snapshots and restore them — enabling “sleep and wake” across sessions. A long-running analysis agent can checkpoint its progress, shut down, and resume exactly where it stopped.

Communication (Coordinator ↔ Workers)

Workers and coordinators communicate via SendMessage. Messages are queued in a per-agent mailbox and read at the start of each agent loop iteration. This is async and non-blocking — the coordinator sends a task and continues planning while workers execute.

Coordinator mailbox: [from: worker-1, "Found 3 auth bugs"]
[from: worker-2, "API spec complete"]
Worker-1 mailbox: [from: coordinator, "Review src/auth/**"]
Worker-2 mailbox: [from: coordinator, "Write API_SPEC.md"]

The coordinator reads all messages at turn start, aggregates results, and decides next steps — never waiting on a blocking call.

Why This Matters to You

  • When to use subagents → focused tasks that benefit from isolated context (exploration, verification, one-off generation)
  • When to use teams → large parallel refactors across multiple modules where workers can truly run independently
  • Why the coordinator can’t read your files → it forces delegation rather than sequential self-execution; not a bug
  • How worktrees prevent merge conflicts → each Fork agent operates on its own Git branch; changes are merged only when complete
  • How to define custom agents → create .claude/agents/your-agent.md with YAML frontmatter; the agent is available in the current session immediately

See also: The Agent LoopTool OrchestrationSkill Engine