Claude Code Subagent Runtime: Context Isolation and State Sharing

3 min readAI Agents

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.

ai-agentsclaude-codesubagentstasksruntime

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

Files covered in this post2 files
src/
├── tools/
│   ├── AgentTool/
│   │   ├── AgentTool.tsx
│   │   └── runAgent.ts

src/tools/AgentTool/AgentTool.tsx

Control plane for spawning subagents

Critical

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.

Module
Subagent Orchestration

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

Critical

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.

Module
Subagent Runtime

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:

  1. resolve the requested agent type and launch mode
  2. decide sync vs async, local vs remote, normal vs fork, and whether isolation is needed
  3. build a worker-specific tool pool and runtime context
  4. register task state for long-running work
  5. run a child query loop inside that scoped runtime
  6. persist transcript and metadata so the child can be resumed or summarized
  7. clean up all child-owned resources

Subagent runtime pipeline

The path from an Agent tool call to a managed child runtime.

  1. 1

    Interpret the request

    AgentTool.call

    AgentTool figures out whether this is a teammate spawn, a fork child, a normal subagent, a worktree-isolated child, or a remote agent.

  2. 2

    Resolve policy and capability

    loadAgentsDir.ts / hasRequiredMcpServers

    The runtime finds the effective agent definition, checks deny rules, required MCP servers, background settings, and isolation settings.

  3. 3

    Materialize child runtime state

    runAgent.ts

    runAgent builds an agent-scoped ToolUseContext, user/system context, tool pool, transcript target, and optional agent-specific MCP clients.

  4. 4

    Run as task-managed work

    Task.ts / tasks.ts / LocalAgentTask

    Foreground and background launches integrate with the task runtime so progress, notifications, and output files stay visible.

  5. 5

    Persist and clean up

    recordSidechainTranscript / writeAgentMetadata / finally block

    Sidechain 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 SubagentStart hooks
  • 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.

typescriptrunAgent.ts (simplified)

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?

hard

Both 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.