Why Claude Code’s Agent Loop Is Over 1,400 Lines
INTERNALS.md
SubscribeSign in
Why Claude Code’s Agent Loop Is Over 1,400 Lines<br>INTERNALS.md #4 · The nine failure modes that turned a simple loop into query.ts.
Lax Meiyappan<br>Jun 03, 2026
Share
When you type a prompt in Claude Code, a loop starts. Nine conditions can keep it running automatically, without asking you for input. Most of them have nothing to do with whether your task is finished.<br>The loop lives in query.ts, Claude Code’s core agent logic file, exposed via an npm source map in March 2026 and since analyzed by several engineers in the community. One while(true) spanning over 1,400 lines of production TypeScript.<br>This post opens that loop.
A note on sources. The implementation details here are based on the Claude Code v2.1.88 source, exposed via an npm source map in March 2026. query.ts is not publicly documented. Everything in this post is community analysis, cross-referenced against the official Claude Code docs. Anthropic ships frequently, so the architecture is stable but specific line numbers are not.
The Blueprint:<br>Start here: the naive version, and what breaks it
The architecture: why an async generator, why single-threaded, and what the choice costs
Before you type anything: what loads at session start and what it spends
The nine conditions: each failure mode that grew the loop past 1,400 lines
Start here: the naive version
Every agent tutorial gives you this:<br>while True:<br>response = call_llm(messages)<br>if response.tool_calls:<br>results = execute_tools(response.tool_calls)<br>messages.append(results)<br>else:<br>break While that 'naive' approach works fine in a controlled demo - where the API is stable and the task is trivial, it hits a wall the moment it encounters the messy reality of production - where network jitter is a given and long-running scripts have a habit of hanging indefinitely.<br>Then production happens. The API times out mid-response. The context window fills after 20 tool calls. A bash script hangs with no exit condition and freezes the terminal. A governance hook needs to review the output before the session ends. The laptop sleeps. The session is gone.<br>Each of those is a line in query.ts. The production version handles all of them. The rest of this post explains how.
The loop is not the smart part
Worth stating before anything else.<br>query.ts is a state machine. It calls an API, reads the response, executes whatever tools the model requested, feeds the results back, and calls the API again. The intelligence is entirely in the model. The loop keeps things correct when production introduces conditions the model cannot handle on its own.<br>That framing matters for every design decision that follows.
The architecture
Claude Code is built on a JavaScript runtime (Bun, per the v2.1.88 source analysis) with TypeScript in strict mode. The UI is Ink, a React implementation that renders to terminal cells. The agent logic lives in query.ts.<br>The loop is declared as an async generator:<br>export async function* query(<br>params: QueryParams,<br>): AsyncGenerator<br>Two decisions are embedded in this signature.<br>Typed events, not buffered output. The loop yields events as they happen: text tokens, tool calls, tool results, compaction markers. Each one renders to the terminal immediately. The result is that the user sees output character by character, with no buffering between the API and the screen.<br>A typed exit reason. The loop returns Terminal, not void. When it exits, it tells the caller exactly why: task complete, context limit hit, user interrupted, budget exhausted. Session recovery, Remote Control reconnection, and resume behavior all depend on this.
The loop is single-threaded: one instance, one thread, one loop. There is no shared mutable state between concurrent operations, no locks, and no race conditions within a session.<br>This is a deliberate bet. Parallelism adds throughput, but it also adds shared state between concurrent tool executions. Shared state introduces a class of bugs that single-threaded designs cannot have.<br>Ultimately, Anthropic favored a design that prioritizes deterministic, single-threaded correctness over the throughput gains you might get from aggressive parallel execution.<br>The cost of that bet is specific. A bash tool that runs a tight loop, a synchronous operation that blocks for minutes, a script with no exit condition: none of these can be interrupted from within the loop. The event loop freezes until the operation completes or the user hits Ctrl+C. That is the production risk this architecture accepts.<br>↓ INTERNALS: Architectural Tradeoffs 1<br>Early versions of Claude Code used recursion. The query function called itself. In long sessions with hundreds of tool calls, the call stack grew until it exploded. The current design carries mutable state in a State object between iterations. Each continue is a state transition, and a transition field records the reason, so error recovery and compaction logic know what...