Claude Code Subagent Runtime: Context Isolation and State Sharing
How runAgent materializes a subagent turn, why sub-agents receive a no-op setAppState but setAppStateForTasks always pierces to the root store, and how fork agents share prompt cache.
There is a big difference between:
- telling a model to "delegate work"
and:
- building a runtime that can actually launch, track, resume, isolate, and clean up child agents
Claude Code is doing the second thing.
File structure
src/
├── tools/
│ ├── AgentTool/
│ │ ├── AgentTool.tsx
│ │ └── runAgent.tssrc/tools/AgentTool/AgentTool.tsx
Control plane for spawning subagents
Routes Agent tool calls into teammate, fork, sync, async, worktree, or remote execution paths; resolves agent definitions; checks required MCP servers; and hands work off to runAgent or remote launch flows.
Key Exports
- AgentTool
- inputSchema
- outputSchema
Why It Matters
- AgentTool is an orchestrator, not a leaf tool.
- Subagent launching branches early into several runtime shapes with different lifecycle rules.
- Claude Code treats delegation as governed execution, not just prompt specialization.
src/tools/AgentTool/runAgent.ts
Materialize and run a child runtime
Builds an agent-scoped ToolUseContext, preloads skills, hooks, and agent-specific MCP servers, records transcript state, then runs a fresh query loop with explicit cleanup.
Key Exports
- runAgent
- filterIncompleteToolCalls
Why It Matters
- A subagent is a fresh query runtime with its own execution envelope.
- Agent-specific MCP, hooks, skills, and transcript state are assembled before the child loop starts.
- Cleanup is part of the contract: child-owned MCP servers, hooks, todos, and shell tasks are explicitly torn down.
The core idea
Claude Code does not implement subagents by recursively calling the model and hoping the transcript stays understandable.
It treats delegation as managed execution:
- resolve the requested agent type and launch mode
- decide sync vs async, local vs remote, normal vs fork, and whether isolation is needed
- build a worker-specific tool pool and runtime context
- register task state for long-running work
- run a child query loop inside that scoped runtime
- persist transcript and metadata so the child can be resumed or summarized
- clean up all child-owned resources
Subagent runtime pipeline
The path from an Agent tool call to a managed child runtime.
- 1
Interpret the request
AgentTool.callAgentTool figures out whether this is a teammate spawn, a fork child, a normal subagent, a worktree-isolated child, or a remote agent.
- 2
Resolve policy and capability
loadAgentsDir.ts / hasRequiredMcpServersThe runtime finds the effective agent definition, checks deny rules, required MCP servers, background settings, and isolation settings.
- 3
Materialize child runtime state
runAgent.tsrunAgent builds an agent-scoped ToolUseContext, user/system context, tool pool, transcript target, and optional agent-specific MCP clients.
- 4
Run as task-managed work
Task.ts / tasks.ts / LocalAgentTaskForeground and background launches integrate with the task runtime so progress, notifications, and output files stay visible.
- 5
Persist and clean up
recordSidechainTranscript / writeAgentMetadata / finally blockSidechain transcripts, metadata, hooks, MCP clients, todos, and child shell tasks are explicitly managed through the child lifecycle.
Why AgentTool.tsx matters so much
The key architectural observation is that AgentTool.tsx is not shaped like a normal tool.
It is a routing and lifecycle file.
Inside call(), Claude Code can choose among:
- teammate spawning
- implicit fork spawning
- regular local subagent execution
- background task launch
- worktree-isolated execution
- remote execution
That branching is exactly why the file matters.
It is the control plane for multi-agent behavior.
Agent definitions are more than prompt presets
loadAgentsDir.ts makes this even clearer.
Agents can define:
- prompt/system prompt behavior
- tool allowlists or deny lists
- skills
- MCP servers
- hooks
- model and effort
- permission mode
- memory scope
- background preference
- isolation mode
- max turn count
That is not just prompt templating.
It is a runtime envelope definition.
Fork mode is built around cache-stable delegation
One of the most interesting implementation details is fork mode.
The fork path is not just "spawn a child with the same context."
It is carefully designed to preserve:
- the parent's rendered system prompt bytes
- identical tool definitions
- most of the parent message prefix
Why?
Prompt cache stability.
That is a very specific and very practical optimization, and it tells you that Claude Code thinks about delegation at the runtime economics level, not only at the prompt-design level.
runAgent.ts is where a child becomes real
runAgent.ts is the actual materialization step.
Before the child query loop starts, it can:
- clone or rebuild file-read state
- choose abort-controller linkage
- override permission mode
- preload skills
- execute
SubagentStarthooks - register frontmatter hooks
- initialize agent-specific MCP servers
- record sidechain transcripts
Only then does it call query().
That ordering is the point.
The child starts as a prepared runtime, not as a bare recursive function call.
Tasks are what make background subagents durable
Task.ts is short, but it is doing an important job.
It gives long-running work a shared vocabulary:
- task type
- task status
- task id
- output file path
- terminal-state integration hooks
Then task implementations like local agent tasks and remote agent tasks plug into that contract.
This is why background subagents remain visible, inspectable, and killable instead of vanishing into transcript noise.
The real lesson
Claude Code does not treat subagents as a cute orchestration trick.
It treats them as managed child runtimes with:
- explicit launch modes
- typed task state
- isolated capability envelopes
- resumable transcript state
- cleanup guarantees
That is the right mental model if you want delegation to survive real engineering use instead of only working in demos.
Subagent launch and lifecycle across runtimes
The core pattern: resolve → build context → run child loop → clean up. JS matches the actual source; Python and Go show the same lifecycle.
The actual child runtime materialization from learning-claude-code
export async function runAgent({
agentId, systemPrompt, userContent, toolUseContext, agentDef, ...
}) {
// 1. Build agent-scoped ToolUseContext
const agentContext = {
...toolUseContext,
abortController: linkAbortController(toolUseContext.abortController),
readFileState: agentDef.shareFileReadState ? toolUseContext.readFileState : new FileStateCache(),
getAppState: () => appState,
setAppState: isBackgroundAgent ? noOp : toolUseContext.setAppState,
setAppStateForTasks: toolUseContext.setAppStateForTasks,
}
// 2. Preload agent-specific capabilities
await preloadSkills(agentDef.skills, agentContext)
const agentMcpClients = await connectAgentMcpServers(agentDef.mcpServers, agentContext)
await runSubagentStartHooks(agentContext)
await registerFrontmatterHooks(agentDef, agentContext)
// 3. Record sidechain transcript target
const transcriptPath = getSidechainTranscriptPath(agentId)
try {
// 4. Run child query loop
for await (const event of query({
messages: initialMessages,
systemPrompt: agentSystemPrompt,
toolUseContext: agentContext,
querySource: `agent:${agentId}`,
maxTurns: agentDef.maxTurns,
})) {
yield event
}
} finally {
// 5. Explicit cleanup — always runs
await disconnectAgentMcpServers(agentMcpClients)
await teardownFrontmatterHooks(agentContext)
await cleanupChildShellTasks(agentId, agentContext)
await cleanupChildTodos(agentId, agentContext)
await recordSidechainTranscript(transcriptPath, agentContext)
}
}What is the key difference between setAppState and setAppStateForTasks in Claude Code's subagent runtime?
hardBoth setAppState and setAppStateForTasks mutate session state. The distinction matters for background agents and session-scoped infrastructure.
AsetAppStateForTasks is only used by the task registry; setAppState is used by tools
Incorrect.The distinction is about scope, not who calls it.BsetAppState is a no-op for background async agents; setAppStateForTasks always reaches the root store
Correct!Correct. Async subagents have a no-op setAppState to prevent them from corrupting parent store state mid-turn. But session-scoped infrastructure (background tasks, session hooks) needs to register itself in the root store regardless of agent depth — that's what setAppStateForTasks provides.CsetAppState is synchronous; setAppStateForTasks is async
Incorrect.Both take a function argument and mutate state — the difference is which store they reach.DsetAppStateForTasks is only available in the main session, not in subagents
Incorrect.Opposite: setAppStateForTasks is specifically needed by subagents that need to register session-level infrastructure despite having a no-op setAppState.