Chapter 12: Building Agents with Claude: SDK, Loops, and Hooks
Learning Objectives
Explain what the Claude Agent SDK provides and when to use it versus the raw Messages API or a third-party framework.
Describe the four-stage agent loop and the anatomy of a custom harness, including turns, message types, and iteration controls.
Compare self-hosted Agent SDK deployment with Anthropic-hosted Managed Agents, including state, compliance, and operational trade-offs.
Use hooks to insert deterministic actions into the agent loop, including PreToolUse decisions and their precedence.
Pre-Reading Check — The Claude Agent SDK
1. What is the single most important thing the Claude Agent SDK's query() function does for you?
2. When you spawn a subagent via the Agent tool, what does the parent agent receive?
3. In which situation is dropping down to the raw Messages API (instead of the Agent SDK) most justified?
4. How does the SDK authenticate to third-party model providers like Amazon Bedrock?
1. The Claude Agent SDK
Key Points
The Claude Agent SDK (renamed from the Claude Code SDK in late 2025) embeds "Claude Code as a library" in Python and TypeScript, giving you the same tools, agent loop, and context management.
Its main entry point, query(), is an async iterator that runs the entire tool-use loop for you—you write a prompt and options, not a loop.
It ships built-in tools (Read/Write/Edit, Glob/Grep, Bash, WebSearch/WebFetch, Agent/Skill), plus subagents, MCP, permissions, and sessions.
Subagents start with a fresh conversation, don't see the parent's turns, and return only their final response—a key context-management strategy.
Reach for the SDK for autonomous tool use; drop to the raw Messages API only when you need loop-level control the SDK doesn't expose.
An agent is a program that pursues a goal by repeatedly deciding what to do next, taking an action, observing the result, and deciding again—until the goal is met. A raw language model can only produce text; to make it act, someone must run the requested command, capture output, feed it back, and repeat. The Agent SDK is that "someone."
Built-in tools
Category
Tools
What they do
File operations
Read, Write, Edit
Read, create, and modify files
Search
Glob, Grep
Find files by pattern; regex-search file contents
Execution
Bash, Monitor
Run shell commands; watch a background script line by line
Web
WebSearch, WebFetch
Search the web; fetch a specific URL
Discovery
ToolSearch
Dynamically find and load tools on demand
Orchestration
Agent, Skill, AskUserQuestion, TaskCreate
Spawn subagents, invoke skills, ask the user, manage tasks
Beyond tools, the SDK provides subagents (fresh conversation, returns only its final response), MCP integration (tools named mcp__<server>__<action>), permissions (allowed_tools, disallowed_tools, permission_mode), and sessions (capture a session_id to resume, or fork to branch).
Building an agent
A minimal bug-fixing agent in Python demonstrates that you do not write the loop—you observe an agent that already reads, hypothesizes, edits, and verifies on its own:
from claude_agent_sdk import query, ClaudeAgentOptions
async for message in query(
prompt="Find and fix the bug in auth.py",
options=ClaudeAgentOptions(allowed_tools=["Read", "Edit", "Bash"]),
):
print(message)
Python also offers ClaudeSDKClient for long-lived conversational agents. Install via pip install claude-agent-sdk (Python 3.10+) or npm install @anthropic-ai/claude-agent-sdk (Node 18+), and set ANTHROPIC_API_KEY. Third-party providers use env vars like CLAUDE_CODE_USE_BEDROCK=1 or CLAUDE_CODE_USE_VERTEX=1.
SDK vs. framework vs. raw API
The raw Messages API gives direct model access—you implement the tool loop yourself (maximum control, maximum work). The Agent SDK runs the loop for you with production-grade tools. Third-party frameworks wrap multiple providers behind their own abstractions but add a layer that rarely matches Claude's tuned harness.
[Animation slot: the SDK "running the loop for you" — a prompt entering a black box that auto-cycles read → edit → run, versus the raw API where you wire each step by hand]
Post-Reading Check — The Claude Agent SDK
1. What is the single most important thing the Claude Agent SDK's query() function does for you?
2. When you spawn a subagent via the Agent tool, what does the parent agent receive?
3. In which situation is dropping down to the raw Messages API (instead of the Agent SDK) most justified?
4. How does the SDK authenticate to third-party model providers like Amazon Bedrock?
Pre-Reading Check — Custom Agent Loops and Harnesses
1. Which stage of the four-stage agent loop is described as "what separates a reliable agent from a plausible-sounding one"?
2. In the SDK execution loop, when does the loop stop?
3. What best defines a "turn" in the agent loop?
4. Which tools can the harness safely run concurrently?
5. Why should persistent rules live in CLAUDE.md rather than buried in the initial prompt?
2. Custom Agent Loops and Harnesses
Key Points
Anthropic's guiding principle is to "give Claude a computer"; the agent operates in a four-stage loop: gather context → take action → verify work → repeat.
The verify step separates a reliable agent from a plausible-sounding one—agents that check their own output catch mistakes before they compound.
A harness is the code around the model that implements the loop; the SDK is a harness, and a custom loop on the raw Messages API is a hand-built one.
A turn is one round trip of model-output-then-tool-execution; the loop ends when Claude returns no tool calls. Govern it with max_turns, max_budget_usd, and effort.
Context accumulates across turns—stable content is prompt-cached, and the SDK auto-compacts near the limit. Read-only tools run in parallel; state-modifying tools run sequentially.
The anatomy of an agent loop
The four stages: (1) Gather context—use grep/tail to load only relevant portions; subagents parallelize gathering and compaction summarizes old messages. (2) Take action—tools, bash, generated code, MCP. (3) Verify work—rules-based checks, screenshots, or a model-as-evaluator for fuzzy criteria. (4) Repeat until done.
flowchart TD
A["Gather context: grep, tail, semantic search"] --> B["Take action: tools, bash, code, MCP"]
B --> C["Verify work: rules, screenshots, model-as-evaluator"]
C --> D{"Goal met?"}
D -->|"No — repeat"| A
D -->|"Yes"| E["Done"]
H1["Subagents (isolated windows)"] -.-> A
H2["Compaction (summarize old messages)"] -.-> A
Building a custom harness
When you call query(), the SDK runs this loop: (1) receive prompt (yields SystemMessage subtype "init"); (2) evaluate and respond (yields AssistantMessage); (3) execute tools—hooks can intercept, modify, or block calls here; (4) repeat (each cycle is one turn); (5) return a ResultMessage with final text, usage, cost, and session ID. The loop continues until Claude produces a response with no tool calls.
Figure 12.2: One turn of the SDK execution loop as a sequence
sequenceDiagram
participant App as Your code
participant SDK as Harness (SDK)
participant Claude as Claude model
participant Tools as Tools
App->>SDK: query(prompt, options)
SDK->>Claude: prompt + system + tools + history
Note over SDK,Claude: yields SystemMessage init
Claude-->>SDK: AssistantMessage (text and/or tool calls)
alt tool calls requested
SDK->>Tools: execute each requested tool
Tools-->>SDK: results
SDK->>Claude: UserMessage (tool results)
Note over SDK,Claude: repeat — each cycle is one turn
else no tool calls
SDK-->>App: ResultMessage (text, usage, cost, session_id)
end
If you wrote the harness yourself against the raw Messages API, it is the classic loop—you write run_tool, append the assistant turn, and decide the stop condition:
messages = [{"role": "user", "content": task}]
while True:
response = client.messages.create(model="claude-opus-4-8",
max_tokens=16000, tools=tools, messages=messages)
if response.stop_reason == "end_turn":
break # no tool calls -> done
messages.append({"role": "assistant", "content": response.content})
tool_results = [run_tool(b) for b in response.content if b.type == "tool_use"]
messages.append({"role": "user", "content": tool_results})
Managing tool calls and iteration
max_turns caps round trips (yields error_max_turns); max_budget_usd caps spend (error_max_budget_usd); effort (low…max) tunes reasoning depth. Two mechanics matter: context accumulates (stable content is prompt-cached; the SDK auto-compacts near the limit, emitting a compact_boundary), and parallel execution—read-only tools run concurrently while state-modifying tools run sequentially.
[Animation slot: turns accumulating in a growing context window, with prompt-caching highlighting stable blocks and a compaction event collapsing old history when the limit nears]
Post-Reading Check — Custom Agent Loops and Harnesses
1. Which stage of the four-stage agent loop is described as "what separates a reliable agent from a plausible-sounding one"?
2. In the SDK execution loop, when does the loop stop?
3. What best defines a "turn" in the agent loop?
4. Which tools can the harness safely run concurrently?
5. Why should persistent rules live in CLAUDE.md rather than buried in the initial prompt?
Pre-Reading Check — Deployment Models
1. When you self-host the Agent SDK, what does query() spawn under the hood?
2. Why is Anthropic-hosted Managed Agents not eligible for Zero Data Retention or a HIPAA BAA?
3. In Managed Agents, where do model, system, and tools live?
4. With self-hosted Agent SDK, what happens to session transcripts and working-directory artifacts on restart or scale-down?
5. What does the "self-hosted sandboxes with Managed Agents" middle path give you?
3. Deployment Models
Key Points
Two broad models: self-host the Agent SDK (your process/infrastructure) or use Anthropic-hosted Managed Agents (hosted REST API where Anthropic runs the agent and sandbox).
Self-hosting spawns a stateful claude CLI subprocess per session; local JSONL state is lost on restart unless mirrored to a SessionStore (S3/Redis/Postgres).
Managed Agents is organized around Agent, Environment, Session, Events; model/system/tools live on the agent (created once, referenced by ID).
Managed Agents is beta (header managed-agents-2026-04-01) and stateful by design—therefore not eligible for ZDR or a HIPAA BAA.
Common path: prototype with the SDK, productionize on Managed Agents. Anthropic token cost typically dominates container cost by an order of magnitude.
Self-hosted agents
Self-hosting: query() spawns a claude CLI subprocess (one session = one subprocess) owning a shell, cwd, and JSONL transcripts on local disk—all lost on restart unless mirrored. Use a SessionStore adapter to mirror transcripts to durable storage (but it mirrors transcripts only, not memory files). Session patterns include ephemeral, long-running, hybrid, and multi-agent container. Budget roughly 1 GiB RAM, 5 GiB disk, and 1 CPU per agent as a floor.
Anthropic-hosted managed agents
Managed Agents is a hosted REST API and pre-built harness where Anthropic runs the agent and sandbox; you send events and stream results back via SSE. Best for long-running, asynchronous work. A structural rule and common exam trap: model, system, and tools live on the agent, never on the session. It is beta (header managed-agents-2026-04-01) and stateful by design—therefore not eligible for ZDR or a HIPAA BAA. There is also a middle path—self-hosted sandboxes with Managed Agents—where tool execution stays on your host while inputs/outputs flow to Anthropic's control plane where Claude runs.
Choosing a deployment model
Self-hosted Agent SDK
Anthropic-hosted Managed Agents
Runs in
Your process, your infrastructure
Anthropic-managed infrastructure
Interface
Python or TypeScript library
REST API
Agent works on
Files on your infrastructure
A managed sandbox per session
Session state
JSONL on your filesystem (lost on restart unless mirrored)
Anthropic-hosted event log (durable)
Custom tools
In-process Python/TS functions
Claude triggers; you execute and return results
Compliance
You control retention; ZDR/HIPAA possible in your infra
Stateful—not eligible for ZDR or HIPAA BAA
Operational burden
You run sandboxes, session storage, scaling
Anthropic runs sandbox and session infra
Best for
Local prototyping, direct filesystem/service access
Production without operating sandbox/session infra; long-running, async
Figure 12.3: Self-hosted Agent SDK vs. Anthropic-hosted Managed Agents architecture
flowchart LR
subgraph Self["Self-hosted Agent SDK (your infrastructure)"]
direction TB
S1["Your process: query()"] --> S2["claude CLI subprocess (1 per session)"]
S2 --> S3["Local JSONL state, cwd, CLAUDE.md"]
S3 -.->|"mirror (optional)"| S4["SessionStore: S3 / Redis / Postgres"]
end
subgraph Managed["Anthropic-hosted Managed Agents (Anthropic infra)"]
direction TB
M1["Your app: REST API + SSE"] --> M2["Managed harness + sandbox"]
M2 --> M3["Durable server-side event log"]
end
S2 --> API["Anthropic model API"]
M2 --> API
[Animation slot: side-by-side "your office" (self-hosted desk, cabinets, cleanup) vs. "serviced building" (managed desk that logs everything, hence not certified for the most sensitive documents)]
Post-Reading Check — Deployment Models
1. When you self-host the Agent SDK, what does query() spawn under the hood?
2. Why is Anthropic-hosted Managed Agents not eligible for Zero Data Retention or a HIPAA BAA?
3. In Managed Agents, where do model, system, and tools live?
4. With self-hosted Agent SDK, what happens to session transcripts and working-directory artifacts on restart or scale-down?
5. What does the "self-hosted sandboxes with Managed Agents" middle path give you?
Pre-Reading Check — Hooks for Deterministic Actions
1. What makes a hook a source of determinism that a system-prompt instruction is not?
2. Which hook event can block or modify a tool call before it executes?
3. When multiple hooks match one event, what is the precedence of their combined decisions?
4. For a tool hook, what does the matcher pattern match against?
5. What important limitation applies when an agent hits max_turns?
6. Where do hooks run, and what is the consequence for context?
4. Hooks for Deterministic Actions
Key Points
Hooks are callbacks the harness runs at fixed loop points in your process, not the context window—so they consume no context and cannot be talked out of firing.
A deterministic action is guaranteed at a fixed loop point regardless of sampling; a PreToolUsedeny blocks every matching call, unlike a spoofable prompt instruction.
A tool matcher matches the tool name only; path checks happen inside your callback. Precedence when hooks combine is deny > defer > ask > allow.
Limitation: hooks may not fire on a max_turns exit—don't make correctness depend solely on a Stop hook.
What hooks are and when they fire
Everything before hooks is probabilistic—the model decides which tools to call. Some rules ("never write to .env," "log every command," "require approval to deploy") need determinism. Hooks form a deterministic control layer: the harness runs them at fixed points independent of sampling, in your process, so they consume no context and cannot be overridden. A PreToolUse hook returning deny is the badge reader on the server-room door; a prompt instruction is a sign asking people not to enter.
Figure 12.4: Where PreToolUse and PostToolUse fire around a tool call
sequenceDiagram
participant Claude as Claude model
participant Harness as Harness
participant Pre as PreToolUse hook
participant Tool as Tool
participant Post as PostToolUse hook
Claude->>Harness: request tool call
Harness->>Pre: fire PreToolUse (tool_name, tool_input)
Pre-->>Harness: allow / deny / ask / defer (+ updatedInput)
alt allowed
Harness->>Tool: execute tool call
Tool-->>Harness: result
Harness->>Post: fire PostToolUse (result)
Post-->>Harness: additionalContext / updatedToolOutput
Harness->>Claude: feed result back
else denied
Harness-->>Claude: blocked (tool not executed)
end
Key hook events: PreToolUse (before execution; can block/modify), PostToolUse (after return; cannot undo, but adds context or side effects like audit trails), PostToolUseFailure, UserPromptSubmit, Stop (save state on exit), SubagentStart/SubagentStop, PreCompact (archive transcript; trigger is manual or auto), PermissionRequest, and Notification.
Inserting deterministic behavior
Register hooks via the hooks option; keys are event names, values are arrays of matchers. A matcher filters when callbacks fire and matches the tool name only—path checks go inside the callback. The return value controls the operation: for PreToolUse, set permissionDecision (allow/deny/ask/defer) plus optional updatedInput; for PostToolUse, set additionalContext or updatedToolOutput. The canonical guardrail:
def protect_env_files(input_data, tool_use_id, context):
path = input_data["tool_input"].get("file_path", "")
if path.endswith(".env") or path.startswith("/etc/"):
return {"hookSpecificOutput": {
"permissionDecision": "deny",
"permissionDecisionReason": "Writes to .env and /etc are blocked."}}
return {} # allow everything else unchanged
When multiple hooks match one event they all run in parallel with non-deterministic order, so each must act independently. Outputs combine by precedence deny > defer > ask > allow—a single deny blocks the operation.
flowchart TD
A["Combine all matching hook outputs"] --> B{"Any hook returned deny?"}
B -->|"Yes"| Deny["Block the tool call"]
B -->|"No"| C{"Any hook returned defer?"}
C -->|"Yes"| Defer["Defer to normal permission flow"]
C -->|"No"| D{"Any hook returned ask?"}
D -->|"Yes"| Ask["Prompt human for approval"]
D -->|"No"| Allow["Allow the tool call"]
Combining hooks with the agent loop
Hooks are wired into the loop from Section 2: they sit at step 3 ("execute tools"), where PreToolUse and PostToolUse fire on every matching call. The model chooses whether to call a tool; the harness chooses—every time—whether that call is allowed, modified, or blocked. Common patterns: guardrails (PreToolUse deny/rewrite), audit and observability (PostToolUse logging, Notification to Slack, Stop persisting state), human-in-the-loop (PreToolUse ask), and transcript preservation (PreCompact). One limitation: hooks may not fire on a max_turns exit because the session ends first, so don't make correctness depend solely on a Stop hook.
[Animation slot: the four-stage loop with hooks drawn in—control detouring through PreToolUse before each tool, PostToolUse after, and Stop when the loop ends; guardrails on a road the model drives]
Post-Reading Check — Hooks for Deterministic Actions
1. What makes a hook a source of determinism that a system-prompt instruction is not?
2. Which hook event can block or modify a tool call before it executes?
3. When multiple hooks match one event, what is the precedence of their combined decisions?
4. For a tool hook, what does the matcher pattern match against?
5. What important limitation applies when an agent hits max_turns?
6. Where do hooks run, and what is the consequence for context?