Inside the Agent SDK loop: how Claude Code runs your prompts
A tour through the execution loop that powers every agent — the five-step cycle, message types, tool execution, and the control surfaces that keep agents predictable.
Overview
The Agent SDK wraps Claude Code’s execution loop into a programmable interface. When query() is called with a prompt, the SDK does not make a single API request. It enters a loop — Claude evaluates the prompt, calls tools, reads results, and repeats until the task is done. The SDK yields a stream of typed messages at each stage so the application can observe progress, intercept tool calls, and handle the final result.
Understanding the loop matters for three reasons. First, every agent behavior — tool selection, parallel execution, context compaction — is a product of this cycle, not magic. Second, the control surfaces (max turns, budget, effort, permissions) directly shape how the loop runs. Third, debugging an agent that stalls, loops, or drains context is easier when the loop’s internals are visible.
The loop cycle
Every agent session follows the same five-step cycle. The SDK sits between the user and Claude, forwarding prompts, executing tools, and collecting results. Claude never talks to tools directly — the SDK mediates every interaction.
The loop has a branching point. When Claude responds with tool calls (row 3), the SDK executes them and feeds results back (rows 4–6). The cycle repeats — Claude may call more tools or produce a final answer. When Claude responds with text only and no tool calls (row 7), the loop ends and the ResultMessage fires (row 8).
A quick question like “what files are here?” might take one turn: Claude calls Glob and responds with the listing. A complex task like “refactor the auth module and update the tests” can chain dozens of tool calls across many turns — reading files, editing code, running tests — with Claude adjusting its approach based on each result.
Inside a turn
A turn is one round trip inside the loop: Claude produces output, the SDK executes any tools, and the results feed back. This happens without yielding control back to the calling code. Turns repeat until Claude produces output with no tool calls.
The SDK yields five message types as the loop runs. Each type marks a specific lifecycle stage.
SystemMessage fires at session start (subtype "init", contains session ID and metadata) and after compaction (subtype "compact_boundary"). AssistantMessage fires after each Claude response — it carries text content blocks and tool call blocks from that turn. UserMessage fires after tool execution with the results sent back to Claude. ResultMessage marks the end of the loop with final text, token usage, cost, and session ID. StreamEvent is optional — it carries raw API streaming deltas when partial messages are enabled.
Consider the prompt “Fix the failing tests in auth.ts” across four turns:
- Claude calls
Bashto runnpm test— the SDK yields an AssistantMessage, executes the command, then yields a UserMessage with the output (three failures). - Claude calls
Readonauth.tsandauth.test.ts— the SDK returns the file contents. - Claude calls
Editto fix the code, thenBashto re-run tests — all three pass. - Claude produces a text-only response: “Fixed the auth bug, all three tests pass now.” The SDK yields the final AssistantMessage, then a ResultMessage.
That was four turns: three with tool calls, one final text-only response.
Tool execution
Tools turn Claude from a text generator into an agent that reads files, runs commands, searches code, and spawns subagents. The SDK includes the same tools that power Claude Code: Read, Write, Edit for file operations; Glob, Grep for search; Bash for execution; WebSearch, WebFetch for web access; and Agent, Skill, AskUserQuestion, TodoWrite for orchestration.
Tools can be extended with MCP servers, custom tool handlers, and project-level skills loaded via settingSources.
Permissions
Three options work together to control what runs. allowed_tools auto-approves listed tools — a read-only agent with ["Read", "Glob", "Grep"] runs those without prompting. disallowed_tools blocks listed tools regardless of other settings. permission_mode controls what happens to everything else: "default" triggers an approval callback, "acceptEdits" auto-approves file edits and common filesystem commands, "plan" allows only read-only exploration, and "bypassPermissions" runs everything without asking (for CI and containers).
When a tool is denied, Claude receives a rejection message and typically attempts a different approach.
Parallel execution
When Claude requests multiple tool calls in a single turn, read-only tools (Read, Glob, Grep, MCP tools marked read-only) run concurrently. Tools that modify state (Edit, Write, Bash) run sequentially to avoid conflicts. Custom tools default to sequential; set readOnlyHint in their annotations to enable parallel execution.
Control surfaces
Four parameters shape how the loop runs. All are fields on ClaudeAgentOptions (Python) or Options (TypeScript).
| Parameter | What it controls | Default |
|---|---|---|
max_turns / maxTurns | Maximum tool-use round trips before stopping | No limit |
max_budget_usd / maxBudgetUsd | Maximum cost before stopping | No limit |
effort | Reasoning depth per turn ("low" to "max") | "high" (TS), unset (Python) |
model | Which Claude model to use | SDK default |
When a limit is hit, the ResultMessage carries an error subtype (error_max_turns or error_max_budget_usd). Without limits, the loop runs until Claude finishes on its own — fine for well-scoped tasks but potentially expensive for open-ended prompts. Setting a budget is a good default for production agents.
The effort parameter trades latency and token cost for reasoning depth within each response. Use "low" for routine tasks like file lookups and directory listings. Use "high" or "xhigh" for refactors and debugging. Effort is independent of extended thinking — the two can be combined or used separately.
The context window
The context window does not reset between turns. Everything accumulates: system prompt, tool definitions, conversation history, tool inputs, and tool outputs. Content that stays the same across turns (system prompt, CLAUDE.md, tool schemas) is automatically prompt-cached, reducing cost for repeated prefixes.
Large tool outputs are the fastest way to fill context. Reading a big file or running a verbose command can consume thousands of tokens in a single turn.
Automatic compaction
When the context window approaches its limit, the SDK automatically compacts the conversation — it summarizes older history to free space while keeping recent exchanges and key decisions intact. A SystemMessage with subtype "compact_boundary" fires when this happens.
Compaction replaces older messages with a summary, so specific instructions from early in the conversation may not survive. Persistent rules belong in CLAUDE.md (loaded via settingSources), which is re-injected on every request rather than carried through history.
Keeping context lean
Three strategies for long-running agents. Use subagents for subtasks — each starts with a fresh context, and only the summary returns to the parent. Scope subagent tools to the minimum set they need with the tools field on AgentDefinition. Use ToolSearch for MCP servers to load tools on demand instead of preloading all schemas.
Hooks as interception points
Hooks fire at specific points in the loop without consuming context. They run in the application process, not inside Claude’s context window.
| Hook | When it fires | Common uses |
|---|---|---|
PreToolUse | Before a tool executes | Validate inputs, block dangerous commands |
PostToolUse | After a tool returns | Audit outputs, trigger side effects |
UserPromptSubmit | When a prompt is sent | Inject additional context |
Stop | When the agent finishes | Validate result, save session state |
PreCompact | Before context compaction | Archive full transcript |
A PreToolUse hook that rejects a tool call prevents execution entirely — Claude receives the rejection as the tool result and adapts. Hooks can short-circuit the loop at every critical junction.
Handling the result
When the loop ends, the ResultMessage’s subtype field is the primary way to check termination state.
| Subtype | Meaning | result field |
|---|---|---|
success | Task completed normally | Yes |
error_max_turns | Hit turn limit before finishing | No |
error_max_budget_usd | Hit budget limit before finishing | No |
error_during_execution | API failure or cancelled request | No |
error_max_structured_output_retries | Structured output validation failed | No |
Always check the subtype before reading result. All subtypes carry total_cost_usd, usage, num_turns, and session_id for cost tracking and resumption. The stop_reason field indicates why the model stopped on its final turn — end_turn (normal), max_tokens (output limit), or refusal (declined request).
The session ID captured from ResultMessage enables resuming the full context later or forking into a different approach.
Takeaways
The loop is the engine
Every agent behavior — tool selection, parallel execution, context management — is a product of the five-step evaluate-execute-repeat cycle, not a black box.
Messages are the observability surface
Five typed messages (System, Assistant, User, StreamEvent, Result) give a structured window into what the agent is doing at each lifecycle stage.
Turns accumulate context
Context does not reset between turns. Long sessions with many tool calls build up history quickly; subagents and tool scoping keep the main thread lean.
Control surfaces prevent runaway agents
max_turns, max_budget_usd, and effort give hard stops. Without them, open-ended prompts can consume unbounded tokens.
Hooks are the interception layer
PreToolUse, PostToolUse, Stop, and PreCompact hooks fire outside the context window and can short-circuit the loop at every critical junction.