Claude Code Agent Loop Deep Dive (9): Sidechains, Sub-agents, and agentId Routing
The previous eight posts described one loop: a user presses Enter, the loop runs until completion, and control returns to the user.
Claude Code also has a more surprising arrangement: **a loop can run inside another loop**.
When the primary conversation requests the `Agent` tool, Claude Code starts a sub-agent to perform an independent task and return only its final result. From the main loop's perspective, this is a long-running tool that eventually returns text. From the sub-agent's perspective, it is a complete loop with its own messages, tool execution, and stop decisions.
The central question is whether this requires a separate sub-agent loop implementation—or whether it is the same loop with a different context.
## The same `queryLoop`, called recursively
The obvious design would define two implementations, perhaps `MainLoop` and `SubagentLoop`. Claude Code takes the opposite approach: main agents and sub-agents run the **same** `queryLoop` function.
Starting a sub-agent follows this shape:
The main loop sees an Agent tool_use
↓
Create a new toolUseContext
├── agentId = a new generated ID
├── messages = [] (a fresh history)
├── system prompt = a sub-agent-specific prompt
└── other scoped fields
↓
Call queryLoop(newToolUseContext) recursively
↓
The sub-agent reaches completed and returns its result
↓
Append that result to the main history as tool_result
It is one function, recursively invoked. A sub-agent can even start another sub-agent, which is simply another recursive call.
The benefit is maintenance: a bug fix, recovery transition, or scheduling improvement applies to both primary and sub-agent runs without being implemented twice.
## `agentId` routes the main-thread-only behavior
Shared code still needs to know which context it is running in. Claude Code uses a single flag:
* Main loop: `toolUseContext.agentId === undefined`
* Sub-agent: `toolUseContext.agentId` is a generated identifier
The loop contains several checks like `if (!toolUseContext.agentId)`. They select behaviors that exist only on the primary thread:
* **Memory prefetch** at session start; a sub-agent has its own prompt instead.
* **Mobile UI summaries** after the main turn ends; sub-agent turns do not need them.
* **MCP connection cleanup** ; a sub-agent must not close a connection the primary loop may reuse.
* **Stop-hook reentrancy locking** ; Stop hooks apply to the primary loop, not a completed sub-agent.
* **Cadence reminders** such as recurring TodoWrite prompts.
* **`CLAUDE.md` loading**; sub-agents use their own scoped instructions rather than automatically loading user-level instructions.
* **Session storage** ; main and sub-agent messages go to different transcript files.
One flag covers many branch points. It costs some local readability—each branch needs a reason—but preserves the much larger advantage of shared loop code.
## Sidechain transcripts live in separate files
Main-loop messages are persisted in a session transcript such as:
~/.claude/projects/<encoded-project>/<sessionId>.jsonl
Each line is a message and includes a `parentUuid` pointing to its predecessor.
Sub-agent messages do **not** go into that primary file. They are appended independently to a sidechain transcript:
.claude/subagents/agent-<agentId>.jsonl
This separation matters because the message histories are separate. If they shared a file, resuming a session would need to disentangle primary and sub-agent messages. A dedicated sidechain keeps the main transcript clean while preserving the sub-agent trace for debugging.
The main transcript records only the sub-agent's final `tool_result`, paired to the Agent tool-use ID. Its intermediate reasoning and tool calls stay in the sidechain.
## `parentUuid` creates a cross-file tree
The primary session's `parentUuid` values form a conversation tree. A sub-agent has its own independent chain in its own JSONL file, but its first message points back to the UUID of the main-loop message that requested the Agent tool:
Main tree:
user message
└── assistant: Agent tool_use
└── user: tool_result containing the final sub-agent result
Sidechain tree:
sub-agent message 1 (parentUuid = Agent tool_use UUID)
└── sub-agent message 2
└── ...
Logically, the sidechain hangs from the parent tool-use node. A tool can merge both trees when a complete trace is needed, while normal session viewing remains focused on the clean main tree.
## Sub-agent permissions are deliberately reset
As Part 1: From Tool Declarations to Pre-Execution Approval explains, a sub-agent does not inherit session-level `alwaysAllow` decisions from the main agent. It keeps immutable CLI-level configuration but starts with its own `allowedTools` policy.
The principle is conservative by design: trusting the main agent to “always allow Bash” does not automatically mean trusting an independent sub-agent the same way. The trade-off is that a sub-agent may ask for approval again; the benefit is safer default behavior.
This is especially important because shared `queryLoop` code means a sub-agent otherwise has the same underlying capabilities: tools, hooks, and recovery mechanisms. The isolation happens through **context** , not a stripped-down code path:
* Reset permission memory.
* Apply an explicit `allowedTools` allowlist.
* Provide sub-agent-specific instructions instead of inheriting user `CLAUDE.md` instructions.
The code is shared and the capability set is broad, but each sub-agent sees only its own context.
## Fork is a special kind of sub-agent
Claude Code also supports **forks**. A normal sub-agent starts with a fresh context and one assigned instruction. A fork inherits the main agent's full message history, but replaces every historical `tool_result` with the placeholder:
Fork started — processing in background
Why replace results? Forks are commonly launched in batches for similar independent tasks. If every fork preserved a different set of detailed tool results, their prompts would diverge and prompt-cache reuse would disappear. Replacing results makes the fork histories byte-identical, producing high cache reuse and sharply reducing the cost of a large batch.
Architecturally, forks are an extreme application of the same shared loop: many ordinary sub-agent loops can run with no separate execution engine.
## Interrupt propagation and error isolation
Sub-agents do not create their own `AbortController`; they receive the main loop's controller through `toolUseContext`. Ctrl-C on the primary loop therefore interrupts every active sub-agent too.
The reverse does not apply to errors. If a sub-agent hits `prompt_too_long` or another unrecoverable issue, it runs its own recovery path first. Only its final failure returns to the main loop as an error `tool_result`, and the main agent decides what to do next.
This is a useful isolation boundary: primary cancellation propagates downward, while sub-agent failures remain contained unless the main loop chooses to act on their reported result.
## All previous loop mechanisms still apply
Because sub-agents reuse `queryLoop`, the mechanisms from the previous posts still apply inside them:
* Approval exists, but with a separate permission context.
* Most hooks run, except primary-thread-specific behavior such as Stop hooks.
* Tools are batched by concurrency safety.
* Stop reasons determine continuation or completion.
* The state machine uses the same transitions.
* Model calls still stream through SSE, even when no UI displays that stream.
* The same retry and recovery layers apply.
* The shared abort controller can interrupt all active loops.
The lasting insight is simple: one loop, two contexts. Claude Code's loop is not only a mechanism for a user talking to an LLM; it is a general mechanism for an AI to autonomously advance a task. The main conversation, a sub-agent, and a fork are all instances of it.
## Takeaways
* Sub-agents recursively invoke the same `queryLoop`; they do not use a separate loop implementation.
* `agentId` gates primary-thread-only behavior such as memory prefetch, Stop hooks, MCP cleanup, and storage.
* Sidechain JSONL files preserve isolated sub-agent transcripts while keeping the main session clean.
* `parentUuid` links a sidechain root to the main Agent tool-use node across files.
* Permission memory is reset so trust in the main agent does not silently transfer to a sub-agent.
* Forks replace prior tool results with placeholders to maximize prompt-cache reuse.
* Cancellation propagates downward through the shared abort controller, while sub-agent errors return upward as tool results.
The final article closes the series by extracting five core insights and assembling the ten posts into one complete map of a general-purpose agent loop.
## References
**Primary source locations (v2.1.220):**
* `src/query.ts`: recursive `queryLoop` execution for sub-agents.
* `src/tools/AgentTool/AgentTool.tsx`: Agent-tool entry point.
* `src/tools/AgentTool/runAgent.ts`: sub-agent execution, permission reset, and isolated storage.
* `src/utils/sessionStorage.ts`: sidechain transcript paths.
* `src/tools/AgentTool/forkSubagent.ts`: fork behavior and tool-result placeholders.
**Related reading:**
* Part 0: From the Chat Window to the Loop
* Part 1: From Tool Declarations to Pre-Execution Approval
* Part 5: The QueryEngine Main Loop as a State Machine
* Part 8: From Ctrl-C to a Synthetic tool_result
* Claude Code documentation: Sub-agents