Exam Overview — Claude Certified Architect – Foundations (CCAR-F)
Everything on this page comes from the official Exam Guide v1.0, effective July 2026. Where I add interpretation or outside material I say so explicitly — look for (not in the guide).
The facts you actually need
|
|
| Credential |
Claude Certified Architect – Foundations |
| Exam code |
CCAR-F (community shorthand CCA-F — not in the guide) |
| Items |
60 |
| Item format |
Multiple-choice and multiple-response — each item states how many responses to select |
| Structure |
4 scenarios drawn from a bank of 6 |
| Time |
120 minutes |
| Delivery |
Proctored — online proctored and/or Pearson VUE test center |
| Passing score |
720 on a scaled range of 100–1,000 |
| Fee |
$125 USD per attempt |
| Validity |
12 months from award |
| Result reporting |
Pass/fail with scaled score, plus percent-correct by domain (diagnostic only — the pass decision is on total scaled score) |
Scoring is criterion-referenced: you are measured against a fixed standard set by a formal standard-setting study, not against other candidates. 720 is a scaled score, not 72% correct — do not try to compute "how many can I miss."
Version note
The v0.2 draft (June 2026) and v1.0 (July 2026) have identical domains, all 30 task statements, and all 12 sample questions. Only the framing changed. Two deltas worth knowing:
- v0.2 said "All questions are multiple choice… one correct response and three incorrect." v1.0 says multiple-choice and multiple-response. Expect at least some "select two" items. Read the instruction line on every question.
- v1.0 adds the policy sections (retakes, ID, NDA, appeals, recertification) that v0.2 lacked. It also introduces the exam code CCAR-F and adds percent-correct by domain to the score report; v0.2 reported pass/fail only.
Blueprint — memorize this table
| # |
Domain |
Weight |
≈ items of 60 |
| 1 |
Agentic Architecture & Orchestration |
27% |
~16 |
| 2 |
Tool Design & MCP Integration |
18% |
~11 |
| 3 |
Claude Code Configuration & Workflows |
20% |
~12 |
| 4 |
Prompt Engineering & Structured Output |
20% |
~12 |
| 5 |
Context Management & Reliability |
15% |
~9 |
Domain 1 is the single largest block and carries seven task statements. Domains 1 + 3 + 4 are two-thirds of the exam.
You cannot choose which four you get, so study all six. Each is a paragraph of production context that frames a run of questions.
1. Customer Support Resolution Agent. A support agent built with the Claude Agent SDK handling high-ambiguity requests (returns, billing disputes, account issues) through custom MCP tools: get_customer, lookup_order, process_refund, escalate_to_human. Target is 80%+ first-contact resolution while knowing when to escalate.
Primary domains: 1, 2, 5.
2. Code Generation with Claude Code. A team using Claude Code for generation, refactoring, debugging and documentation, integrating it with custom slash commands and CLAUDE.md, and deciding plan mode vs direct execution.
Primary domains: 3, 5.
3. Multi-Agent Research System. A coordinator delegating to four specialized subagents — web search, document analysis, synthesis, report generation — producing comprehensive cited reports.
Primary domains: 1, 2, 5.
4. Developer Productivity with Claude. An Agent SDK agent helping engineers explore unfamiliar codebases, understand legacy systems, generate boilerplate and automate repetitive tasks, using built-in tools (Read, Write, Bash, Grep, Glob) plus MCP servers.
Primary domains: 2, 3, 1.
5. Claude Code for Continuous Integration. Claude Code in a CI/CD pipeline running automated code reviews, generating test cases and giving PR feedback, with a stated emphasis on actionable feedback and minimizing false positives.
Primary domains: 3, 4.
6. Structured Data Extraction. Extracting information from unstructured documents, validating output with JSON schemas, maintaining high accuracy, handling edge cases gracefully, integrating downstream.
Primary domains: 4, 5.
A useful way to read this: the six scenarios are six costumes over the same five domains. The same mechanic — say, "deterministic enforcement beats a prompt instruction" — shows up as a refund gate in scenario 1 and as a CI gate in scenario 5.
Out of scope — do not study these
The guide publishes an explicit exclusion list, and it is generous. Anything below will not be tested, so ignore it even though it appears all over Anthropic's docs and most community study guides:
Fine-tuning or training custom models · Claude API authentication, billing, or account management · language- and framework-specific implementation detail beyond what tool and schema configuration needs · deploying or hosting MCP servers (infrastructure, networking, containers) · Claude's internal architecture, training, or weights · Constitutional AI, RLHF, safety training · embedding models and vector database internals · computer use (browser/desktop automation) · vision and image analysis · streaming API implementation and server-sent events · rate limiting, quotas, pricing calculations · prompt caching implementation details beyond knowing it exists · token counting algorithms and tokenization · OAuth, API key rotation, authentication protocols · specific cloud provider configuration (AWS, GCP, Azure) · performance benchmarking and model comparison metrics.
This is the highest-leverage page in the guide. A lot of community prep material drills prompt-cache TTLs, 429 handling, pause_turn, and MCP transport specs. None of that is on this exam.
In-scope topic list (the guide's own summary)
Agentic loop implementation (control flow on stop_reason, tool result handling, termination) · multi-agent orchestration (coordinator-subagent, decomposition, parallel execution, iterative refinement) · subagent context management (explicit passing, structured state persistence, crash recovery via manifests) · tool interface design (descriptions, splitting vs consolidating, naming) · MCP tool and resource design · MCP server configuration (project vs user scope, env var expansion, multi-server access) · error handling and propagation (structured responses, transient vs business vs permission, local recovery before escalation) · escalation decision-making · CLAUDE.md configuration (hierarchy, @import, .claude/rules/ glob patterns) · custom commands and skills (scope, context: fork, allowed-tools, argument-hint) · plan mode vs direct execution · iterative refinement (I/O examples, test-driven iteration, interview pattern) · structured output via tool_use (schema design, tool_choice, nullable fields) · few-shot prompting · batch processing · context window optimization · human review workflows (confidence calibration, stratified sampling, accuracy segmentation) · information provenance.
Appendix: technologies named in the guide
The guide lists the concrete surface area it may reference. Recognize each of these by name:
- Claude Agent SDK — agent definitions, agentic loops,
stop_reason handling, hooks (PostToolUse, tool call interception), subagent spawning via the Task tool, allowedTools configuration
- MCP — servers, tools, resources, the
isError flag, tool descriptions, tool distribution, .mcp.json, environment variable expansion
- Claude Code — CLAUDE.md hierarchy (user/project/directory),
.claude/rules/ with YAML frontmatter path scoping, .claude/commands/, .claude/skills/ with SKILL.md frontmatter (context: fork, allowed-tools, argument-hint), plan mode, direct execution, /memory, /compact, --resume, fork_session, the Explore subagent
- Claude Code CLI —
-p / --print, --output-format json, --json-schema
- Claude API —
tool_use with JSON schemas, tool_choice (auto, any, forced), stop_reason values (tool_use, end_turn), max_tokens, system prompts
- Message Batches API — 50% cost savings, up to 24-hour window,
custom_id, polling, no multi-turn tool calling
- JSON Schema — required vs optional, enums, nullable,
"other" + detail string, strict mode
- Pydantic — schema validation, semantic validation errors, validation-retry loops
- Built-in tools — Read, Write, Edit, Bash, Grep, Glob
- Few-shot prompting, prompt chaining, context window management, session management, confidence scoring
(Not in the guide: this appendix is the guide's own list, not an exhaustive snapshot of current API/product surface — domains 01 and 02 flag specific spots where current capability has moved past what's named here, e.g. the Task tool's current SDK name, additional stop_reason/tool_choice values, and a third MCP scope.)
Intended audience
The guide describes the ideal candidate as a solution architect with 6+ months hands-on experience across the Claude API, Agent SDK, Claude Code and MCP. This is a description, not a gate — the guide sets out no prerequisites (inference, not stated).
Logistics and policies
Registration runs through the Anthropic Partner Academy; you then create a Pearson VUE account and schedule. The fee at checkout reflects any discount that applies to your partner tier. The guide states no eligibility restriction.
Cancel or reschedule up to 24 hours before your appointment. Inside 24 hours, or a no-show, forfeits the fee.
ID. Valid, unexpired, government-issued photo ID. The name on it must match your registration exactly — name corrections go to [email protected] before scheduling.
Retakes. 14 days after a first fail, 30 days after a second, 90 days after a third. Maximum four attempts per rolling twelve months, per exam. Full fee each time.
Conduct. Stay in view of the webcam if testing online; workspace clear of notes, books, phones, secondary monitors; no communication with anyone; no capturing exam content. Prohibited: phones, smart watches, headphones, study materials, recording devices. Scratch paper only if the proctor provides it.
NDA. You accept a confidentiality agreement before the exam starts. Decline and the session ends with no refund.
Recertification. On-time renewal is a free, non-proctored assessment on the Partner Academy. Let it lapse and you retake the full exam at full price.
Appeals within 14 days, via Pearson VUE support. The standard-setting outcome and individual item content are not appealable.
Accommodations must be requested and approved by Pearson VUE before you schedule.
Three-Day Study Plan
Built for someone who already works with Claude Code daily and needs to convert that fluency into exam-shaped knowledge — including the API and SDK layer underneath it, which the exam tests and daily Claude Code use never exposes you to. Roughly 5–6 hours a day; compress to the starred items if you have less.
The guide describes this as a test of practical judgment about architecture, configuration, and tradeoffs, not recall. So the plan front-loads the two things that actually move your score: the task statements (which tell you what's testable) and the answer explanations (which tell you how the exam reasons).
Day 1 — Map the territory, then own Domain 1
Morning (~2.5h)
★ Read Exam Overview end to end. Pay real attention to two sections: the six scenarios and the out-of-scope list. The out-of-scope list is worth ten minutes of study on its own — it deletes maybe a third of what community prep material drills, and knowing something is out of scope lets you eliminate distractors on sight.
★ Read §6 of the official PDF — the full Detailed Objectives by Domain — once, straight through, without stopping to study. You want the shape of all 30 task statements in your head before you go deep on any one.
Afternoon (~2.5h)
★ Work through Domain 1 properly. It is 27% of the exam and seven task statements. Do not skim it because agentic loops feel familiar — the exam tests the anti-patterns as hard as the patterns.
Then do practice questions 1–11 (Practice Questions). Mark anything you got right for the wrong reason; that counts as a miss.
Evening (~45min)
Read the four Preparation Exercises in §8 of the PDF. You will not have time to build them, but reading them tells you what the item writers had in mind. Exercise 1 (multi-tool agent with escalation) and Exercise 4 (multi-agent research pipeline) map almost one-to-one onto scenarios 1 and 3.
End of day, you should be able to say without looking: the five domains and their weights, the six scenarios, what terminates an agentic loop, why subagents get nothing for free, and when a hook beats a prompt.
Day 2 — Domains 2, 3 and 4
Morning (~2h)
★ Domain 2 — Tool Design & MCP, then questions 12–18.
The two ideas to lock in: descriptions are the primary selection mechanism (and usually the correct first fix), and the four error categories with only transient being retryable.
★ Domain 3 — Claude Code, then questions 19–27.
Domain 3 is the most memorizable domain on the exam — it's largely "which file, which directory, which flag." Build the file map from the Quick Reference from memory on paper. If you can reproduce it cold, Domain 3 is 12 items you will not lose.
Afternoon (~2.5h)
★ Domain 4 — Prompt Engineering & Structured Output, then questions 28–36.
Spend extra time on one distinction: syntax versus semantics. Schemas fix malformed JSON. They do not fix JSON that is well-formed and wrong. Every question describing values that don't add up, fields swapped, or fabricated data is testing whether you reach for a validation layer instead of a stricter schema.
Also lock the Batch API facts as a block: 50% · ≤24h · no SLA · no multi-turn tool calling · custom_id. And the rule that it is a latency decision, not a cost decision.
Evening (~1h)
★ Do the twelve official sample questions in §9 of the PDF. Read every answer explanation twice, including for the ones you got right. These twelve explanations are the most reliable signal available about how the exam weighs options — particularly the phrases "over-engineered," "most effective first step," "addresses tool availability rather than tool ordering," and "probabilistic LLM compliance."
Day 3 — Domain 5, synthesis, and consolidation
Morning (~2h)
★ Domain 5 — Context Management & Reliability, then questions 37–40 and the five additional items 41–45.
Domain 5 is only 15% but it's a primary domain on four of the six scenarios, so it leaks into everything. The three highest-value blocks: the three valid escalation triggers, the four-part structured error payload, and the provenance checklist.
Midday (~1.5h) — the scenario pass
For each of the six scenarios, write yourself half a page from memory: what is it, which domains does the guide list as primary, and what are the three most likely things to go wrong in it. Then check against Exam Overview.
This is the highest-leverage exercise on day 3. Every question on your form arrives wrapped in one of these six, and knowing the scenario's failure modes before you read the stem is most of the work.
Afternoon (~1.5h) — weak-spot repair
Go back to whichever domain page you scored worst on. Re-read only its decision table and ten things to have cold. Then re-attempt the questions you missed, out of order.
If you have energy left, re-read the task statement titles only — all 30 — and for each one say out loud what it's about. Any title that draws a blank is a gap; go read that section.
Evening (~45min)
★ Read the Quick Reference once. Then stop. Do not cram new material the night before; you will displace things you already know.
Confirm the logistics: ID name matches your registration exactly, Pearson VUE system test done if you're testing online, room clear, 30 minutes of buffer before your slot.
Exam-day tactics
Budget. 60 items in 120 minutes is two minutes each. Most items are answerable in under a minute once you recognize the pattern; bank that time for the four or five that are genuinely close.
Read the instruction line. v1.0 says multiple-choice and multiple-response. Each item states how many to select. Missing "select two" is an avoidable loss.
Find the binding constraint. Almost every stem contains one phrase that decides the answer — must never, before any, every time, available to every developer, developers wait for, regardless of directory, the same session. Locate it before you read the options.
Eliminate by principle, not by plausibility. Three eliminations usually work:
- Anything that answers a guarantee requirement with a prompt instruction.
- Anything that adds infrastructure (a classifier, a trained model, a routing layer, a bigger model, a bigger context window) when a description, a criterion, or a schema field would do.
- Anything that pushes the work onto a human or the customer when the system was supposed to handle it.
When two options both look right, ask which one addresses the root cause named in the stem. The guide's explanations repeatedly reject correct-but-adjacent fixes on exactly that basis.
Don't leave anything blank. There is no penalty for guessing, and v0.2 noted the platform requires an answer before advancing anyway.
If you only have one day
Overview (out-of-scope list especially) → Domain 1 → Domain 3 → the twelve official sample questions with explanations → Quick Reference. That's 47% of the blueprint covered deeply plus the exam's own calibration signal.
Domain 1 — Agentic Architecture & Orchestration (27%, ~16 items)
The heaviest domain, seven task statements, and the one where the questions are diagnostic rather than factual: you get a broken production system and pick the fix. The recurring theme across all seven is deterministic mechanisms beat probabilistic instructions when the consequence is real, balanced against don't add machinery the failure doesn't require.
In the book: Chapter 2 — The Agentic Loop (1.1) · Chapter 5 — Orchestration (1.2, 1.3) · Chapter 6 — Hooks & Gates (1.4, 1.5) · Chapter 1 — Agents & Workflows (1.6) · Chapter 7 — Sessions (1.7)
1.1 Design and implement agentic loops
The loop
1. POST /v1/messages → system, messages[], tools[]
2. Read response.stop_reason
3. If "tool_use":
append the assistant message (with its tool_use blocks) to messages
execute each requested tool
append a USER message containing tool_result block(s)
go to 1
4. If "end_turn": done — present the final response
Three facts the guide states directly:
- The loop is driven by inspecting
stop_reason: "tool_use" means continue, "end_turn" means terminate.
- Tool results are appended to conversation history so the model can reason about the next action. The history is how the agent accumulates knowledge.
- There is a real distinction between model-driven decision-making — Claude reasons about which tool to call next from context — and pre-configured decision trees or fixed tool sequences. Agentic design means the former.
The three named anti-patterns
The guide calls these out explicitly, so they are almost certainly distractors somewhere on your form:
- Parsing natural language signals to determine loop termination. Checking whether the text contains "done" or "I've finished."
- Setting arbitrary iteration caps as the primary stopping mechanism. A cap is a safety net against runaway loops; it is not how you know the task is complete.
- Checking for assistant text content as a completion indicator. A response with
stop_reason: "tool_use" can also contain text — Claude often narrates before calling a tool.
Answer heuristic. Any option that determines completion by anything other than stop_reason is wrong.
(Not in the guide: current-generation models add a third stop_reason, "model_context_window_exceeded" — a distinct signal for "the response hit the context window mid-generation," separate from both tool_use and the guide's binary framing. The guide's two-value model is what's tested.)
1.2 Orchestrate multi-agent systems with coordinator-subagent patterns
Hub and spoke
A coordinator agent manages all inter-subagent communication, error handling, and information routing. Subagents do not talk to each other. Routing everything through the coordinator buys three things the guide names: observability, consistent error handling, and controlled information flow.
The coordinator's job is task decomposition, delegation, result aggregation, and deciding which subagents to invoke based on query complexity — not mechanically running the full pipeline every time.
Subagents operate with isolated context. They do not inherit the coordinator's conversation history automatically. This single fact underpins several questions across domains 1 and 5.
The named failure mode: overly narrow decomposition
The guide flags "risks of overly narrow task decomposition by the coordinator, leading to incomplete coverage of broad research topics." Sample question 7 is exactly this: every subagent succeeds, the report still misses three quarters of the topic, and the coordinator's log shows it split "creative industries" into three visual-arts subtasks. When all downstream agents execute correctly but coverage is wrong, the root cause is upstream decomposition.
Skills tested
- Coordinators that analyze query requirements and dynamically select subagents rather than always routing through the full pipeline.
- Partitioning scope to minimize duplication — assign distinct subtopics or distinct source types per agent.
- Iterative refinement loops: coordinator evaluates synthesis output for gaps → re-delegates to search and analysis with targeted queries → re-invokes synthesis → repeat until coverage is sufficient.
- Routing all subagent communication through the coordinator.
1.3 Configure subagent invocation, context passing, and spawning
| Concept |
Detail |
| Spawning mechanism |
The Task tool |
| Requirement |
allowedTools must include "Task" for a coordinator to invoke subagents |
| Context |
Must be explicitly provided in the prompt. Subagents do not inherit parent context or share memory between invocations |
| Configuration |
AgentDefinition — descriptions, system prompts, tool restrictions per subagent type |
| Branching |
Fork-based session management for exploring divergent approaches from a shared analysis baseline |
(Not in the guide: the current Claude Agent SDK names this tool Agent, not Task — Task is retained as a legacy alias. The guide and this exam's v1.0 material say Task throughout, so know both names, but answer with Task on the form.)
(Not in the guide: a subagent's session_id/agentId can be captured from its result and passed back via resume: sessionId to continue that specific subagent's own transcript directly — a current-API alternative to re-pasting all prior findings into a fresh prompt when re-delegating for the iterative-refinement pattern in 1.2/1.6. The guide's stated mechanism is still "provide context explicitly in the prompt.")
Skills tested
- Include complete findings from prior agents directly in the subagent's prompt. If synthesis needs the web search results and the document analysis output, you paste them in. "Analyze the findings" with no findings attached is the classic wrong answer.
- Use structured data formats that separate content from metadata — source URLs, document names, page numbers — when passing context between agents, so attribution survives the handoff.
- Spawn parallel subagents by emitting multiple Task tool calls in a single coordinator response, not across separate turns. Multiple calls in one turn is parallel; one call per turn is sequential.
- Write coordinator prompts that specify research goals and quality criteria rather than step-by-step procedural instructions, so subagents can adapt.
If a question asks "why doesn't my coordinator spawn subagents?" — check allowedTools for "Task".
If it asks "why does the synthesis agent produce generic output?" — the findings weren't in its prompt.
If it asks "how do I parallelize?" — multiple Task calls in one response.
1.4 Multi-step workflows with enforcement and handoff
Programmatic enforcement vs prompt guidance
This is the highest-yield idea in the whole exam. The guide's own wording: "When deterministic compliance is required (e.g., identity verification before financial operations), prompt instructions alone have a non-zero failure rate."
Prerequisite gates block downstream tool calls until prerequisites complete. The canonical example, verbatim from the 1.4 skills bullet: block process_refund until get_customer has returned a verified customer ID. (Sample question 1 is the same idea, with option A gating both lookup_order and process_refund.)
Sample question 1's answer explanation is worth internalizing: enhancing the system prompt (B) and adding few-shot examples (C) both "rely on probabilistic LLM compliance, which is insufficient when errors have financial consequences." A routing classifier (D) "addresses tool availability rather than tool ordering, which is not the actual problem."
Structured handoff protocols
When escalating mid-process to a human who lacks access to the conversation transcript, compile a structured summary: customer ID, root cause, refund amount, recommended action. Not a raw transcript dump.
Multi-concern decomposition
Decompose multi-concern customer requests into distinct items, investigate each in parallel using shared context, then synthesize a unified resolution. Not: handle the first issue and ask them to open a second ticket.
1.5 Agent SDK hooks for interception and normalization
Two hook uses are named:
| Purpose |
Hook |
Example from the guide |
| Transform tool results before the model sees them |
PostToolUse |
Normalize heterogeneous formats — Unix timestamps, ISO 8601, numeric status codes — coming from different MCP tools |
| Block policy-violating outgoing tool calls |
Tool call interception (pre-execution) |
Block refunds exceeding $500, redirect to human escalation |
And the framing that ties it back to 1.4: "the distinction between using hooks for deterministic guarantees versus relying on prompt instructions for probabilistic compliance." Choose hooks when business rules require guaranteed compliance.
Direction matters and is an easy distractor: PostToolUse acts on results coming back; interception acts on calls going out. You cannot block a refund with a PostToolUse hook — by then the money moved.
The counterweight, which the exam also tests: don't reach for a hook for a soft preference. Match the enforcement strength to the consequence.
1.6 Task decomposition strategies
The core choice:
| Pattern |
Use when |
Guide's example |
| Fixed sequential pipeline (prompt chaining) |
The steps are predictable and multi-aspect |
Analyze each file individually, then run a cross-file integration pass |
| Dynamic adaptive decomposition |
Open-ended investigation where subtasks depend on what you find |
"Add comprehensive tests to a legacy codebase" |
Skills tested
- Prompt chaining for predictable multi-aspect reviews; dynamic decomposition for open-ended investigation.
- Split large code reviews into per-file local analysis passes plus a separate cross-file integration pass — the guide's stated reason is avoiding attention dilution. Sample question 12 is this exact fix, and its explanation kills two tempting alternatives: a bigger context window "doesn't solve attention quality issues," and requiring 2-of-3 consensus across independent passes "would actually suppress detection of real bugs."
- Decompose open-ended tasks by first mapping structure, then identifying high-impact areas, then creating a prioritized plan that adapts as dependencies surface.
1.7 Session state, resumption, and forking
| Mechanism |
What it does |
When to use |
--resume <session-name> |
Continue a specific prior named conversation with its context |
Prior context is mostly still valid |
fork_session |
Create an independent branch from a shared analysis baseline |
Exploring divergent approaches — comparing two testing strategies or two refactoring approaches from the same codebase analysis |
| Start fresh with an injected structured summary |
New session seeded with distilled state |
Prior tool results are stale |
(Not in the guide: continue/continue_conversation is a fourth real mechanism — it resumes the most recent session in the working directory with no session ID needed, distinct from --resume's specific-named-session semantics. Recognize it as a distractor shape: "most recent, no ID" vs. "specific, by ID.")
Two judgment points the guide states outright:
- "Starting a new session with a structured summary is more reliable than resuming with stale tool results." Resumption is not automatically the right answer.
- When you do resume after code has changed, inform the agent about the specific file changes so it re-analyzes those targets, rather than forcing a full re-exploration.
Domain 1 decision table
| Symptom in the stem |
Answer |
| Loop won't terminate / terminates early |
Drive control flow off stop_reason |
| Agent skips a mandatory prerequisite step |
Programmatic prerequisite gate, not a prompt |
| A policy threshold is sometimes violated |
Tool call interception hook |
| Tool results arrive in inconsistent formats |
PostToolUse normalization hook |
| Subagents produce generic or unattributed output |
Pass findings + metadata explicitly in the prompt |
| Coordinator can't spawn subagents |
allowedTools needs "Task" |
| Parallel delegation is slow / sequential |
Multiple Task calls in one coordinator response |
| Report misses whole subtopics, subagents all fine |
Coordinator decomposition too narrow |
| Large multi-file review is shallow and contradictory |
Per-file passes + separate integration pass |
| Two approaches to compare from one baseline |
fork_session |
| Resuming a session where files changed |
Resume + tell it what changed (or restart with a summary if results are stale) |
| Escalating to a human with no transcript access |
Structured handoff summary |
Ten things to have cold
stop_reason is the only legitimate loop-control signal. "tool_use" → continue, "end_turn" → stop.
- Iteration caps are a safety net, never the termination mechanism.
- Text content in the response does not mean the turn is finished.
- Subagents inherit nothing — not conversation history, not memory across invocations.
allowedTools must include "Task" to spawn subagents.
- Parallel = multiple Task calls in one response.
- Prompt instructions have a non-zero failure rate; use a gate or hook wherever the consequence is financial, compliance, or security.
PostToolUse transforms results; interception blocks calls. Direction is not interchangeable.
- All downstream agents succeeding + wrong overall coverage = upstream decomposition problem.
- Fresh session with a structured summary beats resuming with stale tool results.
Domain 2 — Tool Design & MCP Integration (18%, ~11 items)
Five task statements. Note what is not here: no MCP transports, no OAuth, no protocol internals, no server hosting. The guide's out-of-scope list explicitly removes "deploying or hosting MCP servers (infrastructure, networking, container orchestration)" and "OAuth, API key rotation, or authentication protocol details." This domain is about interface design and configuration, not the protocol spec.
In the book: Chapter 3 — Designing Tools (2.1, 2.3, 2.5) · Chapter 4 — Errors & Recovery (2.2) · Chapter 8 — MCP (2.4)
The governing principle
Tool descriptions are the primary mechanism LLMs use for tool selection. Minimal descriptions lead to unreliable selection among similar tools. That sentence is nearly verbatim from the guide and it is the answer to a whole family of questions.
A good description includes:
- what the tool does and what it returns
- input formats it handles
- example queries
- edge cases
- boundary explanations — when to use it versus similar alternatives
The named failure: misrouting
Ambiguous or overlapping descriptions cause misrouting. The guide's own example pair is analyze_content vs analyze_document with near-identical descriptions. Sample question 2 uses get_customer ("Retrieves customer information") vs lookup_order ("Retrieves order details") — both minimal, both accepting similar identifier formats.
Sample question 2's correct answer is expand the descriptions, and its explanation dismantles the three alternatives in a way worth memorizing: few-shot examples "add token overhead without fixing the underlying issue"; a routing layer is "over-engineered and bypasses the LLM's natural language understanding"; consolidating the two tools is "a valid architectural choice but requires more effort than a first step warrants."
System prompt keyword sensitivity
A subtle one the guide calls out: keyword-sensitive instructions in the system prompt can create unintended tool associations and override well-written tool descriptions. If descriptions are good and selection is still wrong, review the system prompt for stray keywords.
The three repair moves
| Move |
Guide's example |
| Rename + rewrite the description to eliminate functional overlap |
analyze_content → extract_web_results with a web-specific description |
| Split a generic tool into purpose-specific tools with defined I/O contracts |
analyze_document → extract_data_points, summarize_content, verify_claim_against_source |
| Review the system prompt for keyword-sensitive instructions |
— |
(Not in the guide: current tool definitions support "strict": true, which guarantees a tool's input matches its schema structurally rather than "reliably." This does not fix misrouting — it does nothing for which tool gets called, only for whether the arguments to the chosen tool are well-formed. Don't confuse it with the guide's actual answer to misrouting, which is description quality.)
(Not in the guide: the tool search tool (tool_search_tool_bm25_20251119 / tool_search_tool_regex_20251119) defers most tool definitions out of context and loads them on demand — a current-API lever for the "huge tool inventory" problem in 2.3, distinct from and not a replacement for the guide's answer of scoping tools per agent role.)
The mechanism
The MCP isError flag is how a tool communicates failure back to the agent. (Outside detail, not in the guide: the Messages API tool_result block spells the equivalent field is_error. The guide only ever writes isError.)
The four categories — memorize these
| Category |
Meaning |
Retryable |
| transient |
timeouts, service unavailability |
yes |
| validation |
invalid input |
no |
| business |
policy violations |
no |
| permission |
insufficient/expired access |
no |
What a good error response carries
errorCategory — transient / validation / permission (the guide's own enumeration in the skills bullet)
isRetryable boolean
- a human-readable description
- for business rule violations, a
retriable: false flag plus a customer-friendly explanation so the agent can communicate appropriately rather than retrying
Generic "Operation failed" prevents the agent from making appropriate recovery decisions. Structured metadata prevents wasted retry attempts.
Two more skills
- Local recovery inside subagents for transient failures. Only propagate to the coordinator what cannot be resolved locally — and when you do, include partial results and what was attempted.
- Distinguish access failures from valid empty results. A timeout needs a retry decision. A query that ran fine and matched nothing is a success with an empty set. Conflating the two is a recurring distractor.
The guide gives a concrete number: giving an agent 18 tools instead of 4–5 degrades tool selection reliability by increasing decision complexity. And agents holding tools outside their specialization tend to misuse them — the named example is a synthesis agent attempting web searches.
Scoped tool access: give each agent only the tools its role needs, plus limited cross-role tools for specific high-frequency needs.
Sample question 9 is the exemplar. Synthesis needs verification; 85% of verifications are simple fact-checks, 15% need real investigation. The right answer is a scoped verify_fact tool for the common case, with complex verifications still routed through the coordinator — least privilege, not full web-search access (over-provisioning, breaks separation of concerns) and not batching (creates blocking dependencies).
Also named: replace generic tools with constrained alternatives — fetch_url → load_document that validates document URLs.
| Value |
Behavior |
Use for |
"auto" |
Model decides; may return text instead of calling a tool |
Open-ended agent turns |
"any" |
Model must call a tool, but chooses which |
Guaranteeing structured output when the document type is unknown and several extraction schemas exist; guaranteeing you don't get conversational text |
{"type": "tool", "name": "..."} |
Forces a specific named tool |
Ensuring a particular tool runs first — e.g. forcing extract_metadata before enrichment tools, then handling subsequent steps in follow-up turns |
(Not in the guide: a fourth value, {"type": "none"}, forces a text-only response with no tool call — the opposite of "any". The guide's table lists three values; know there's a fourth.)
2.4 Integrate MCP servers into Claude Code and agent workflows
Scoping
| Scope |
File |
For |
| Project |
.mcp.json |
Shared team tooling |
| User |
~/.claude.json |
Personal / experimental servers |
(Not in the guide: current Claude Code has a third scope, local — also stored in ~/.claude.json but per-project and private, not shared via git and not global either. It's the current product's answer to "personal/experimental servers with credentials you don't want committed," a use case the guide's table assigns to user scope. Note "local" here means something different from .claude/settings.local.json general local settings — a naming trap, not the same concept.)
Environment variable expansion in .mcp.json — ${GITHUB_TOKEN} — is how you manage credentials without committing secrets. Hardcoding a token in .mcp.json is always a wrong answer.
Tools from all configured MCP servers are discovered at connection time and available simultaneously. Project-scoped and user-scoped servers coexist; scope controls sharing, not availability or priority.
(Not in the guide: this "no priority" claim is correct for which tool the model picks among different servers — nothing changes that. But current Claude Code does have scope-resolution precedence for a same-named server appearing in more than one scope: local > project > user > plugin-provided > claude.ai connectors. That's a different question — resolving a naming collision, not choosing between tools — and it's likely untested given the guide's own scoping table only names two scopes.)
MCP resources
Resources expose content catalogs — issue summaries, documentation hierarchies, database schemas — to reduce exploratory tool calls. The distinction the guide draws in its in-scope list: "resources for content catalogs, tools for actions."
(Not in the guide: current MCP documentation also names prompts — reusable prompt templates a server can provide — as a third primitive alongside tools and resources.)
Two judgment calls
- Enhance MCP tool descriptions to explain capabilities and outputs in detail, or the agent will keep preferring built-in tools like Grep over your more capable MCP tool. Note this is again a description fix, not a scope or configuration fix.
- Choose existing community MCP servers over custom implementations for standard integrations (Jira is the named example). Reserve custom servers for team-specific workflows.
| Tool |
Purpose |
| Grep |
Content search — file contents for patterns: function names, error messages, import statements |
| Glob |
File path pattern matching — finding files by name or extension, e.g. **/*.test.tsx |
| Read / Write |
Full file operations |
| Edit |
Targeted modification using unique text matching |
| Bash |
Shell execution. Named in the task statement title and in scenario 4 — but where a dedicated tool exists (Grep, Glob, Read), prefer it over shelling out |
When Edit fails due to non-unique text matches, fall back to Read + Write. That specific fallback is named in the guide.
Exploration strategy
The guide describes building codebase understanding incrementally: start with Grep to find entry points, then Read to follow imports and trace flows — rather than reading all files upfront. And for tracing function usage across wrapper modules: first identify all exported names, then search for each name across the codebase.
Domain 2 decision table
| Symptom |
Answer |
| Agent picks the wrong one of two similar tools |
Expand and differentiate the descriptions (first step) |
| Descriptions are good, selection still wrong |
Check the system prompt for keyword-sensitive instructions |
| One tool does three unrelated things |
Split into purpose-specific tools with defined I/O contracts |
| Agent retries a policy violation forever |
Return errorCategory: business + retriable: false + customer-friendly explanation |
| Coordinator can't decide how to recover from a subagent failure |
Structured error context: failure type, what was attempted, partial results, alternatives |
| Agent treats "no matching orders" as a failure |
Distinguish access failure from valid empty result |
| Agent has 18 tools and misuses them |
Scope tools per role, ~4–5 each, plus limited cross-role tools |
| Synthesis agent needs occasional fact checks |
Scoped verify_fact tool for the common case; coordinator for the rest |
| Must guarantee a tool call rather than prose |
tool_choice: "any" |
| A specific tool must run first |
tool_choice: {"type": "tool", "name": "..."} |
| Team needs a shared MCP server |
.mcp.json (project scope), credentials via ${ENV_VAR} |
| Personal experimental MCP server |
~/.claude.json (user scope) |
| Agent prefers Grep over your better MCP tool |
Enhance the MCP tool's description |
| Agent burns turns exploring what data exists |
Expose a content catalog as an MCP resource |
| Need a standard Jira integration |
Use the existing community MCP server |
| Searching for a function's callers |
Grep |
Finding all *.test.tsx files |
Glob |
| Edit can't find unique anchor text |
Read + Write |
Ten things to have cold
- Tool descriptions are the primary selection mechanism — and usually the correct first fix.
- Descriptions need input formats, example queries, edge cases, and boundaries vs siblings.
- MCP failures come back via
isError; error payloads carry errorCategory and isRetryable.
- Only transient errors are retryable. Validation, business, and permission errors are not.
- Empty result ≠ error. A timeout is an error; zero matches is a success.
- Subagents recover locally from transient failures; they propagate only the unresolvable — with partial results.
- ~4–5 tools per agent. 18 degrades selection. Out-of-specialization tools get misused.
auto may return text · any forces some tool · forced selection pins a named tool.
.mcp.json = project/shared; ~/.claude.json = user/personal. ${ENV_VAR} for secrets. Both load simultaneously.
- Grep = contents. Glob = filenames. Edit needs unique text; fall back to Read + Write. Bash is in the toolset, but don't shell out where a dedicated tool exists.
Domain 3 — Claude Code Configuration & Workflows (20%, ~12 items)
Six task statements. This is the most "where does the file go" domain on the exam, and it is also where the guide's scope is narrower than the live Claude Code docs. settings.json precedence, permission modes, the full hooks event catalogue, plugins, marketplaces, output styles and authentication appear nowhere in the objectives or in the §17 appendix. Study what's below, not the whole Claude Code manual. (Hooks are tested — but under Domain 1, task 1.5, and only PostToolUse and tool-call interception.)
In the book: Chapter 9 — Configuring Claude Code (3.1, 3.3) · Chapter 10 — Extending Claude Code (3.2, 3.4) · Chapter 12 — Prompt Engineering (3.5) · Chapter 11 — Claude Code in CI (3.6)
3.1 CLAUDE.md hierarchy, scoping, and modular organization
The three levels (guide) — plus two more in the current product
| Level |
Path |
Shared? |
| User |
~/.claude/CLAUDE.md |
No — personal, not shared with teammates via version control |
| Project |
.claude/CLAUDE.md or root CLAUDE.md |
Yes, committed |
| Directory |
subdirectory CLAUDE.md files |
Yes, scoped to that subtree |
The guide highlights one diagnostic explicitly: "a new team member not receiving instructions because they're in user-level rather than project-level configuration." If a scenario says "it works for me but not for the rest of the team," the instructions are in ~/.claude/CLAUDE.md and belong in the project.
(not in the guide) Current Claude Code has two more levels the v1.0 guide doesn't name. Above user/project: a managed/enterprise policy CLAUDE.md at a fixed OS path (org-controlled, loads first, can't be deleted or excluded by an individual engineer). Below project: CLAUDE.local.md, a gitignored file at the project root — personal like the user-level file, but scoped to this one project and loaded right after the project's own CLAUDE.md. Neither is likely tested against the guide's three named levels, but if a scenario mentions org-wide policy or a personal-but-project-specific preference that shouldn't go in the shared file, these are the current answer.
Modular organization
@import syntax references external files to keep CLAUDE.md modular — e.g. importing the specific standards files relevant to each package, chosen by that package's maintainer domain knowledge.
.claude/rules/ is a directory for topic-specific rule files as an alternative to a monolithic CLAUDE.md — testing.md, api-conventions.md, deployment.md.
/memory and /context
Correction: /memory does not show which memory files are loaded this session — it lists and opens the CLAUDE.md/rules file locations Claude Code knows about (including ones that don't exist yet), which makes it the browse/edit tool, not the diagnostic one. /context is the command that shows what's actually loaded right now, in this session, for this directory and file. If behavior is inconsistent across sessions, check /context first; once it shows a file didn't load, use /memory to open that file and find out why (wrong level, stale @import, non-matching glob).
(not in the guide) Separately, Auto Memory is Claude-written memory (distinct from CLAUDE.md, which you write) that persists across sessions automatically — closer to "things Claude noticed about how you work" than versioned project standards. It can reduce the need for hand-maintained CLAUDE.md workarounds for that kind of state, but it isn't a hierarchy level and isn't a guide-tested mechanism.
3.2 Custom slash commands and skills
Scope
| Artifact |
Project (shared via version control) |
User (personal) |
| Slash commands |
.claude/commands/ |
~/.claude/commands/ |
| Skills |
.claude/skills/ |
~/.claude/skills/ |
Sample question 4 is exactly this: a /review command that must be available to every developer on clone or pull goes in .claude/commands/ in the project repository. The explanation kills the distractors: ~/.claude/commands/ isn't shared; CLAUDE.md is for context, not command definitions; and .claude/config.json with a commands array does not exist.
For personal customization without disturbing teammates: create a personal variant in ~/.claude/skills/ under a different name. (not in the guide) Current frontmatter also offers a more direct lever for "who can trigger this": disable-model-invocation (only a human can invoke it, e.g. a /deploy with side effects) and user-invocable: false (only Claude can invoke it, for background-knowledge skills) — worth knowing as the more precise current answer to "control which skills auto-trigger," separate from the naming-convention workaround above.
(not in the guide) The scope table above is also incomplete against the current product: it also has plugin-provided skills/commands and an enterprise/managed level, plus nested-directory discovery (e.g. apps/web/.claude/skills/ → /apps/web:deploy). The guide only tests project vs. user.
SKILL.md frontmatter — three options are tested
| Option |
What it does |
When to use |
context: fork |
Runs the skill in an isolated sub-agent context, preventing skill output from polluting the main conversation |
Skills producing verbose output (codebase analysis) or exploratory context (brainstorming alternatives) |
allowed-tools |
Restricts tool access during skill execution |
Limiting to file write operations to prevent destructive actions |
argument-hint |
Prompts developers for required parameters |
When the skill is invoked without arguments |
Skills vs CLAUDE.md
The guide states the trade-off directly: skills are on-demand invocation for task-specific workflows; CLAUDE.md is always-loaded universal standards.
3.3 Path-specific rules for conditional convention loading
.claude/rules/ files take YAML frontmatter with a paths field containing glob patterns:
---
paths: ["terraform/**/*"]
---
Path-scoped rules load only when editing matching files, which reduces irrelevant context and token usage.
The discriminator vs subdirectory CLAUDE.md
This is the point of the whole task statement, and sample question 6 turns on it. Subdirectory CLAUDE.md files are directory-bound. Glob-pattern rules apply by file type regardless of directory location.
So when test files are spread throughout the codebase alongside the code they test (Button.test.tsx next to Button.tsx) and all tests must follow the same conventions, the answer is .claude/rules/ with **/*.test.tsx — not a CLAUDE.md per directory (can't handle files spread across many directories), not consolidating everything in root CLAUDE.md under headers (relies on inference rather than explicit matching), and not skills (requires manual invocation or Claude choosing to load them, which contradicts "automatic").
Rule of thumb: conventions that follow a directory → subdirectory CLAUDE.md. Conventions that follow a file type across directories → .claude/rules/ with globs.
3.4 Plan mode vs direct execution
|
Plan mode |
Direct execution |
| For |
Complex tasks: large-scale changes, multiple valid approaches, architectural decisions, multi-file modifications |
Simple, well-scoped changes |
| Guide's examples |
Microservice restructuring, library migrations affecting 45+ files, choosing between integration approaches with different infrastructure requirements |
Single-file bug fix with a clear stack trace, adding a date validation conditional |
| Value |
Safe codebase exploration and design before committing to changes, preventing costly rework |
Speed on well-understood work |
Sample question 5 (monolith → microservices) answers plan mode, and its explanation rejects "start direct and switch to plan mode if complexity emerges" on the grounds that the complexity is already stated in the requirements — it isn't something that might emerge later.
Combining them is valid and tested: plan mode for investigation, then direct execution for implementation — e.g. plan a library migration, then execute the planned approach.
The Explore subagent
Named here: use the Explore subagent for verbose discovery phases, isolating discovery output and returning summaries to preserve main conversation context and prevent context window exhaustion during multi-phase tasks.
3.5 Iterative refinement techniques
Four techniques, each with a "when":
Concrete input/output examples. The most effective way to communicate expected transformations when prose descriptions are interpreted inconsistently. The guide says 2–3 concrete examples. Also the fix for edge case handling — e.g. specific test cases with example input and expected output for null values in migration scripts.
Test-driven iteration. Write test suites first — covering expected behavior, edge cases, and performance requirements — then iterate by sharing test failures to guide progressive improvement.
The interview pattern. Have Claude ask questions to surface considerations the developer may not have anticipated, before implementing. Named use: unfamiliar domains, surfacing things like cache invalidation strategies and failure modes.
Single message vs sequential. Provide all issues in one detailed message when the problems interact; fix them sequentially when the problems are independent.
3.6 Integrate Claude Code into CI/CD
The flags
| Flag |
Purpose |
-p (or --print) |
Non-interactive mode. Processes the prompt, writes the result to stdout, exits without waiting for input |
--output-format json |
Machine-parseable output |
--json-schema |
Enforces a structured output shape in CI |
Sample question 10 is a CI job hanging on interactive input; the answer is -p, and the explanation notes that CLAUDE_HEADLESS=true and --batch are non-existent features, and redirecting stdin from /dev/null is a Unix workaround that doesn't properly address the command syntax.
(not in the guide) -p fixes the stdin-hang case sample question 10 describes, but a separate CI failure mode — hanging on an interactive permission prompt rather than stdin — needs --permission-mode (auto/dontAsk/acceptEdits) or --allowedTools to auto-approve specific tools non-interactively. Worth distinguishing if a scenario describes a hang that isn't about input redirection.
Use --output-format json with --json-schema to produce machine-parseable structured findings suitable for automated posting as inline PR comments. (not in the guide) The official claude-code-action GitHub Action wraps this pattern for Actions workflows — it runs headless claude -p with your flags and surfaces structured_output directly as an Actions output, replacing hand-rolled subprocess/parsing glue.
CLAUDE.md is the CI context mechanism
CLAUDE.md is how you give CI-invoked Claude Code project context: testing standards, fixture conventions, review criteria. Documenting valuable-test criteria and available fixtures there improves test generation quality and reduces low-value test output.
(not in the guide) Current Claude Code also has a --bare mode for CI determinism — it skips hook/skill/command/plugin/MCP/CLAUDE.md/auto-memory auto-discovery so runs are reproducible across machines. That's in tension with the paragraph above: a --bare CI run does not get CLAUDE.md automatically, so a bot that wants both determinism and CLAUDE.md-driven standards has to pass that content explicitly (e.g. --append-system-prompt-file) rather than relying on default discovery.
Session context isolation
The guide states it plainly: the same Claude session that generated code is less effective at reviewing its own changes than an independent review instance. (Domain 4.6 develops this.)
Two avoid-duplicate-work skills
- Re-running reviews after new commits: include prior review findings in context and instruct Claude to report only new or still-unaddressed issues, so you don't post duplicate comments.
- Test generation: provide the existing test files in context so it avoids suggesting scenarios already covered.
Domain 3 decision table
| Symptom / requirement |
Answer |
| Teammates aren't getting your instructions |
They're in ~/.claude/CLAUDE.md; move to project level |
| CLAUDE.md has grown monolithic |
Split into .claude/rules/ topic files, or use @import |
| Which memory files are actually loaded this session? |
/context (not /memory — /memory browses/edits file locations) |
| Command must reach every dev on clone |
.claude/commands/ in the repo |
| A skill floods the main conversation |
context: fork |
| A skill must not delete things |
allowed-tools in frontmatter |
| Skill invoked with no arguments |
argument-hint |
| Always-on universal standards |
CLAUDE.md |
| On-demand task-specific workflow |
Skill |
| Conventions for a file type spread across dirs |
.claude/rules/ with a glob in paths: |
| Conventions for one directory |
Subdirectory CLAUDE.md |
| Multi-file architectural change |
Plan mode |
| Single-file fix with a clear stack trace |
Direct execution |
| Verbose discovery is eating the context window |
Explore subagent |
| Prose spec keeps being misread |
2–3 concrete input/output examples |
| Unfamiliar domain, unknown unknowns |
Interview pattern |
| Several fixes that interact |
One detailed message |
| Several independent fixes |
Sequential iteration |
| CI job hangs waiting for input |
-p |
| Need parseable findings for PR comments |
--output-format json + --json-schema |
| CI reviews lack project standards |
Put them in CLAUDE.md |
| Re-review posts duplicate comments |
Include prior findings; report only new/unaddressed |
| Generated tests duplicate existing ones |
Provide existing test files in context |
Ten things to have cold
- User-level CLAUDE.md is not shared. "Only works for me" = wrong level.
.claude/rules/ with paths: globs beats subdirectory CLAUDE.md when files are spread across directories.
.claude/commands/ = team; ~/.claude/commands/ = personal. .claude/config.json does not exist.
- The three SKILL.md frontmatter options tested:
context: fork, allowed-tools, argument-hint.
context: fork = isolated subagent context, keeps verbose output out of the main conversation.
- Skills = on-demand. CLAUDE.md = always loaded.
- Plan mode when the complexity is already known from the requirements — don't wait for it to emerge.
- Explore subagent for verbose discovery.
-p for CI. CLAUDE_HEADLESS and --batch are fictional.
--output-format json + --json-schema for structured CI findings; CLAUDE.md supplies the review criteria.
Domain 4 — Prompt Engineering & Structured Output (20%, ~12 items)
Six task statements, heavily anchored to two scenarios: Structured Data Extraction and Claude Code for CI (where "minimize false positives" is stated in the scenario text itself).
Scope note: this domain does not test extended thinking parameters, prompt caching mechanics, temperature, prefill, or model selection. Those are either absent from the objectives or on the out-of-scope list. The one place extended thinking appears is 4.6, and only as the wrong answer.
In the book: Chapter 12 — Prompt Engineering (4.1, 4.2) · Chapter 13 — Structured Output (4.3, 4.4, 4.5) · Chapter 15 — Escalation & Provenance (4.6)
4.1 Explicit criteria to improve precision and reduce false positives
The core claim
Explicit categorical criteria beat vague instructions. The guide's own contrast:
✅ "flag comments only when claimed behavior contradicts actual code behavior"
❌ "check that comments are accurate"
And it names the specific failure: general instructions like "be conservative" or "only report high-confidence findings" fail to improve precision compared to specific categorical criteria. If an option offers to tell the model to be more conservative, it is wrong.
Why false positives matter disproportionately
High false positive rates in one category undermine developer confidence in the accurate categories too. Trust is not compartmentalized.
Three skills
- Write specific review criteria defining which issues to report (bugs, security) versus skip (minor style, local patterns) — rather than relying on confidence-based filtering.
- Temporarily disable high false-positive categories to restore developer trust while you improve the prompts for those categories. This is a legitimate answer, not a cop-out.
- Define explicit severity criteria with concrete code examples for each severity level to get consistent classification.
4.2 Few-shot prompting
When few-shot is the answer
The guide positions few-shot as the most effective technique for achieving consistently formatted, actionable output when detailed instructions alone produce inconsistent results. Four named uses:
- Demonstrating ambiguous-case handling — tool selection for ambiguous requests, branch-level test coverage gaps.
- Enabling generalization to novel patterns rather than matching only pre-specified cases.
- Reducing hallucination in extraction — informal measurements, varied document structures.
- Format consistency — showing the exact desired output shape.
How many, and what in them
2–4 targeted examples for ambiguous scenarios, and critically: examples that show the reasoning for why one action was chosen over plausible alternatives. Input→output pairs alone are weaker than input→reasoning→output.
Other named patterns:
- Examples demonstrating a specific output format — the guide's tuple is location, issue, severity, suggested fix.
- Examples distinguishing acceptable code patterns from genuine issues, to cut false positives while still generalizing.
- Examples covering varied document structures — inline citations vs bibliographies, methodology sections vs embedded details, narrative descriptions vs structured tables.
- Examples addressing empty/null extraction of required fields by showing correct extraction from varied formats.
The 4.1 / 4.2 boundary
Both reduce false positives. The exam distinguishes them by what's missing:
- The decision boundary is undefined → explicit criteria (4.1).
- The boundary is defined but application is inconsistent, especially on ambiguous cases → few-shot examples (4.2).
Sample question 2 (Domain 2) is a reminder that few-shot is not a universal answer: there, the root cause was thin tool descriptions, and few-shot "adds token overhead without fixing the underlying issue."
The mechanism
tool_use with JSON schemas is the most reliable approach for guaranteed schema-compliant structured output. You define an extraction tool whose input_schema is your target schema, then read the structured data out of the tool_use response — not out of the response text.
(not in the guide) Two current-API mechanisms sharpen this, and are worth knowing precisely because they answer two different questions than the one above:
strict: true on the tool definition upgrades the guarantee from "reliable in practice" (forced tool_choice plus ordinary validation) to structurally guaranteed schema compliance on the tool's input — the same mechanism, hardened. This is the single highest-value current-API fact for this domain, since it's a direct strengthening of the claim this section already makes.
output_config.format: {"type": "json_schema", ...} guarantees schema-compliant JSON with no tool involved at all — the JSON lands in a plain text block, not a tool_use block. More direct than inventing a fake extraction tool when there's no real tool semantics, which is exactly the extract_invoice-style scenario this domain is built around.
Neither of these does anything for semantics. They guarantee shape, not correctness — a schema-valid, strict-validated extraction can still have stated_total and calculated_total disagree. That's still §4.4's job, unchanged.
| Value |
Behavior |
Named use in this domain |
"auto" |
Model may return text instead of calling a tool |
— |
"any" |
Must call a tool, chooses which |
Guarantee structured output when multiple extraction schemas exist and the document type is unknown |
{"type": "tool", "name": "extract_metadata"} |
Must call that specific tool |
Ensure a particular extraction runs before enrichment steps |
(not in the guide) A fourth value, {"type": "none"}, forces a text-only response (no tool call) in the current API. The guide's scope appears limited to the three values above — know the fourth exists, but expect the exam to test only these three.
Syntax vs semantics — the single most important idea here
Strict JSON schemas via tool use eliminate syntax errors but do not prevent semantic errors — line items that don't sum to total, values in the wrong fields.
If a scenario describes malformed JSON, the answer is a schema. If it describes well-formed JSON that is wrong, the answer is validation logic (4.4), not a stricter schema.
Schema design
| Pattern |
Why |
| Optional / nullable fields where source documents may not contain the information |
Prevents the model from fabricating values to satisfy required fields. This is the schema-level hallucination fix |
Enum value "unclear" |
Gives the model an honest option for ambiguous cases |
Enum "other" + a detail string |
Extensible categorization without forcing a bad fit |
| Format normalization rules in the prompt alongside the strict schema |
Handles inconsistent source formatting — the schema constrains shape, the prompt handles messy inputs |
4.4 Validation, retry, and feedback loops
Retry with error feedback
On retry, send the original document, the failed extraction, and the specific validation error. Appending the concrete error is what guides self-correction — a bare retry usually reproduces the same output.
When retry will not work
The guide is explicit: retries are ineffective when the required information is simply absent from the source document. Format errors and structural output errors are fixable by retry; missing information is not. The named example is information that exists only in an external document that wasn't provided.
Classify before you retry, and route the unfixable to human review.
Self-correction validation flows
Two concrete designs the guide names:
- Extract
calculated_total alongside stated_total and flag discrepancies. This is how you catch a semantic error a schema can't.
- Add a
conflict_detected boolean for inconsistent source data.
detected_pattern
Add a detected_pattern field to structured findings so you can analyze which code constructs trigger findings — enabling systematic analysis of what developers dismiss, i.e. your false-positive patterns.
4.5 Batch processing
Message Batches API facts
- 50% cost savings
- Up to 24-hour processing window, no guaranteed latency SLA
- Does not support multi-turn tool calling within a single request — you cannot execute tools mid-request and return results
custom_id correlates request/response pairs
The judgment
Appropriate: non-blocking, latency-tolerant workloads — overnight reports, weekly audits, nightly test generation.
Inappropriate: blocking workflows — pre-merge checks where a developer is waiting.
Sample question 11 is exactly this split: batch the overnight technical-debt report, keep the pre-merge check synchronous. Its explanation rejects "batches are often faster in practice" as unacceptable for a blocking workflow, and notes that batch result ordering is a non-issue because custom_id handles correlation.
Three operational skills
- Match API to latency requirement, per workflow, not per organization.
- Calculate submission frequency from SLA constraints — the guide's worked example: submit in 4-hour windows to guarantee a 30-hour SLA given 24-hour batch processing.
- Handle failures by
custom_id: resubmit only the failed documents, with appropriate modifications such as chunking documents that exceeded context limits.
- Refine the prompt on a sample set before batch-processing large volumes, to maximize first-pass success and avoid costly resubmission cycles.
4.6 Multi-instance and multi-pass review
Why self-review is weak
A model retains reasoning context from generation, making it less likely to question its own decisions in the same session. Therefore:
Independent review instances — without the generator's prior reasoning context — are more effective at catching subtle issues than self-review instructions or extended thinking.
Note that extended thinking is named here specifically as the inferior option. Don't pick it.
Multi-pass for large reviews
Split large reviews into per-file local analysis passes plus separate cross-file integration passes, to avoid attention dilution and contradictory findings. (Same pattern as task 1.6; sample question 12 is the item.)
Confidence scoring — the nuance
4.6 does list a skill: "Running verification passes where the model self-reports confidence alongside each finding to enable calibrated review routing." This looks like it contradicts sample question 3, where self-reported confidence is the wrong answer for escalation. It doesn't, and the distinction matters:
- Self-reported confidence as an autonomous escalation trigger in a live conversation → wrong (poorly calibrated; the agent is already overconfident on hard cases).
- Self-reported confidence as a signal that gets calibrated against a labeled validation set and used to prioritize finite human reviewer attention → right (see task 5.5).
Confidence is acceptable as an input to a calibrated routing system. It is not acceptable as a decision on its own.
Domain 4 decision table
| Symptom |
Answer |
| Vague review instructions, inconsistent findings |
Explicit categorical criteria (which issues to report vs skip) |
| Someone suggests "tell it to be conservative" |
Wrong — specific criteria instead |
| One category has terrible precision, trust is collapsing |
Disable that category temporarily; fix its prompt |
| Severity labels are inconsistent |
Explicit severity criteria with concrete code examples per level |
| Criteria are clear but ambiguous cases go wrong |
2–4 few-shot examples with reasoning |
| Output format varies run to run |
Few-shot examples demonstrating the exact format |
| Extraction fails on varied document structures |
Few-shot examples across those structures |
| Malformed JSON breaks the parser |
tool_use with a JSON schema |
| Valid JSON, but totals don't add up |
Semantic validation (calculated_total vs stated_total), not a stricter schema |
| Model invents values for missing data |
Make those fields optional / nullable |
| Category doesn't fit the enum |
"other" + detail string; "unclear" for ambiguity |
| Document type unknown, several schemas |
tool_choice: "any" |
| One extraction must run before enrichment |
Forced tool selection |
| Validation fails — retry? |
Yes for format/structure; no if the information isn't in the source |
| Retry keeps producing the same error |
Include the specific validation error in the follow-up |
| Need to find false-positive patterns |
detected_pattern field on findings |
| Blocking pre-merge check |
Synchronous API |
| Overnight / weekly analysis |
Batch API (50%, ≤24h, no SLA) |
| Batch job partially failed |
Resubmit by custom_id, chunk oversized documents |
| Reviewing code the same session generated |
Independent review instance |
| 14-file PR review is shallow and contradictory |
Per-file passes + cross-file integration pass |
| Limited human reviewer capacity |
Field-level confidence, calibrated on a labeled set |
Ten things to have cold
- Explicit categorical criteria beat "be conservative" or "only report high-confidence findings."
- Few-shot examples should show reasoning, not just input→output. 2–4 of them.
- Schemas fix syntax. They do not fix semantics.
- Nullable/optional fields are the schema-level fix for fabricated values.
"unclear" and "other" + detail are named enum patterns.
- Retry works for format and structure errors; it cannot conjure absent information.
- Retries must carry the original document + the failed extraction + the specific error.
- Batch: 50% off, ≤24h, no SLA, no multi-turn tool calling,
custom_id for correlation. Never for blocking workflows.
- Independent review instance beats self-review — and beats extended thinking, which the guide names as inferior.
- Self-reported confidence: invalid as an autonomous escalation trigger, valid as a calibrated input to human-review routing.
Domain 5 — Context Management & Reliability (15%, ~9 items)
Six task statements. The lightest domain by weight, but it is a primary domain on four of the six scenarios (1, 2, 3, 6), so its ideas turn up inside other domains' questions.
Scope note: no context-editing API, no memory tool, no compaction API, no 1M-token beta, no rate limits, no prompt-cache mechanics. The context management tested here is architectural — what you put in the window and what you keep outside it.
In the book: Chapter 14 — Context Engineering (5.1, 5.4) · Chapter 15 — Escalation & Provenance (5.2, 5.5, 5.6) · Chapter 4 — Errors & Recovery (5.3)
Four named risks
Progressive summarization risk. Summarizing repeatedly condenses numerical values, percentages, dates, and customer-stated expectations into vague summaries. The specifics are exactly what you can't afford to lose.
"Lost in the middle." Models reliably process information at the beginning and end of long inputs but may omit findings from middle sections.
Tool result accumulation. Tool results consume tokens disproportionately to their relevance — the guide's example is 40+ fields per order lookup when only 5 are relevant.
History completeness. You must pass the complete conversation history in subsequent API requests to maintain conversational coherence.
The six fixes
| Fix |
Detail |
| Case facts block |
Extract transactional facts (amounts, dates, order numbers, statuses) into a persistent "case facts" block included in each prompt, outside summarized history |
| Separate context layer |
Persist structured issue data (order IDs, amounts, statuses) into its own layer for multi-issue sessions |
| Trim tool outputs at the source |
Keep only the relevant fields before they accumulate — e.g. only return-relevant fields from an order lookup |
| Position-aware ordering |
Put key findings summaries at the beginning of aggregated inputs; organize details under explicit section headers |
| Require metadata from subagents |
Dates, source locations, methodological context in structured outputs, to support accurate downstream synthesis |
| Structured data, not prose, upstream |
Modify upstream agents to return key facts, citations, relevance scores instead of verbose content and reasoning chains, when downstream agents have limited context budgets |
The unifying move: pull the durable facts out into a structured layer that never gets summarized, and let the narrative be compressible.
Current API note (outside this domain's tested scope): the memory tool (memory_20250818, a persistent /memories directory) and the context editing API (clear_tool_uses_20250919) are now official mechanisms for the same durable-facts-block and trim-at-source problems above. Not tested here — the scope note at the top of this domain stands.
5.2 Escalation and ambiguity resolution
Valid escalation triggers — exactly three
- The customer requests a human.
- Policy exceptions or gaps — and the guide adds a pointed qualifier: "not just complex cases."
- Inability to make meaningful progress.
Invalid triggers
Sentiment-based escalation and self-reported confidence scores are named as "unreliable proxies for actual case complexity." Sample question 3 rejects both: confidence fails because "the agent is already incorrectly confident on hard cases," and sentiment "solves a different problem entirely; sentiment doesn't correlate with case complexity."
The nuance on explicit requests
Two rules that sit side by side:
- Honor an explicit demand for a human immediately, without first attempting investigation.
- But when the customer is frustrated rather than demanding, acknowledge the frustration while offering resolution if the issue is within your capability — escalating only if the customer reiterates their preference.
The discriminator is whether they asked for a human, not how upset they sound.
Policy gaps
Escalate when policy is ambiguous or silent on the customer's specific request. The guide's example: competitor price matching when the policy only addresses own-site adjustments. Not covered ≠ not allowed; that's a human decision.
Multiple matches
When a tool returns multiple customer matches, instruct the agent to ask for additional identifiers — not to select heuristically.
Fixing bad calibration
Sample question 3's fix for an agent escalating easy cases and attempting hard ones: explicit escalation criteria in the system prompt, with few-shot examples showing when to escalate versus resolve. Proportionate first response — not a trained classifier, not sentiment analysis, not confidence thresholds.
5.3 Error propagation across multi-agent systems
What good propagation carries
Failure type · what was attempted · partial results · potential alternative approaches. That four-part payload is what lets the coordinator make an intelligent recovery decision — retry with a modified query, try an alternative, or proceed with partial results.
The two anti-patterns, both named
- Silently suppressing errors — returning empty results as success. Prevents any recovery and risks silently incomplete output.
- Terminating the entire workflow on a single failure.
And a third, softer one: generic error statuses like "search unavailable" hide valuable context from the coordinator. Sample question 8 rejects retry-with-backoff-then-generic-status on exactly this basis, even though the retry logic itself is fine.
Local recovery first
Subagents implement local recovery for transient failures, and propagate only what they cannot resolve — including what was attempted and partial results.
Access failure vs valid empty result
Same distinction as task 2.2, restated here for the multi-agent case. A timeout needs a retry decision; a successful query with no matches does not.
Coverage annotations
Structure synthesis output with coverage annotations indicating which findings are well-supported versus which topic areas have gaps due to unavailable sources. Partial results are acceptable; undisclosed partial results are not.
5.4 Context in large codebase exploration
Context degradation
The symptom the guide describes: in extended sessions, models start giving inconsistent answers and referencing "typical patterns" rather than the specific classes discovered earlier. If a stem describes an agent drifting toward generic advice about a codebase it explored an hour ago, this is the diagnosis.
Five techniques
| Technique |
Detail |
| Scratchpad files |
Persist key findings across context boundaries; reference them for subsequent questions to counteract degradation |
| Subagent delegation |
Spawn subagents for specific questions ("find all test files", "trace refund flow dependencies") while the main agent preserves high-level coordination and isolates verbose exploration output |
| Phase summaries |
Summarize key findings from one exploration phase before spawning subagents for the next, injecting summaries into their initial context |
| Structured state persistence / manifests |
Each agent exports state to a known location; the coordinator loads a manifest on resume and injects it into agent prompts. This is the named crash-recovery design |
/compact |
Reduce context usage during extended exploration when the window fills with verbose discovery output |
Current API note (outside this domain's tested scope): the memory tool matches the scratchpad/manifest pattern above closely — a persistent /memories directory Claude reads and writes across sessions. Server-side compaction (compact_20260112) can also take a custom instructions field, letting you directly tell it to preserve dollar amounts/dates verbatim rather than relying only on the manual case-facts workaround. Not tested here.
5.5 Human review workflows and confidence calibration
The headline risk
Aggregate accuracy metrics can mask poor performance on specific document types or fields. 97% overall can hide 60% on one document class. Validate accuracy by document type and field segment before automating high-confidence extractions.
The four techniques
- Stratified random sampling of high-confidence extractions, for ongoing error rate measurement and detecting novel error patterns. You keep sampling the ones you think are fine, precisely because that's where blind spots hide.
- Accuracy analysis by document type and field to verify consistent performance across all segments before reducing human review.
- Field-level confidence scores, calibrated using labeled validation sets, for routing review attention.
- Route to human review extractions with low model confidence or ambiguous/contradictory source documents — prioritizing limited reviewer capacity.
Note again the confidence nuance: here it is legitimate, because it is field-level, calibrated against labeled data, and used for prioritization rather than as an autonomous decision.
5.6 Provenance and uncertainty in multi-source synthesis
How provenance is lost
Source attribution is lost during summarization steps when findings are compressed without preserving claim-source mappings. The fix is structural: require structured claim-source mappings — source URLs, document names, relevant excerpts — that downstream agents preserve and merge through synthesis.
Conflicting sources
Annotate conflicts with source attribution rather than arbitrarily selecting one value. Two credible sources with different statistics → report both, attributed. Extended: complete document analysis with the conflicting values included and explicitly annotated, letting the coordinator decide how to reconcile before passing to synthesis.
Temporal data
Require publication or data collection dates in structured outputs, to prevent temporal differences from being misinterpreted as contradictions. Two "conflicting" figures may just be from different years.
Current API note (outside this domain's tested scope): the citations API (citations: {"enabled": true} on a document content block) is the built-in equivalent of structured claim-source mapping when Claude reads a document directly — it returns cited text plus document/location automatically. The DIY mapping above stays necessary for subagent-to-subagent handoffs, where there's no document block, just a prior subagent's own findings. Not tested here.
Report structure
- Explicit sections distinguishing well-established findings from contested ones, preserving original source characterizations and methodological context.
- Render different content types appropriately — financial data as tables, news as prose, technical findings as structured lists — rather than converting everything into a uniform format.
Domain 5 decision table
| Symptom |
Answer |
| Amounts, dates and order numbers go vague over a long chat |
Persistent "case facts" block outside summarized history |
| Findings from the middle of a long input get dropped |
Key findings summary at the top; explicit section headers |
| Order lookups return 40+ fields, 5 are relevant |
Trim tool outputs before they enter context |
| Downstream agent has no context budget |
Upstream returns structured facts + citations, not prose |
| Customer says "get me a human" |
Escalate immediately, no investigation first |
| Customer is angry but the issue is simple |
Acknowledge, offer resolution; escalate if they reiterate |
| Policy is silent on the request |
Escalate — policy gap |
get_customer returns three matches |
Ask for another identifier |
| Someone proposes sentiment or confidence-based escalation |
Wrong — explicit criteria + few-shot examples |
| Subagent times out |
Structured error context: type, attempted query, partial results, alternatives |
| Subagent returns empty-as-success |
Anti-pattern — suppression |
| One subagent fails, workflow dies |
Anti-pattern — proceed with partial results |
| Report is based on partial coverage |
Coverage annotations distinguishing supported from gapped |
| Agent starts citing "typical patterns" mid-session |
Context degradation → scratchpad files |
| Verbose exploration is filling the window |
Delegate to subagents; /compact |
| Long-running exploration needs crash recovery |
Structured state exports + manifest the coordinator loads on resume |
| 97% accuracy, want to cut human review |
Segment accuracy by document type and field first |
| Need ongoing error detection in the "safe" bucket |
Stratified random sampling of high-confidence extractions |
| Limited reviewer capacity |
Calibrated field-level confidence routing |
| Citations vanish after synthesis |
Structured claim-source mappings preserved through every step |
| Two credible sources disagree |
Annotate both with attribution; don't pick one |
| Figures look contradictory |
Require publication/collection dates |
Ten things to have cold
- Progressive summarization destroys numbers, dates, and stated expectations — pull them into a persistent facts block.
- Lost in the middle: beginning and end are reliable; the middle is not. Order inputs accordingly.
- Trim verbose tool outputs at the source, before they accumulate.
- Three valid escalation triggers: customer asks · policy gap · no progress. Sentiment and self-confidence are not triggers.
- Explicit human request → escalate immediately, no investigation first.
- Multiple matches → ask for another identifier, never guess.
- Structured error context = failure type + what was attempted + partial results + alternatives.
- Never suppress an error as success; never kill the workflow over one failure. Annotate coverage gaps instead.
- Long sessions degrade → scratchpad files, subagent delegation, phase summaries, manifests,
/compact.
- Provenance survives synthesis only if claim-source mappings are structured and explicitly preserved; conflicts get annotated, and dates get required.
Practice Questions
Forty-five original scenario questions written against the v1.0 task statements and in the style of the guide's own samples — D1 ×11, D2 ×8, D3 ×9, D4 ×10, D5 ×7. Two items are multiple-response, as v1.0 says to expect.
These are mine, not Anthropic's. The twelve official sample questions are in §9 of the exam guide PDF — do those too; their answer explanations are the single best calibration you can get for how the exam thinks.
Answers and explanations follow the questions. Resist scrolling.
Domain 1 — Agentic Architecture & Orchestration
Q1. (Customer Support Resolution Agent) Your agentic loop terminates as soon as the assistant response contains any text, on the theory that Claude only writes prose when it has a final answer. Support engineers report the agent frequently replies "Let me look that up for you" and then stops. What is the correct fix?
A. Add a system prompt instruction telling Claude not to narrate before calling tools.
B. Continue the loop while stop_reason is "tool_use" and terminate only on "end_turn".
C. Set a minimum of three iterations before the loop is allowed to exit.
D. Strip text content blocks from the response before evaluating the termination condition.
Q2. (Multi-Agent Research System) Your coordinator delegates to a web search subagent and then a synthesis subagent. The synthesis subagent's prompt is "Synthesize the research findings into a report." Its output is generic and cites nothing. What is the root cause?
A. The synthesis subagent's model is too small for synthesis work.
B. The synthesis subagent needs the WebSearch tool so it can gather its own material.
C. The coordinator did not include the prior agents' findings in the synthesis subagent's prompt; subagents do not inherit parent context.
D. The coordinator should invoke synthesis in the same turn as web search so context is shared.
Q3. (Multi-Agent Research System) You want four subagents to run concurrently rather than one after another. How does the coordinator achieve this?
A. Emit multiple Task tool calls in a single coordinator response.
B. Set parallel: true in each AgentDefinition.
C. Issue one Task call per turn and rely on the SDK to overlap them.
D. Set disable_parallel_tool_use to false on the coordinator's requests.
Q4. (Customer Support Resolution Agent) Company policy is that refunds above $500 require human approval. Your system prompt says so clearly. Over three months, four refunds above $500 were processed autonomously. Choose the most appropriate remediation.
A. Restate the rule at both the beginning and end of the system prompt for position emphasis.
B. Add few-shot examples showing the agent escalating a $600 refund.
C. Intercept outgoing process_refund calls and block any exceeding $500, redirecting to escalation.
D. Add a PostToolUse hook on process_refund that flags refunds above $500 for after-the-fact audit.
Q5. (Developer Productivity with Claude) Different MCP servers return timestamps as Unix epochs, ISO 8601 strings, and human-readable dates. The agent frequently miscompares dates. What is the cleanest fix?
A. A PostToolUse hook that normalizes timestamp formats before the model processes the results.
B. A system prompt section explaining all three formats and how to convert between them.
C. Rewrite each MCP server to emit ISO 8601.
D. A tool-call interception hook that rejects calls to tools known to return non-ISO timestamps.
Q6. (Code Generation with Claude Code) You analyzed a codebase in a session two days ago. Since then, six files in the authentication module were rewritten. You want to continue the investigation. What is the best approach?
A. Resume the session with --resume and say nothing; Claude will re-read files as needed.
B. Resume the session with --resume and tell it specifically which files changed so it re-analyzes those targets.
C. Fork the session so the original analysis is preserved.
D. Always start fresh; resumed sessions cannot be trusted after code changes.
Q7. (Developer Productivity with Claude) You want to compare two refactoring strategies, both starting from the same completed codebase analysis, and keep the analysis session intact. Which mechanism?
A. --resume twice with different follow-up prompts.
B. fork_session to create independent branches from the shared baseline.
C. /compact with instructions focused on each strategy.
D. Two new sessions, each seeded with a copy of the analysis summary.
Q8. (Multi-Agent Research System) The coordinator always runs the full pipeline — web search, document analysis, synthesis, report generation — even for a query like "what year was the GDPR enacted?" Latency and cost are high for trivial queries. What should change?
A. Reduce each subagent's max_tokens so trivial queries cost less.
B. Have the coordinator analyze query requirements and dynamically select which subagents to invoke.
C. Add a caching layer in front of the web search subagent.
D. Merge web search and document analysis into one subagent to halve the pipeline.
Q9. (Code Generation with Claude Code) A task reads: "Add comprehensive tests to this legacy service." You do not know the module structure, the existing coverage, or the dependency graph. Which decomposition strategy fits?
A. A fixed prompt chain: generate unit tests, then integration tests, then end-to-end tests.
B. Dynamic adaptive decomposition — map structure first, identify high-impact areas, then build a prioritized plan that adapts as dependencies surface.
C. Parallel subagents, one per directory, each writing tests independently.
D. A single prompt with the whole repository in context.
Q10. (Customer Support Resolution Agent) A customer writes: "My order arrived damaged, I was double-charged, and I want to change my shipping address." Your agent addresses the damage and ignores the other two. Which design change follows the guide's recommended pattern?
A. Instruct the agent to ask the customer to submit one issue per message.
B. Decompose the request into distinct items, investigate each in parallel using shared context, then synthesize a unified resolution.
C. Escalate any message containing more than one issue to a human.
D. Process the issues strictly in the order stated, one conversation turn each.
Q11. (Customer Support Resolution Agent) Select TWO. Your agent is escalating to human agents who cannot see the conversation transcript. Which two elements belong in the handoff?
A. The complete raw conversation transcript, verbatim.
B. Root cause analysis.
C. The agent's self-reported confidence score for the escalation decision.
D. Customer ID, refund amount, and recommended action.
E. A list of all tools the agent called, with timings.
Domain 2 — Tool Design & MCP Integration
Q12. (Multi-Agent Research System) Two tools, analyze_content ("Analyzes content") and analyze_document ("Analyzes documents"), are selected almost at random. What is the most effective first step?
A. Consolidate them into one analyze tool that branches internally.
B. Rename and rewrite descriptions to eliminate overlap — e.g. analyze_content → extract_web_results with a web-specific description.
C. Add a deterministic keyword router in front of the model.
D. Set tool_choice: "any" so the model must commit to one of them.
Q13. (Customer Support Resolution Agent) lookup_order times out against a flaky backend. Which error response best enables agent recovery?
A. {"error": "Operation failed"}
B. isError: true, errorCategory: "transient", isRetryable: true, plus a human-readable description of what was attempted.
C. Return an empty order list so the conversation can continue.
D. Raise the exception to the top level and end the session.
Q14. (Customer Support Resolution Agent) A customer requests a refund on an item outside the 30-day return window. The tool correctly refuses. What should it return?
A. errorCategory: "transient" with isRetryable: true, so the agent can retry after backoff.
B. errorCategory: "business", retriable: false, and a customer-friendly explanation of the policy.
C. A generic failure so the agent escalates automatically.
D. A successful empty result, since no refund was created.
Q15. (Multi-Agent Research System) Your synthesis agent has been given all 18 tools available in the system "so it never gets stuck." It now attempts web searches mid-synthesis and produces inconsistent reports. What is the best correction?
A. Add a system prompt line telling the synthesis agent not to search.
B. Scope the synthesis agent to its role's tools, and add one narrow cross-role tool (verify_fact) for the high-frequency simple case, routing complex verification through the coordinator.
C. Remove all tools from the synthesis agent so it can only write.
D. Increase the synthesis agent's context window so it retains its instructions better.
Q16. (Developer Productivity with Claude) Your team's custom MCP server exposes a search_internal_docs tool with the description "Searches docs." The agent keeps using Grep on the local repo instead. What fixes this?
A. Move the server from user scope to project scope in .mcp.json.
B. Remove Grep from allowedTools.
C. Expand the MCP tool's description to explain its capabilities, coverage, and outputs in detail.
D. Set tool_choice: "any" to force a tool call.
Q17. (Developer Productivity with Claude) You need a shared GitHub MCP server available to the whole team, authenticated with a token that must not be committed. What do you do?
A. Add it to ~/.claude.json with the token inline and tell everyone to copy the file.
B. Add it to .mcp.json with ${GITHUB_TOKEN} and commit that file.
C. Add it to .mcp.json with the token inline and add .mcp.json to .gitignore.
D. Write a custom MCP server that reads the token from a vault at runtime.
Q18. (Developer Productivity with Claude) You need to find every caller of calculatePremium() across a large TypeScript monorepo. Which tool?
A. Glob with **/*.ts
B. Grep
C. Read on each file, following imports
D. Bash with find
Domain 3 — Claude Code Configuration & Workflows
Q19. (Code Generation with Claude Code) You documented your team's commit message conventions and Claude follows them perfectly for you. Two new hires report Claude ignores them entirely. Where are the conventions?
A. In .claude/rules/ without a paths: field.
B. In ~/.claude/CLAUDE.md.
C. In .claude/CLAUDE.md, but the new hires haven't run /memory.
D. In a skill that requires manual invocation.
Q20. (Code Generation with Claude Code) Your repository has React components, API handlers, and database models, each with different conventions. Test files sit next to the code they test, scattered across every directory, and must all follow one shared testing convention. What is the most maintainable configuration?
A. A CLAUDE.md in each subdirectory.
B. All conventions in the root CLAUDE.md under clear headers.
C. .claude/rules/ files with YAML paths: glob patterns, including **/*.test.tsx for tests.
D. A skill per code type containing the relevant conventions.
Q21. (Code Generation with Claude Code) A /codebase-map skill produces 8,000 tokens of directory listings and dependency graphs every time it runs, crowding out the actual work. Which frontmatter option addresses this?
A. allowed-tools: Read, Grep
B. argument-hint: [directory]
C. context: fork
D. model: haiku
Q22. (Code Generation with Claude Code) Which statement correctly distinguishes skills from CLAUDE.md?
A. Skills are always loaded; CLAUDE.md is loaded on demand.
B. Skills are for on-demand task-specific workflows; CLAUDE.md is for always-loaded universal standards.
C. Skills are project-scoped only; CLAUDE.md can be user- or project-scoped.
D. Skills replace CLAUDE.md in projects that use them.
Q23. (Code Generation with Claude Code) You need to migrate the application from one HTTP client library to another. It touches roughly 45 files, and there are two viable migration strategies with different testing implications. What is the right approach?
A. Direct execution with a very detailed instruction listing every file.
B. Plan mode to explore and design, then direct execution to implement the chosen plan.
C. Direct execution, switching to plan mode only if something unexpected appears.
D. Split the work into 45 single-file direct-execution tasks.
Q24. (Claude Code for CI) Your pipeline runs claude "Review this PR" and the job times out with no output. What is the fix?
A. claude -p "Review this PR"
B. export CLAUDE_HEADLESS=true
C. claude --batch "Review this PR"
D. claude "Review this PR" < /dev/null
Q25. (Claude Code for CI) You want Claude Code's review findings posted as inline PR comments by a downstream script. Which combination produces reliably parseable output?
A. --output-format json together with --json-schema
B. A prompt instructing Claude to reply in JSON only
C. --output-format text and a regex parser
D. --json-schema alone, since it implies JSON output
Q26. (Claude Code for CI) Every time a developer pushes a new commit, the review bot re-posts the same twelve comments from the previous run. What change fixes this?
A. Only run the review on the first push of a PR.
B. Include the prior review findings in context and instruct Claude to report only new or still-unaddressed issues.
C. Deduplicate comments in the posting script by hashing their text.
D. Reduce the review scope to only the files changed in the latest commit.
Q27. (Code Generation with Claude Code) You describe a data transformation in prose three times and get three different interpretations. What is the guide's recommended technique?
A. Move the description into CLAUDE.md so it is always loaded.
B. Provide 2–3 concrete input/output examples.
C. Ask Claude to restate the requirement before implementing.
D. Increase specificity by adding more prose constraints.
Domain 4 — Prompt Engineering & Structured Output
Q28. (Claude Code for CI) Your review bot flags too many false positives on the "misleading comment" category. Your current instruction is "check that comments are accurate." Which change most improves precision?
A. Append "be conservative and only report high-confidence findings."
B. Replace it with "flag comments only when the claimed behavior contradicts the actual code behavior."
C. Require the model to output a confidence score and filter below 0.8.
D. Run the review three times and report only issues appearing in all three.
Q29. (Claude Code for CI) Six months in, the "security" category is 92% precise and the "performance" category is 30% precise. Developers have started ignoring all bot comments. What is a legitimate immediate action?
A. Temporarily disable the performance category while you improve its prompt.
B. Lower the overall reporting threshold so fewer findings surface across all categories.
C. Switch to a larger model for the performance pass only.
D. Post all findings but mark performance ones as "low confidence."
Q30. (Structured Data Extraction) Your extractor returns well-formed JSON that validates against the schema, but on 8% of invoices the line items do not sum to the stated total. What addresses this?
A. Enable strict mode on the tool schema.
B. Extract calculated_total alongside stated_total and flag discrepancies in a validation step.
C. Add additionalProperties: false to the schema.
D. Switch tool_choice from "auto" to "any".
Q31. (Structured Data Extraction) Roughly a fifth of your source contracts have no governing-law clause. The model invents a plausible jurisdiction for those. The governing_law field is currently required. What is the fix?
A. Add a few-shot example showing a contract with no governing law.
B. Make governing_law optional/nullable so the model can honestly return null.
C. Add "do not guess" to the system prompt.
D. Lower the temperature to 0.
Q32. (Structured Data Extraction) You receive three document types and cannot tell which type a document is until you read it. You have a distinct extraction tool per type and must never get conversational prose back. Which tool_choice?
A. "auto"
B. "any"
C. {"type": "tool", "name": "extract_invoice"}
D. "none" with a prefilled response
Q33. (Structured Data Extraction) Validation fails because total_paid is absent. Investigation shows the figure lives in a payment remittance file that was never supplied to the model. What should the pipeline do?
A. Retry with the validation error appended; the model will locate it.
B. Retry up to five times with exponential backoff.
C. Recognize that retry cannot recover absent information, and route the document to human review.
D. Make the field nullable and accept null as correct.
Q34. (Structured Data Extraction) Select TWO. Which statements about the Message Batches API are accurate?
A. It offers 50% cost savings.
B. It guarantees completion within one hour.
C. It supports multi-turn tool calling within a single request.
D. custom_id correlates requests with responses.
E. It is appropriate for blocking pre-merge checks because it is usually fast.
Q35. (Structured Data Extraction) A batch of 100 documents completes with 7 failures, all because the documents exceeded context limits. What is the right recovery?
A. Resubmit the entire batch with a larger model.
B. Identify the 7 by custom_id, chunk them, and resubmit only those.
C. Process all 100 synchronously instead.
D. Reduce max_tokens and resubmit the whole batch.
Q36. (Claude Code for CI) Claude generates a module, then you ask it in the same session to review its own work. It finds nothing. What is the most effective architecture?
A. Enable extended thinking on the review request.
B. Ask it to review the code three times and take the union of findings.
C. Send the code to a second, independent Claude instance without the generator's reasoning context.
D. Add "be critical of your own work" to the review prompt.
Domain 5 — Context Management & Reliability
Q37. (Customer Support Resolution Agent) Forty turns into a billing dispute, the agent refers to "the disputed amount" and "the charge from last month" instead of $247.80 and 14 August. Summarization has been running throughout. What fixes it?
A. Truncate the history to the last ten turns and drop everything earlier.
B. Extract transactional facts — amounts, dates, order numbers, statuses — into a persistent case-facts block included in each prompt, outside the summarized history.
C. Instruct the summarizer to be more detailed.
D. Move to a model with a larger context window.
Q38. (Customer Support Resolution Agent) Your agent responds to "I've had enough, put me through to a person" by first running lookup_order and offering a replacement. What is correct?
A. Escalate immediately, without first attempting investigation.
B. Attempt one resolution; escalate if the customer repeats the request.
C. Run sentiment analysis to confirm the frustration is genuine before escalating.
D. Escalate only if the agent's confidence in resolving the case is below threshold.
Q39. (Multi-Agent Research System) A document analysis subagent times out after processing 3 of 5 papers. Which propagation approach best enables coordinator recovery?
A. Return structured error context: failure type, what was attempted, the 3 completed analyses as partial results, and possible alternative approaches.
B. Retry internally with backoff, then return "document analysis unavailable."
C. Return the 3 completed analyses marked as a successful, complete result.
D. Propagate the timeout to a top-level handler that aborts the research run.
Q40. (Multi-Agent Research System) Two credible sources report the market size as $4.2B and $6.8B. Your synthesis agent silently picks $4.2B. What should it do instead, and what would have prevented the misreading in the first place?
A. Average the two figures; require confidence scores from each source.
B. Pick the higher figure as a conservative upper bound; require source URLs.
C. Annotate both values with source attribution and let the report present the conflict; require publication or data-collection dates so temporal differences are not mistaken for contradictions.
D. Escalate to a human reviewer; require the coordinator to deduplicate sources before synthesis.
Additional items — under-drilled task statements
Q41. (D4 · Claude Code for CI) Your review criteria are specific and well-defined, and precision is good on clear-cut cases. But on borderline code — a broad catch that is arguably intentional, a magic number that may be a domain constant — the bot's verdicts swing run to run. What most improves consistency?
A. Add "when uncertain, do not report the issue" to the criteria.
B. Add 2–4 few-shot examples of ambiguous cases showing the reasoning for why one verdict was chosen over the plausible alternative.
C. Enumerate every borderline pattern in the codebase as an explicit exception list.
D. Increase the number of review passes and report only findings that recur.
Q42. (D5 · Developer Productivity with Claude) Three hours into exploring a legacy payments service, the agent starts answering questions with "typically, services like this validate at the controller layer" instead of naming the PaymentValidator class it identified earlier. What is happening, and what fixes it?
A. The system prompt has been diluted; restate it every ten turns.
B. Context degradation in an extended session; have the agent maintain a scratchpad file recording key findings and reference it for subsequent questions.
C. The model is hallucinating; lower the temperature.
D. Tool results were malformed; add a PostToolUse normalization hook.
Q43. (D5 · Structured Data Extraction) Your extraction pipeline reports 97% overall accuracy and you want to cut human review. What must you verify first?
A. That the 3% of errors are evenly distributed across reviewers.
B. That accuracy is consistent when segmented by document type and by field, since an aggregate figure can mask poor performance on specific segments.
C. That the model's self-reported confidence exceeds 0.9 on the 97%.
D. That the validation set is at least 10,000 documents.
Q44. (D5 · Structured Data Extraction) Having reduced review to low-confidence extractions only, how do you keep detecting new error patterns in the high-confidence bucket you no longer inspect?
A. Wait for downstream systems to report anomalies.
B. Stratified random sampling of high-confidence extractions for ongoing error-rate measurement and novel pattern detection.
C. Re-run every high-confidence extraction with a second model and compare.
D. Raise the confidence threshold each quarter.
Q45. (D2 · Developer Productivity with Claude) Your internal MCP server fronts a documentation system with 4,000 pages across a deep hierarchy. Agents burn six or seven exploratory search_docs calls just working out what topics exist before asking a useful question. What is the appropriate MCP mechanism?
A. Increase the result count returned by search_docs.
B. Expose the documentation hierarchy as an MCP resource so agents can see the content catalog without exploratory tool calls.
C. Add a list_topics tool and instruct the agent to always call it first.
D. Cache search_docs results so repeat exploration is cheap.
---
Answers
Q1 — B. Loop control flow must be driven by stop_reason. The guide names "checking for assistant text content as a completion indicator" as an anti-pattern outright: a tool_use response can and often does also contain text. A treats a structural bug as a prompting problem; C substitutes an arbitrary iteration count for a real signal; D still uses the wrong signal, just more carefully.
Q2 — C. Subagents operate with isolated context and do not inherit the coordinator's conversation history. The coordinator must include complete findings from prior agents directly in the synthesis prompt. B would work around the problem by making synthesis re-do the research — over-provisioning and violating separation of concerns. D misunderstands the mechanism; there is no shared turn context.
Q3 — A. Parallel execution means emitting multiple Task tool calls in a single coordinator response. B invents a field. C is exactly the sequential pattern. D names a real Messages API parameter but it governs parallel tool calls, not subagent orchestration — and it appears nowhere in the guide's API appendix.
Q4 — C. Deterministic compliance is required — the consequence is financial — and prompt instructions have a non-zero failure rate. Tool-call interception blocks the action before it happens. A and B are both probabilistic. D is the right hook family but the wrong direction: PostToolUse fires after execution, so the refund has already gone out.
Q5 — A. PostToolUse hooks intercepting tool results for transformation before the model processes them is the guide's named pattern for exactly this — normalizing heterogeneous formats from different MCP tools. B is probabilistic and burns tokens on every turn. C may be impossible for third-party servers and is more work. D blocks useful tools rather than fixing their output.
Q6 — B. Resume when prior context is mostly valid, and inform the resumed session about specific file changes for targeted re-analysis rather than full re-exploration. A wastes the benefit and risks stale reasoning. C solves a different problem (divergent branches). D overstates: the guide prefers a fresh session with a structured summary only when prior tool results are stale, which six changed files out of a whole codebase does not necessarily make everything.
Q7 — B. fork_session creates independent branches from a shared analysis baseline for exploring divergent approaches — precisely the stated use case. A pollutes one session with two strategies. C compresses, it doesn't branch. D discards the detailed analysis you already paid for.
Q8 — B. The coordinator's role includes deciding which subagents to invoke based on query complexity, dynamically selecting rather than always routing through the full pipeline. A degrades quality without addressing the structure. C and D are narrower optimizations that leave the routing problem intact.
Q9 — B. Open-ended investigation calls for dynamic adaptive decomposition: map structure, identify high-impact areas, build a prioritized plan that adapts as dependencies are discovered. This is the guide's own worked example for "add comprehensive tests to a legacy codebase." A imposes a fixed chain on an unpredictable task. C guarantees duplication and no coherent prioritization. D invites attention dilution.
Q10 — B. Decompose multi-concern requests into distinct items, investigate each in parallel using shared context, then synthesize a unified resolution. A and C push work back onto the customer or a human for a case the agent can handle. D is serial and slow, and risks the same drop-off.
Q11 — B and D. The guide specifies structured handoff summaries containing customer ID, root cause, refund amount, and recommended action, for human agents who lack transcript access. A is the raw dump the pattern exists to replace. C is an unreliable signal. E is operational telemetry, not what the human needs to act.
Q12 — B. Tool descriptions are the primary mechanism for tool selection; the guide's own repair for this exact pair is renaming and rewriting to eliminate functional overlap. A (consolidation) is a defensible architecture but more work than a first step. C over-engineers and bypasses the model's language understanding. D forces a tool call without improving which tool.
Q13 — B. Structured error metadata — category, retryable flag, human-readable description of what was attempted — is what lets the agent make an appropriate recovery decision. A is the named anti-pattern ("Operation failed"). C silently suppresses a failure as success. D terminates the workflow over a recoverable fault.
Q14 — B. Business errors are non-retryable and need a customer-friendly explanation so the agent can communicate rather than retry. A would cause pointless retries of a policy decision. C throws away the reason. D conflates a policy refusal with an empty result set.
Q15 — B. Too many tools degrades selection, and out-of-specialization tools get misused. The guide's pattern is scoped tool access plus a limited cross-role tool for a specific high-frequency need. A is probabilistic. C removes a genuine capability the agent needs 85% of the time. D misdiagnoses the cause.
Q16 — C. The named skill is enhancing MCP tool descriptions to explain capabilities and outputs in detail, precisely to prevent the agent from preferring built-in tools like Grep. A confuses scope with priority — scope controls sharing, not selection. B cripples a useful tool. D forces a call without steering which.
Q17 — B. Project-scoped .mcp.json for shared team tooling, with environment variable expansion for credentials so nothing secret is committed. A is user scope and leaks the token. C loses the sharing that motivated the change. D reinvents what env var expansion already does.
Q18 — B. Grep searches file contents for patterns such as function names. Glob matches file paths, which finds the files but not the callers. C is the read-everything-upfront anti-pattern. D shells out where a dedicated tool exists.
Q19 — B. User-level ~/.claude/CLAUDE.md applies only to that user and is not shared via version control — the guide names this exact diagnostic. A would still be committed and would still load (a rule without paths: just isn't conditionally scoped). C is contradicted by "ignores them entirely." D doesn't match "follows them perfectly for you" without manual invocation.
Q20 — C. .claude/rules/ with glob patterns applies conventions by file type regardless of directory location — essential when test files are spread throughout. A is directory-bound and can't cover scattered tests. B relies on inference rather than explicit matching. D requires invocation and is not automatic.
Q21 — C. context: fork runs the skill in an isolated sub-agent context, preventing verbose skill output from polluting the main conversation. A restricts tools, not context. B prompts for arguments. D is not one of the frontmatter options the guide tests, and a cheaper model would still emit 8,000 tokens.
Q22 — B. Skills are on-demand invocation for task-specific workflows; CLAUDE.md is always-loaded universal standards. A inverts it. C is false — personal skills live in ~/.claude/skills/. D is not a trade-off the guide draws.
Q23 — B. Plan mode is for large-scale changes with multiple valid approaches and architectural implications; combining plan mode for investigation with direct execution for implementation is a named skill. A assumes you already know the answer. C ignores that the complexity is stated in the requirements, not something that might emerge. D loses the cross-file coherence the migration needs.
Q24 — A. -p / --print is the documented non-interactive mode. CLAUDE_HEADLESS and --batch do not exist. Redirecting stdin is a Unix workaround that doesn't address the command syntax.
Q25 — A. --output-format json with --json-schema produces machine-parseable structured findings for automated posting as inline PR comments. B is unenforced. C is fragile. D breaks the guide's pairing in half — it only ever names the two flags together.
Q26 — B. Include prior review findings in context and instruct Claude to report only new or still-unaddressed issues. A abandons the feature. C hides the symptom and breaks when wording shifts slightly. D loses issues in unchanged files that interact with the change.
Q27 — B. Concrete input/output examples are the guide's named remedy when prose descriptions are interpreted inconsistently, and it specifies 2–3 of them. A changes where the ambiguity lives, not whether it's ambiguous. C is the interview pattern, aimed at surfacing unknown considerations rather than pinning down a known transformation. D is more of what already failed.
Q28 — B. Explicit categorical criteria beat vague instructions — this is the guide's own before/after pair. A is named as a technique that fails to improve precision. C relies on poorly calibrated self-reported confidence. D suppresses genuine intermittent findings (the reasoning the guide uses to reject 2-of-3 consensus in sample question 12).
Q29 — A. Temporarily disabling high false-positive categories to restore developer trust while improving those prompts is a named skill. High false positives in one category undermine confidence in the accurate ones, so protecting trust is the priority. B degrades the good category too. C treats a criteria problem as a capability problem. D still spends the developer's attention.
Q30 — B. Schemas eliminate syntax errors but not semantic ones; the guide's named self-correction flow is extracting calculated_total alongside stated_total to flag discrepancies. A, C and D are all schema-level, and the JSON already validates.
Q31 — B. Designing fields as optional/nullable where source documents may not contain the information is the guide's stated way to prevent the model fabricating values to satisfy required fields. A helps at the margin but leaves the structural pressure in place. C is a prompt instruction against a schema-level incentive. D is not a lever the guide tests and wouldn't stop the fabrication.
Q32 — B. tool_choice: "any" guarantees the model calls a tool while letting it choose which — the guide's named use for exactly this case (multiple extraction schemas, unknown document type). A permits a text reply. C forces the wrong schema two thirds of the time. D disallows tools entirely.
Q33 — C. Retries are ineffective when the required information is simply absent from the source; the skill tested is identifying when retry will not help. A and B burn cost on an unrecoverable case. D would silently record a null for a value that exists — wrong, and it hides a real data-pipeline gap.
Q34 — A and D. 50% cost savings and custom_id correlation are both stated. B is wrong — up to 24 hours with no guaranteed latency SLA. C is wrong — batch does not support multi-turn tool calling within a request. E is the reasoning sample question 11 explicitly rejects.
Q35 — B. Handle batch failures by resubmitting only the failed documents, identified by custom_id, with appropriate modifications such as chunking oversized documents. A, C and D all reprocess 93 documents that already succeeded.
Q36 — C. A model retains its reasoning context from generation and is less likely to question its own decisions in the same session; independent review instances are more effective than self-review instructions or extended thinking. A is named as inferior. B is still self-review, three times. D is a self-review instruction.
Q37 — B. Extracting transactional facts into a persistent case-facts block included in each prompt, outside the summarized history, is the guide's named fix for progressive summarization condensing numbers and dates. A discards exactly the early transactional detail the customer will hold you to, and the guide is explicit that complete conversation history matters for coherence. C asks the summarizer to stop being a summarizer. D is a capacity answer to a curation problem.
Q38 — A. Honour explicit customer requests for a human immediately, without first attempting investigation. B is the correct behaviour for frustration without an explicit request — the customer here asked. C and D are the two named unreliable proxies.
Q39 — A. Structured error context — failure type, what was attempted, partial results, potential alternatives — lets the coordinator decide whether to retry, substitute, or proceed with coverage annotations. B's generic status hides that context. C is silent suppression, the named anti-pattern. D terminates the whole workflow over one recoverable failure.
Q40 — C. Conflicting statistics from credible sources should be annotated with source attribution rather than arbitrarily resolved, and requiring publication or data-collection dates in structured outputs prevents temporal differences from being misread as contradictions. A fabricates a figure neither source reported. B is arbitrary selection with a rationalization. D escalates a case the system is designed to represent, and deduplication doesn't address genuinely different measurements.
Q41 — B. Few-shot examples are the guide's named technique for demonstrating ambiguous-case handling, and it specifies 2–4 targeted examples that show the reasoning for why one action was chosen over a plausible alternative. A is a "be conservative"-class instruction, which the guide says fails to improve precision. C doesn't generalize to novel patterns — the exact benefit few-shot is credited with. D suppresses genuine intermittent findings.
Q42 — B. The guide describes context degradation in extended sessions as models "giving inconsistent answers and referencing typical patterns rather than specific classes discovered earlier" — this stem is that symptom verbatim. The named remedy is scratchpad files persisting key findings across context boundaries. A, C and D all misdiagnose a context problem as a prompting, sampling, or data-format problem.
Q43 — B. Aggregate accuracy metrics can mask poor performance on specific document types or fields; the guide requires analyzing accuracy by document type and field before reducing human review. A misreads whose errors matter. C leans on uncalibrated confidence. D is volume without segmentation, which is exactly what hides the problem.
Q44 — B. Stratified random sampling of high-confidence extractions is the named technique for ongoing error-rate measurement and novel pattern detection. A is reactive and slow. C costs as much as the review you just eliminated. D trades recall for a false sense of safety without ever looking at what's in the bucket.
Q45 — B. MCP resources expose content catalogs — the guide's own examples are issue summaries, documentation hierarchies, and database schemas — specifically to reduce exploratory tool calls. Remember the split: resources for content catalogs, tools for actions. A returns more noise per call. C adds a tool where a resource is the designed mechanism. D makes an unnecessary pattern cheaper rather than removing it.
Scoring yourself
Out of 45. Below 34 and you have real gaps — go back to the domain page for whichever section you missed most. Above 39 and you are in good shape; spend remaining time on the domain pages' "ten things to have cold" lists and on the twelve official sample questions.
Quick Reference — final-hour review
One page. Read this on the morning of the exam.
Exam mechanics
60 items · 120 min (2 min/item) · 4 of 6 scenarios · pass 720/1000 scaled · multiple-choice and multiple-response — read how many to select.
D1 27% · D2 18% · D3 20% · D4 20% · D5 15%.
- Deterministic beats probabilistic when the consequence is real. Money, compliance, security, identity, required ordering → prerequisite gate or hook. Never a system prompt instruction, never few-shot examples.
- But pick the proportionate fix. The guide repeatedly rewards the lowest-effort fix that addresses the root cause and rejects "over-engineered" options — routing classifiers, trained models, extra infrastructure — when a description rewrite or explicit criteria would do.
- Fix the root cause, not the symptom. If all downstream components work, look upstream. If descriptions are thin, fix descriptions rather than papering over with examples.
- Structure beats instruction for anything that must survive a handoff. Provenance, error context, case facts, escalation summaries — make them structured fields, not prose the next step must re-derive.
- A bigger model or bigger context window is essentially never the answer.
Words in the stem that decide the answer
| Phrase |
Points to |
| "must never", "always before", "guaranteed" |
Programmatic enforcement (gate / hook) |
| "most effective first step" |
Lowest-effort root-cause fix |
| "developers wait for the result", "blocking", "pre-merge" |
Synchronous API, not batch |
| "overnight", "weekly", "nightly" |
Batch API |
| "spread throughout the codebase", "regardless of location" |
.claude/rules/ glob |
| "available to every developer on clone" |
.claude/commands/ (project) |
| "multiple valid approaches", "architectural" |
Plan mode |
| "single-file", "clear stack trace" |
Direct execution |
| "the same session that generated it" |
Independent review instance |
| "inconsistent depth", "contradictory across files" |
Per-file passes + integration pass |
| "customer asks for a human" |
Escalate immediately |
| "policy is silent on" |
Escalate — policy gap |
| "multiple matches returned" |
Ask for another identifier |
| "values don't sum", "wrong field" |
Semantic validation, not a stricter schema |
| "model fabricated a value" |
Make the field nullable/optional |
| "information isn't in the document" |
Retry won't help — human review |
| "job hangs waiting for input" |
-p |
| "verbose output pollutes the conversation" |
context: fork / Explore subagent |
Fixed facts
stop_reason — "tool_use" → continue the loop · "end_turn" → terminate. Nothing else terminates a loop for what's tested here. (Current API note: also has stop_sequence and, on Claude 4.5+, model_context_window_exceeded — not part of this guide's tested set.)
Spawning subagents — the Task tool; allowedTools must include "Task"; parallel = multiple Task calls in one response; subagents inherit nothing. (Current SDK renamed this the Agent tool, with Task kept as an alias — exam material still says Task.)
tool_choice — "auto" may return text · "any" must call some tool · {"type":"tool","name":"x"} forces one. (Current API also has {"type":"none"}, forcing text-only — not part of this guide's tested set.)
Error categories — transient · validation · business · permission. Transient is the retryable one; business violations carry retriable: false plus a customer-friendly explanation. Payload: errorCategory, isRetryable, human-readable description. MCP flag is isError.
Empty result ≠ error. Timeout = error. Zero matches = success.
Tool count — ~4–5 per agent; 18 degrades selection.
Batch API — 50% off · ≤24h · no SLA · no multi-turn tool calling · custom_id correlates and drives selective resubmission.
CI flags — -p / --print · --output-format json · --json-schema. (CLAUDE_HEADLESS and --batch don't exist.)
File and directory map
~/.claude/CLAUDE.md user-level, NOT shared
~/.claude/commands/ personal slash commands
~/.claude/skills/ personal skills (rename to avoid clashing)
~/.claude.json user-scope MCP servers (personal/experimental)
CLAUDE.md or .claude/CLAUDE.md project-level, committed
<subdir>/CLAUDE.md directory-scoped conventions
.claude/rules/*.md topic files; YAML `paths:` globs for conditional loading
.claude/commands/*.md team slash commands, version-controlled
.claude/skills/<name>/SKILL.md team skills
.mcp.json project-scope MCP servers, ${ENV_VAR} for secrets
SKILL.md frontmatter tested: context: fork · allowed-tools · argument-hint.
Commands used: /memory (browse/edit CLAUDE.md file locations) · /context (what's actually loaded this session) · /compact · --resume <session-name> · fork_session.
Current-product note (beyond this file/directory map): a managed/enterprise policy CLAUDE.md level and a project-root CLAUDE.local.md (gitignored, personal-but-project-scoped) also exist, and Claude Code MCP config now has a third local scope (per-project, private, not git-tracked) alongside project/user above. Not part of this guide's tested hierarchy.
Read · Write · Edit · Bash · Grep · Glob — all six are named in task 2.5.
Grep = search file contents. Glob = match file paths/names. Read/Write = whole file. Edit = targeted, needs unique anchor text → on failure fall back to Read + Write. Bash exists, but don't shell out where a dedicated tool does the job.
Exploration order: Grep for entry points → Read to follow imports. Not read-everything-upfront.
Confidence scores — the one distinction that trips people
| Use |
Verdict |
| Agent self-reports confidence and auto-escalates below a threshold |
Wrong (5.2) — poorly calibrated, overconfident on hard cases |
| Field-level confidence, calibrated against a labeled validation set, used to prioritize limited human reviewers |
Right (5.5, 4.6) |
Few-shot vs explicit criteria vs enforcement
| The problem is… |
The fix is… |
| The rule doesn't exist / is vague |
Explicit categorical criteria |
| The rule exists but ambiguous cases go wrong |
2–4 few-shot examples with reasoning |
| The rule exists, is clear, and must never be violated |
Programmatic gate or hook |
Escalation triggers
✅ customer requests a human (immediately, no investigation) · policy exception or gap · inability to make progress
❌ sentiment analysis · self-reported confidence · "the case seems complex" · turn count
Provenance checklist for multi-source synthesis
Structured claim-source mappings (URL, document name, excerpt) preserved through every hop · publication/collection dates required · conflicts annotated with attribution, never silently resolved · report sections separating well-established from contested · coverage annotations for gaps · content types rendered natively (tables for financials, prose for news, lists for technical).
Off the table
Fine-tuning and training custom models · Claude API auth, billing, account management · language- and framework-specific implementation detail · deploying or hosting MCP servers (infrastructure, networking, containers) · Claude's internal architecture and weights · Constitutional AI / RLHF · embedding models and vector DBs · computer use · vision · streaming / SSE · rate limits, quotas, pricing calculations · OAuth and key rotation · cloud provider config · performance benchmarking · prompt caching beyond knowing it exists · tokenization.
If an answer option depends on one of these, it is almost certainly a distractor.
Preface — from using Claude Code to building with Claude
There is a particular kind of gap this book exists to close. You can drive Claude Code well. You know what to put in a CLAUDE.md and what to leave out, when to reach for plan mode, how to write a slash command your team will actually use, which MCP servers earn their place in a project. You have watched Claude recover from a failing test a hundred times and formed accurate instincts about when it will and won't. None of that is small; most of the exam's judgment questions reward exactly those instincts.
What you have not done is build the thing. You have never sent a Messages API request, never written the twenty-line while loop that turns a stateless HTTP call into an agent, never authored an MCP server, never designed a JSON schema whose job is to stop a model fabricating a value. The exam assumes six months of that work. It asks you to reason about stop_reason and tool_result blocks and AgentDefinition and tool_choice — the machinery underneath the product you use daily, which the product exists precisely to hide from you.
That gap is smaller than it looks, and it is smaller in a specific, useful way: you have already operated every architecture the exam tests. You just operated it from the outside, as the user of something someone else assembled. Almost nothing in this book is a new idea for you. It is mostly the same ideas with the covers off and different names on them. The fastest way through is not to learn the material fresh but to attach each piece of vocabulary to the behaviour you have already watched a thousand times.
What you already know, and what it is called here
This table is the shortest path from your existing fluency to the exam's vocabulary. Read it now, and again after you finish the book — it reads differently the second time.
| What you do in Claude Code |
What the exam calls it |
Chapter |
| Press enter, watch it grep, read, edit, run tests |
The agentic loop, driven by stop_reason |
2 |
| The tool output indented in your transcript |
tool_result blocks in a user message |
2 |
| A Bash command fails and Claude adapts |
is_error: true on a tool result — the failure came back as readable content, not an exception |
4 |
| Your CLAUDE.md |
The system parameter, and a three-level configuration hierarchy |
2, 9 |
| Approving or denying a permission prompt |
A human-in-the-loop gate between the model asking and your code executing; PreToolUse interception |
2, 6 |
| Installing an MCP server from a config file |
Tool definitions crossing the API boundary; .mcp.json project scope vs ~/.claude.json user scope |
3, 8 |
| Claude reaching for the wrong one of two similar tools |
Tool descriptions as the primary selection mechanism |
3 |
| Claude spawning subagents on its own |
Orchestrator-workers; the Task tool and AgentDefinition; isolated subagent context |
1, 5 |
| A subagent that cannot see your main conversation |
Subagents inherit nothing not written into their prompt |
5, 14 |
Running /compact and watching context shrink |
The messages array being rewritten; what survives summarization |
2, 14 |
| Plan mode |
A programmatic gate between steps of a chain |
1, 10 |
.claude/commands/ and .claude/skills/ |
Project vs user scope; SKILL.md frontmatter |
10 |
--resume on yesterday's session |
Session state, and tool results that were true when fetched |
7 |
| The Explore subagent |
Context isolation for verbose discovery |
10, 14 |
claude -p in a shell script |
Headless invocation for CI |
11 |
The right-hand column is the answer to "where do I go if this row is the one I am shaky on." The left-hand column is the thing to reach for whenever a chapter starts feeling abstract.
The four things you have never done
Being precise about the gap makes it easier to close. Four capabilities separate a fluent Claude Code user from the candidate the exam describes, and each has a chapter that owns it.
You have never written the loop. Chapter 2 is the most important chapter in the book for you, and the one to do the exercises for rather than read. It builds a working agent out of a stateless HTTP endpoint in about forty lines. Once you have written it once, half of Domain 1 turns from reasoning into recognition.
You have never authored a tool. You have installed plenty. Authoring inverts the problem: a tool description is not documentation for a human, it is a prompt competing for the model's attention against every other tool in the set. Chapter 3 covers that, and Chapter 8 makes you the author of an MCP server rather than its consumer.
You have never designed for structured output. Chapter 13 covers the mechanism the exam treats as the reliable one — tool_use with a JSON schema — and the distinction it cares most about, which is that a schema eliminates malformed JSON and does nothing whatever about line items that fail to sum.
You have never had to make a model's behaviour reliable at volume. Prompt engineering as a production discipline, rather than a matter of phrasing a request well, is Chapter 12: explicit categorical criteria over vague instruction, few-shot examples that show reasoning rather than just output, and the reason "be more conservative" never fixes a precision problem.
How the book is arranged
Fifteen chapters, each owning a set of the blueprint's thirty task statements outright, so every statement has exactly one home. Chapters 1 through 8 build the agent: patterns, the loop, tools, errors, orchestration, hooks, sessions, MCP. Chapters 9 through 11 are the Claude Code configuration surface, where you will move fastest — you know the behaviour and need the vocabulary and the exact file paths. Chapters 12 through 15 are prompting, structured output, context, and the reliability judgments that Domain 5 turns on.
Read in order if you have the time. If you do not, Chapter 2 is non-negotiable, Chapter 6 carries the single highest-yield idea on the exam, and Chapters 9 through 11 can be skimmed for the specific configuration details rather than read for argument.
Every chapter closes with a prose section naming the task statements it covers and the judgment calls the exam asks about them, followed by exercises. The exercises are where the book turns into experience, and for the four gaps above they are worth more than the prose.
Where the exam guide and the product disagree
One warning that will save you a confusing hour. The exam guide was published in July 2026 and Claude Code has moved since. In eight places it no longer matches Anthropic's current documentation. Three are wrong semantics — allowedTools gating subagent delegation, allowed-tools restricting a skill's tools, Read + Write as the fallback when Edit hits a non-unique match. Four have simply been overtaken: the Task tool's rename to Agent, --resume taking a session name, /memory reporting what is loaded, MCP having two scopes rather than three. One is cosmetic.
Your daily instincts will fight the answer key on exactly these points, and your instincts will be right about the product and wrong about the exam. Answer the guide's version. Where a chapter teaches something that no longer matches reality, it says so in a marked note immediately afterward, with the guide's answer left standing in the main text. The full list, with sources, is in REVIEW-FINDINGS.md at the repository root. Read it once before the exam.
Note also what those eight have in common: seven sit in the Claude Code and Agent SDK configuration surface, the fastest-moving part of the product. Domains 4 and 5 — prompting, structured output, context, escalation, provenance — have almost no version-sensitive surface and are unaffected. The material that feels most familiar to you is the material most likely to have drifted.
A note on the code
Every substantive example appears in both Python and TypeScript, Python first, using claude-sonnet-4-5 and the tool names from the exam's own scenarios — get_customer, lookup_order, process_refund, escalate_to_human, extract_invoice. The scenarios are worth taking seriously as a study aid: four of the six appear on your form, and the same mechanic tends to recur across them in different costumes. A deterministic gate on a refund in the support scenario is the same idea as a gate in a CI pipeline, and the exam will happily test it in whichever costume you are less comfortable with.
Chapter 1 — Agents, workflows, and knowing which one you need
Most production failures with Claude are not model failures. They are architecture failures wearing a model failure's clothes. A support agent refunds the wrong account, and the postmortem blames hallucination when the real defect was that identity verification was requested in a system prompt instead of enforced in code. A code review gives contradictory feedback across fourteen files, and someone proposes a larger context window when the fix is to stop asking one call to do fourteen jobs.
Each of these is a question about shape: how much of the control flow lives in your code, and how much lives in the model's judgement. Get it wrong in the direction of too little structure and you inherit non-deterministic behaviour on operations that needed guarantees. Get it wrong in the direction of too much structure and you have built a brittle state machine that breaks on the first input its author did not anticipate, at many times the token cost of the thing it replaced.
The Claude Certified Architect – Foundations exam is built almost entirely out of that judgement. Domain 1 carries 27% of the item weight — the heaviest of the five — and its questions are diagnostic rather than factual. You are handed a system that is misbehaving in a specific, described way and asked which change fixes it. The distractors are rarely wrong in the abstract; they are wrong because they solve a different problem, or because they add machinery this failure does not require. This chapter builds the vocabulary and the decision procedure that the remaining chapters assume.
One orientation note first. If you use Claude Code daily, you have already operated every architecture in this chapter — from the outside, as a user of a product someone else assembled. Claude Code is itself an agent built on the primitives the exam asks about: a loop, a tool set, a system prompt, and a set of gates. The work here is to turn those intuitions inside out, so the thing you have been driving becomes the thing you can specify. Where a bridge exists, I name it explicitly. The vocabulary comes from Anthropic's own writing on agent design, which the blueprint tracks closely — the workflow-versus-agent distinction and the pattern catalogue from "Building effective agents," the loop from the Agent SDK engineering work.
The augmented LLM
The base building block of every agentic system is the augmented LLM: a model extended with tools, retrieval, and memory. Not the model alone — the model plus the affordances that let it reach outside its own context and change something.
Be precise about what "the model alone" means, because the exam's framing rests on that boundary. Underneath every product you have used sits the Messages API: a single stateless call taking a system prompt, a list of alternating user and assistant messages, and optionally a list of tool definitions, returning one assistant message. It has no memory of the call before it. It cannot run anything or read a file. Every capability you associate with Claude Code — reading your repository, editing files, running your tests, remembering what it did four steps ago — is scaffolding its authors built around that stateless call. The augmentations are that scaffolding, and this book is largely about building it yourself.
Each augmentation is a decision surface. Tools decide what the model can do and, through their descriptions, what it will choose to do — Chapter 3 is entirely about the fact that a tool description is a prompt, not documentation. If you have installed an MCP server and watched Claude Code reach for the wrong one of its tools, you have seen a description underperform as a prompt. Retrieval decides what the model knows at the moment it acts, and whether that knowledge arrives pre-fetched or fetched on noticing a gap: the difference between your CLAUDE.md, injected before Claude does anything, and the file Claude decides to open on turn six. Memory decides what survives between turns and sessions — and once you have subagents, what does not. Subagents inherit nothing unless you put it in the prompt, which is why a Claude Code subagent cannot see your main thread: it gets a fresh context window and whatever text the parent handed it.
The exam's six scenarios are all augmented LLMs with different augmentation profiles, and it is worth reading each as a statement about which augmentation carries the weight. The support agent's four MCP tools are the visible part, but what gates the refund is memory of a verified customer ID. The research system's hard problem is also memory: what the coordinator carries between subagents. The extraction system barely needs tools and lives or dies on schema design.
Workflows and agents
Anthropic draws the line precisely, and the wording matters because exam options are written against it.
A workflow is a system where LLMs and tools are orchestrated through predefined code paths. You wrote the sequence. The model fills in the steps; your code decides what the steps are and in what order they run.
An agent is a system where the LLM dynamically directs its own process and tool usage, maintaining control over how it accomplishes the task. You wrote the goal and handed over a toolbox. The model decides what to do next based on what it just learned.
Agentic systems is the umbrella covering both. When a question says "agentic system," it is not implying autonomy — it is refusing to prejudge the answer.
Claude Code makes the contrast concrete. When you type a bare request and let it run, you are using an agent: no one wrote down that it should grep, then read three files, then edit one, then run the tests. It chose that sequence from what it found. When you invoke a slash command whose body specifies the steps in order, you have written a workflow, and the model is filling in the content of steps whose sequence you fixed in a markdown file. Same model, same tools, different location for the control flow. That is the entire distinction.
It is architectural, not a spectrum of intelligence, and the exam probes it directly in task statement 1.1: the difference between model-driven decision-making, where Claude reasons about which tool to call next from context, and pre-configured decision trees or fixed tool sequences. Both are legitimate. Neither is the default correct answer. What decides between them is whether you can enumerate the paths in advance.
The agentic loop
Anthropic's framing of the loop, from the Agent SDK work, has three phases that repeat: gather context → take action → verify work → repeat.
Gathering context is the agent fetching what it needs rather than being handed everything upfront. Claude Code's use of the filesystem is the canonical demonstration — grep and tail against a log file instead of reading the whole thing into the window — and you have watched it happen hundreds of times. Taking action is the model calling a tool that changes something. Verifying is checking the result against the goal.
Verification is the phase teams skip, and skipping it is what separates a demo from a system. Not from laziness: in most domains it is expensive or impossible to automate. No cheap oracle tells you whether a summary is good, whether a research report has coverage gaps, whether a customer email struck the right tone. So the loop degrades into gather-act-gather-act, and errors compound silently because nothing is looking for them.
This is why coding is the canonical agent domain, and why Claude Code was the first of these products to work well. Code ships with free, fast, deterministic verifiers: the compiler, the type checker, the test suite. An agent that writes a function can run the tests, read a real failure, and try again — a closed loop with a ground-truth signal and no human in it. So when you design outside coding, the highest-value question is what is my test suite? Sometimes it is a JSON Schema validator (Chapter 13). Sometimes it is a second model call against an explicit rubric, which is the evaluator-optimizer pattern below. Sometimes no verifier exists, and that is your signal to keep the design a workflow with human review rather than an autonomous agent.
Mechanically the loop is a while statement you write yourself, and it is smaller than you expect. Because the Messages API is stateless, you own the conversation: a list of messages you append to and resend in full on every iteration, each response carrying a stop_reason that tells your code whether Claude is finished or is asking you to run a tool on its behalf. Everything Claude Code does is that loop, wrapped in a permission system and a terminal UI. Chapter 2 builds it line by line, with the three anti-patterns the blueprint names. For now hold onto the shape: termination is a protocol signal, not a judgement call.
The five workflow patterns
Anthropic catalogues five workflows plus the autonomous agent. Each has a shape and one condition that makes it the right choice. Learn the condition; the shape follows.
Prompt chaining decomposes a task into a fixed sequence of calls, each processing the previous one's output, with programmatic gates between steps that validate intermediate results before allowing the chain to continue. The gate is the load-bearing part and the part people leave out. A chain without gates is just a long prompt split across several calls; a chain with gates catches a malformed extraction at step two instead of propagating it to step five. Plan mode is a gate you already use: Claude produces a plan, execution stops, and nothing proceeds until it clears a check. Yours will usually be code rather than a human keystroke, but the structural role is identical. Choose prompt chaining when the subtasks are fixed and knowable in advance and you are trading latency for per-step accuracy. The blueprint's own example is a large code review split into per-file passes plus a separate cross-file integration pass.
Routing classifies an input and directs it to a specialized follow-up. Choose it when inputs fall into distinct categories better served by different prompts, models, or toolsets, and when classification is accurate enough that misrouting is rare and recoverable. It is the same trade you make picking a smaller model for a mechanical edit and a larger one for a design question, except a classifier decides instead of you.
Parallelization runs multiple calls concurrently and aggregates, in two variations worth distinguishing. Sectioning splits a task into independent subtasks that run at once — one instance handling the user query while another screens for policy violations — and is right when no subtask needs another's output. Voting runs the same task several times with varied prompts and aggregates for confidence, which suits vulnerability review and anywhere recall matters more than precision. Note the trap: voting raises recall by flagging whatever any run finds, so a consensus rule requiring two of three runs to agree suppresses real findings rather than filtering false ones.
Orchestrator-workers has a central model break a task into subtasks at runtime, delegate them to workers, and synthesize the results. This is what happens every time Claude Code decides on its own to spawn subagents: you asked one question, and something in the middle chose how many investigators to launch and what each should look for, then stitched their answers together. It looks like parallelization and is not.
The discriminator is whether the subtasks are knowable in advance. If you can write the list down before seeing the input, you have parallelization and the decomposition belongs in your code. If the list depends on what the input turns out to contain — how many files the change touches, which subtopics the question fans out into — you have orchestrator-workers, and a model has to do the decomposing. Everything in Domain 1 about coordinators and subagents is this pattern, including its signature failure: when every worker succeeds and the aggregate output is still wrong, the defect is upstream in the decomposition. You have probably seen this from the user side — every subagent returns a confident, correct answer about the wrong three files.
Evaluator-optimizer pairs a generator with a critic in a loop, iterating until a quality bar is met. Choose it when you have clear evaluation criteria and when iteration measurably improves the output — literary translation, search over a hard query space. It fails circularly when no reliable criterion exists, because the evaluator cannot tell good output from bad and the loop just burns tokens. This is the verification phase promoted to a first-class architectural component.
The autonomous agent is the sixth option: a goal, a toolbox, and a loop, with the model deciding each step from what it has learned. Choose it when the number of steps is unpredictable, when you cannot draw the flow in advance, and when the environment gives feedback the model can act on. Agents trade cost and latency for adaptability, so they need guardrails — sandboxed execution, checkpoints, human review at consequential boundaries. Claude Code's permission prompts are those guardrails made visible, and when you build your own agent nobody supplies them for you.
Choosing
Four questions, in order.
Can you draw the decision tree? If you can sketch the flow on a whiteboard and it stays finite, build the workflow. Code that you can read is cheaper to debug than a model's reasoning trace, and it does not vary between runs.
Are the subtasks predictable? This settles parallelization versus orchestrator-workers, and it settles fixed pipelines versus dynamic decomposition. Predictable means you can enumerate them before seeing the input, not that they feel routine.
Is the task worth the extra tokens? An agent resends its whole accumulated message list on every iteration, so spend grows super-linearly in the number of steps, and fanning work across subagents multiplies that again. No exam item will ask you for a specific multiplier, but the shape of the trade is examinable: more steps and more agents cost more than either alone. Fine for a two-hour refactor, absurd for a classification you could route in a single call.
What does an error cost, and would you notice? Cost and discoverability are separate axes, and discoverability is the one people forget. A wrong refund is expensive and instantly visible; a research report with a silent coverage gap is cheap per incident and invisible until a decision is made on it. Low discoverability argues for structure and explicit verification even when the per-error cost looks small.
Above all four sits Anthropic's own guidance: find the simplest solution that works, and add complexity only when it demonstrably improves outcomes. Often the right answer is not an agentic system at all — a single well-specified call with good examples beats a multi-agent architecture more often than architects like to admit.
Task statement 1.6, concretely
Task statement 1.6 — design task decomposition strategies for complex workflows — is where this chapter becomes directly examinable, and it is framed as exactly one choice: fixed sequential pipelines (prompt chaining) versus dynamic adaptive decomposition based on intermediate findings.
|
Prompt chaining |
Dynamic decomposition |
| Subtasks known before you start |
Yes |
No |
| Who decomposes |
Your code |
The model, at runtime |
| Blueprint's example |
Per-file review passes plus a cross-file integration pass |
"Add comprehensive tests to a legacy codebase" |
| Cost of getting it wrong |
Rigid: unanticipated input falls through |
Coverage gaps from a narrow initial split |
| Failure to look for |
Missing gates between steps |
Orchestrator's decomposition, not the workers |
The legacy-tests example repays attention because it shows what "adaptive" actually means in practice: map the structure first, identify high-impact areas from what the mapping revealed, then build a prioritized plan that keeps adapting as dependencies surface. You could not have written that plan up front, because the plan is a function of the codebase. It is the same reason you point Claude Code at a repository and ask it to investigate rather than handing it a numbered list of files.
The same task, two ways
Triaging a support ticket. The workflow version encodes the path: classify, then fetch exactly the records that classification implies, then draft. Three calls, one shape, no surprises. Each helper below wraps a single Messages API call; the control flow between them is ordinary code that you wrote.
Python
async def triage_workflow(ticket: str) -> Resolution:
# Step 1 — classify. Forced tool call guarantees structured output.
category = await classify_ticket(ticket) # "billing" | "shipping" | "account"
# Programmatic gate: unroutable tickets never reach the drafting step.
if category.confidence < 0.7:
return escalate_to_human(ticket, reason="low_confidence_classification")
# Step 2 — predetermined context fetch per category.
if category.name == "billing":
context = await get_customer(category.customer_id)
elif category.name == "shipping":
context = await lookup_order(category.order_id)
else:
context = await get_customer(category.customer_id)
# Step 3 — draft against fetched context only.
return await draft_resolution(ticket, context)
TypeScript
async function triageWorkflow(ticket: string): Promise<Resolution> {
const category = await classifyTicket(ticket);
if (category.confidence < 0.7) {
return escalateToHuman(ticket, "low_confidence_classification");
}
const context =
category.name === "shipping"
? await lookupOrder(category.orderId)
: await getCustomer(category.customerId);
return draftResolution(ticket, context);
}
The agent version encodes the goal and the constraints, then loops on stop_reason until Claude decides it is finished. Read it as the smallest honest version of what Claude Code runs: the message list is the conversation, TRIAGE_TOOLS stands in for the tool definitions a .mcp.json entry ultimately becomes at the API boundary, and SYSTEM plays the role CLAUDE.md plays for you. TRIAGE_TOOLS is shorthand for a list of tool definitions — name, description, input_schema — not the bare functions; Chapter 2 shows their real shape.
Python
TRIAGE_TOOLS = [get_customer, lookup_order, process_refund, escalate_to_human]
SYSTEM = """Resolve the customer's ticket. Verify the customer's identity with
get_customer before any financial operation. Escalate when the request falls
outside documented policy or the customer asks for a human."""
async def triage_agent(ticket: str) -> Resolution:
messages = [{"role": "user", "content": ticket}]
while True:
response = await client.messages.create(
model="claude-sonnet-5",
system=SYSTEM,
tools=TRIAGE_TOOLS,
messages=messages,
max_tokens=2048,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
return Resolution.from_content(response.content)
results = [await execute_tool(block) for block in response.content
if block.type == "tool_use"]
messages.append({"role": "user", "content": results})
TypeScript
async function triageAgent(ticket: string): Promise<Resolution> {
const messages: MessageParam[] = [{ role: "user", content: ticket }];
for (;;) {
const response = await client.messages.create({
model: "claude-sonnet-5",
system: SYSTEM,
tools: TRIAGE_TOOLS,
messages,
max_tokens: 2048,
});
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason === "end_turn") {
return resolutionFrom(response.content);
}
const toolResults = await Promise.all(
response.content
.filter((b): b is ToolUseBlock => b.type === "tool_use")
.map(executeTool),
);
messages.push({ role: "user", content: toolResults });
}
}
Note what changed and what did not. The tools are the same. The model is the same. What moved is the decomposition: out of if statements and into the model's judgement. That buys handling for the ticket that is a billing dispute and a shipping complaint, which the workflow cannot express without a new branch. It costs you determinism on the ordering, which is why the identity-verification rule in that system prompt is not sufficient — prompt instructions have a non-zero failure rate, the same way a CLAUDE.md instruction is usually but not always honoured. A rule with financial consequences needs a programmatic prerequisite gate or an interception hook: the server-side analogue of a Claude Code hook that refuses an action outright rather than asking politely. That is Chapter 6's subject, and the single highest-yield idea on the exam.
The right production answer for this scenario is usually neither pure form: an agent loop for the investigative middle, with hard gates around the operations that must not go wrong. Patterns compose. Routing feeds a chain; an orchestrator wraps evaluator-optimizer workers. Reach for a composite only once a single pattern has demonstrably failed.
What the exam tests
Task statement 1.6 is the direct target of this chapter, and it is narrower than the full pattern catalogue: you are asked to choose between fixed sequential pipelines and dynamic adaptive decomposition, and to justify the choice from what the scenario says about predictability. The blueprint's two worked cases are the ones to internalize — per-file review passes plus a separate cross-file integration pass for a large pull request (fourteen files in this chapter's running example), where the stated cause is attention dilution, and adaptive mapping-then-prioritizing for adding tests to a legacy codebase, where the plan cannot exist until the structure is known. Task statement 1.1 rides on the same material through its distinction between model-driven decision-making and pre-configured tool sequences, and task statement 1.2 inherits the orchestrator-workers failure mode where every subagent succeeds and the aggregate coverage is still wrong. Expect distractors that are competent engineering aimed at the wrong layer: a larger context window for an attention problem, a consensus rule across parallel passes that quietly suppresses real findings, an extra agent for a failure a gate would fix. The judgement being scored is proportionality — whether you can name the failure the scenario actually describes and choose the least complex change that removes it. When two options both work, the simpler one is correct; when an option adds orchestration the described failure does not demand, it is there to catch you.
Exercises
- Build both triage implementations above against four mock MCP tools and run twenty tickets through each — ten single-concern, ten with two concerns in one message — logging token and tool-call counts per ticket. Find the ticket the workflow form mishandles. Do this one first: you have not hand-written a tool-use loop before, and forty lines of it teaches more than three chapters of reading.
- Take the fourteen-file code review case. Implement it first as one call over all files, then as a prompt chain of per-file passes plus an integration pass. Feed both an identical pull request containing one deliberately planted cross-file bug and three single-file bugs, and record which passes catch what.
- Write the decomposition step of an orchestrator for "research the impact of AI on creative industries" and inspect the subtasks before any worker runs. If they are all visual-arts subtopics, rewrite the coordinator prompt to specify coverage criteria rather than procedure, and re-inspect. This is the blueprint's failure mode in twenty lines, and the one thing you cannot observe from inside Claude Code, where the decomposition is hidden from you.
- Pick one of the exam's six scenarios, or something from your own backlog, and write down its verifier in one sentence — the cheap, fast check that would tell you the output was wrong. If you cannot write one, write down what would have to be true for a verifier to exist, and decide whether the design should be an agent at all.
Chapter 2 — The agentic loop in code
You have already watched this loop run several thousand times. Every time you press enter in Claude Code and it reads a file, then runs the test suite, then reads a different file because the failure was not where it expected, then edits and re-runs — that sequence is not a feature someone designed one step at a time. It is one small piece of code executing repeatedly. Claude Code is an agentic loop with a very good tool set wrapped around it, and the loop itself is about forty lines long.
The behaviour you experience as sophisticated — the backtracking, the "actually, let me check something else first", the recovery when a command fails — is not orchestration logic. Nobody wrote a branch that says if the test fails, read the source file. Claude decided that, in the moment, because the failing test output was in its conversation history and it reasoned over it. The forty lines around it did nothing but ferry messages back and forth and check one field.
Those forty lines are the densest patch of examinable material in Domain 1, and the exam tests them by describing a broken loop and asking what is actually wrong with it. The weight they carry comes from their being the boundary between two very different kinds of system. On one side is a workflow: you decide, at design time, that the support agent looks up the customer, then looks up the order, then decides on a refund. On the other side is an agent: you hand Claude the tools and the conversation, and it decides at each step what to do next based on what it has learned so far. The code for the second is shorter than the code for the first, which is why architects coming from an orchestration background frequently write the first and call it the second.
The exam guide states task statement 1.1 in terms of a lifecycle — send a request, inspect stop_reason, execute the requested tools, return results for the next iteration — plus one structural fact about conversation history and one conceptual distinction about who is making decisions. Everything here hangs off those three things. Write the loop twice, by hand, and the exam questions become recognition rather than reasoning.
We will build against the customer support resolution scenario from the exam guide: an agent with get_customer, lookup_order, process_refund, and escalate_to_human, aiming for high first-contact resolution while knowing when to hand off.
The Messages API, from zero
Claude Code talks to a model over an HTTP endpoint called the Messages API, and before you can write the loop you need to know what one call to it looks like. It is a single endpoint, POST /v1/messages, and there are only a handful of fields that matter.
A request carries a model identifier, a max_tokens ceiling on how long the reply may be, an optional system string, and a messages array. The system parameter is the persistent instruction set — role, policy, tone, constraints — and it is a separate field rather than an entry in messages precisely because it is not part of the conversation. CLAUDE.md is the closest thing you already use: content that shapes every turn without being something anyone said. Claude Code composes its own system prompt from a base prompt plus your CLAUDE.md files and sends it in this field on every request.
The messages array is the conversation, ordered oldest first, and each entry has a role of "user" or "assistant" and a content payload. Content may be a plain string for the simple case, but its real form is a list of blocks, and blocks are the concept that everything in this chapter depends on. A block is a typed fragment of a message. A text block holds prose. An image block holds an image. A tool_use block, which only ever appears in an assistant message, is Claude asking you to run something. A tool_result block, which only ever appears in a user message, is you handing back what happened. One message can hold several blocks of mixed types, and that fact alone invalidates a whole family of plausible-looking loop implementations.
The response you get back is itself an assistant message. It has a role of "assistant", a content list of blocks, and — the field that matters most — a stop_reason telling you why generation ended. Because the response is shaped like a message, you append it directly onto messages to continue the conversation. There is no translation step.
The last piece is tools: an array of tool definitions you send with the request, each one a name, a natural-language description, and an input_schema in JSON Schema. This is a declaration, not an installation. You are telling Claude what exists and what arguments each one takes; nothing runs on Anthropic's side. When Claude wants a tool run, it emits a tool_use block and stops. You run the function, on your own infrastructure, and send the result back. That division is the single most important thing to understand about tool use, and it is why the permission prompts in Claude Code work at all — execution happens locally, so there is a moment where a human can be asked.
Critically, the API is stateless. There is no session, no thread id, no server-side memory. Every request re-sends the entire messages array, and the model's only knowledge of what has happened is what is in that array. When you run /compact in Claude Code and watch the context shrink, you are watching that array being rewritten.
One turn of the cycle
Naming the four moving parts precisely matters, because the exam's distractors trade on imprecision. You send a POST /v1/messages carrying a system prompt, the full messages array, and the tools array. Claude replies with an assistant message whose content is a list of blocks. Some of those blocks are text. Some may be tool_use. The response also carries a stop_reason, and that field — not the content — tells you what happens next.
If stop_reason is "tool_use", you append the assistant message in full to messages, run every tool it asked for, and append a single new user message whose content is a list of tool_result blocks. Then you loop. If stop_reason is "end_turn", Claude has finished; the text blocks in that final assistant message are your answer.
Those tool_result blocks are not an abstraction you have to imagine. Every truncated file listing and command output you see indented in a Claude Code transcript is the rendering of one. The two block shapes are worth memorising exactly:
| Field |
tool_use (assistant) |
tool_result (user) |
type |
"tool_use" |
"tool_result" |
| identifier |
id — server-generated, e.g. toolu_01A… |
tool_use_id — must match the id character for character |
| payload |
name (tool name) and input (object matching the schema) |
content — a string, or a list of text/image blocks |
| failure signal |
— |
is_error: true, optional; the message goes in content |
Two pairing rules are enforced by the API and produce a 400, not a degraded answer. Every tool_use block in an assistant turn must be answered by a tool_result block in the immediately following message — if Claude emits three tool_use blocks and you return two results, the request is rejected. And every tool_result must have a preceding tool_use with that id, which is the failure people hit when they truncate history to save context and accidentally cut a conversation so that it opens on a tool_result.
The tool_result blocks all go in one user message. This trips people up because it feels like each tool deserves its own turn. It does not. One assistant turn, one user turn, however many tools were called. There is a third pairing rule of the same kind: within that user message, the tool_result blocks must come first in the content array. Any text you want to add alongside them has to come after all of them — text before a tool_result is another 400.
The gap between Claude emitting a tool_use block and you executing it is also where human-in-the-loop lives. Claude Code's permission prompt sits exactly there: the model has asked, the loop has paused, and nothing has run yet. Approving is the loop proceeding to execution; denying is the loop synthesising a tool_result that says so and handing it back as data. Nothing about the model's turn changes — it is a gate you install in your own code between two steps of the cycle.
The loop, written out
Python
import json
import time
import anthropic
client = anthropic.Anthropic()
TOOLS = [
{
"name": "get_customer",
"description": (
"Retrieve a customer record by email or account ID. Returns the "
"verified customer_id, tier, and account status. Call this before "
"any order lookup or refund."
),
"input_schema": {
"type": "object",
"properties": {"identifier": {"type": "string"}},
"required": ["identifier"],
},
},
{
"name": "lookup_order",
"description": "Fetch an order by order_id, including line items, "
"ship date, and current refund eligibility.",
"input_schema": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
},
{
"name": "process_refund",
"description": "Issue a refund against a verified customer and order. "
"Amount is in USD cents.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {"type": "string"},
"order_id": {"type": "string"},
"amount_cents": {"type": "integer"},
"reason": {"type": "string"},
},
"required": ["customer_id", "order_id", "amount_cents", "reason"],
},
},
{
"name": "escalate_to_human",
"description": "Hand the case to a human agent with a structured "
"summary. Use when policy blocks resolution or the "
"customer asks for a person.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {"type": "string"},
"root_cause": {"type": "string"},
"recommended_action": {"type": "string"},
},
"required": ["customer_id", "root_cause", "recommended_action"],
},
},
]
TOOL_IMPLS = {
"get_customer": get_customer,
"lookup_order": lookup_order,
"process_refund": process_refund,
"escalate_to_human": escalate_to_human,
}
def execute_tool(block):
"""Return a tool_result block for one tool_use block."""
try:
result = TOOL_IMPLS[block.name](**block.input)
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
}
except Exception as exc:
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": f"{type(exc).__name__}: {exc}",
"is_error": True,
}
def run_agent(user_message, max_iterations=25, budget_seconds=120):
messages = [{"role": "user", "content": user_message}]
deadline = time.monotonic() + budget_seconds
for iteration in range(max_iterations):
if time.monotonic() > deadline:
raise TimeoutError("wall-clock budget exhausted")
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=2048,
system=SUPPORT_SYSTEM_PROMPT,
tools=TOOLS,
messages=messages,
)
log.info("iteration=%d stop_reason=%s", iteration, response.stop_reason)
# The assistant message goes back verbatim, text blocks and all.
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use":
tool_uses = [b for b in response.content if b.type == "tool_use"]
results = [execute_tool(b) for b in tool_uses]
messages.append({"role": "user", "content": results})
continue
if response.stop_reason == "end_turn":
return response
if response.stop_reason == "max_tokens":
raise RuntimeError("response truncated; raise max_tokens")
raise RuntimeError(f"unhandled stop_reason: {response.stop_reason}")
raise RuntimeError("iteration cap hit — investigate, do not just raise it")
TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
type Impl = (input: any) => Promise<unknown>;
const toolImpls: Record<string, Impl> = {
get_customer: getCustomer,
lookup_order: lookupOrder,
process_refund: processRefund,
escalate_to_human: escalateToHuman,
};
async function executeTool(
block: Anthropic.ToolUseBlock,
): Promise<Anthropic.ToolResultBlockParam> {
try {
const result = await toolImpls[block.name](block.input);
return {
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result),
};
} catch (err) {
return {
type: "tool_result",
tool_use_id: block.id,
content: err instanceof Error ? err.message : String(err),
is_error: true,
};
}
}
export async function runAgent(
userMessage: string,
{ maxIterations = 25, budgetMs = 120_000 } = {},
): Promise<Anthropic.Message> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage },
];
const deadline = Date.now() + budgetMs;
for (let iteration = 0; iteration < maxIterations; iteration++) {
if (Date.now() > deadline) throw new Error("wall-clock budget exhausted");
const response = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 2048,
system: SUPPORT_SYSTEM_PROMPT,
tools: TOOLS,
messages,
});
log.info({ iteration, stop_reason: response.stop_reason });
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason === "tool_use") {
const toolUses = response.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
);
const results = await Promise.all(toolUses.map(executeTool));
messages.push({ role: "user", content: results });
continue;
}
if (response.stop_reason === "end_turn") return response;
if (response.stop_reason === "max_tokens") {
throw new Error("response truncated; raise max_tokens");
}
throw new Error(`unhandled stop_reason: ${response.stop_reason}`);
}
throw new Error("iteration cap hit — investigate, do not just raise it");
}
Notice what the loop body does not contain. There is no inspection of the text Claude produced. There is no tool-name whitelist deciding what may follow what. There is no state machine tracking which phase of the support workflow we are in. The only branch is on stop_reason. Swap the four support tools for Read, Bash, Edit, and Grep, add a permission gate before execute_tool, and you have the shape of the thing you use all day.
stop_reason is the control signal
| Value |
Meaning |
What the loop does |
"tool_use" |
The turn ended because Claude wants tools run. At least one tool_use block is present. |
Execute all of them, append results, iterate. |
"end_turn" |
Claude finished naturally. No tools pending. |
Terminate; the text blocks are the answer. |
"max_tokens" |
The response hit the max_tokens ceiling mid-generation and was truncated. |
Treat as an error condition. Any partial tool_use block is unusable — do not execute it. |
The exam guide's in-scope list names "tool_use" and "end_turn" specifically, and task statement 1.1 phrases the whole skill as continuing on the former and terminating on the latter. That is where to spend your memory. max_tokens is worth understanding because truncation is a real production failure and the appendix names max_tokens as a concept, but the exotic values are not where the questions live. Do handle the unexpected case defensively anyway: a loop whose else branch silently falls through will one day treat a new stop reason as completion and return half an answer as though it were whole.
Current docs list two further values worth folding into that defensive else. "stop_sequence" fires when generation hits one of the strings you passed in stop_sequences — a deliberate, requested stop, not a failure, and your loop should treat it the way it treats "end_turn". "model_context_window_exceeded", on Claude 4.5+ models, fires when the model runs out of context window space mid-generation rather than hitting the max_tokens ceiling — a different exhaustion than max_tokens (a token budget you set) and one that means the conversation itself, not just this response, has grown too large to continue; the fix is trimming or compacting history, not raising max_tokens. Older models return a hard validation error in this situation instead of a stop_reason, so which behavior you get depends on the model.
Chapter 3 covers designing a tool description for reliable selection; the sibling problem is reliable arguments once a tool is selected. As originally covered, that reliability rested entirely on writing a precise input_schema and trusting the model to fill it in correctly — dependable in practice, not guaranteed. Current docs add a mechanism that closes that gap: adding "strict": true alongside a tool's input_schema turns argument validation from "very likely correct" into a structural guarantee, the same way declaring input_schema at all turned a hand-parsed JSON blob into a structured tool_use.input. It costs nothing beyond the one field, and every tool definition in this book's examples is a reasonable candidate for it — see Chapter 3 for the schema-shape trade-offs it inherits from JSON Schema's supported subset.
One more loop-construction detail worth stating plainly here, since it belongs to the shape of the conversation rather than to prompting: if you enable extended thinking, the thinking (or redacted_thinking) blocks Claude returns must be passed back into messages unmodified on the next turn, in the same position, whenever a tool_use block sits alongside them. Editing, reordering, or dropping a thinking block before resending breaks the cryptographic signature the API uses to verify it, and the request fails outright rather than degrading quietly.
Why history is the agent's memory
The model is stateless. Every iteration re-sends the entire conversation, and the model's only knowledge of what has already happened is what is in that array. This is why the guide's second knowledge bullet — that tool results are appended to conversation history so the model can reason about the next action — is not a detail about serialisation. It is the mechanism by which the agent accumulates knowledge.
Trace it through the support case. Turn one, Claude calls get_customer("[email protected]"). The result lands in history carrying customer_id: "cus_8812" and tier: "enterprise". Turn two, Claude reads that from history and calls lookup_order for the disputed order, and the result says the item shipped nineteen days ago against a thirty-day window. Turn three, Claude has the verified customer id, the order id, and the eligibility fact all sitting in context, so it calls process_refund with arguments it derived rather than arguments you routed.
Now break it. Drop the assistant message and keep only the results: the API rejects the request, because tool_result blocks with no preceding tool_use. Keep the assistant message and drop the results: rejected too, and if you somehow got past validation Claude would re-request the same tool forever, since from its point of view the call never returned. Summarise the tool result into your own prose instead of returning the block: you have quietly become the decision-maker, filtering what the model is allowed to reason over. Each of these is a different flavour of the same mistake, which is treating history as a log rather than as state.
This also reframes context management as an engineering problem you already have opinions about. /compact is a rewrite of the messages array — history replaced by a summary so the array fits the window — and the reason it is a considered operation rather than a free one is that everything it drops is knowledge the agent no longer has. The reason a long Claude Code session sometimes forgets a decision you made an hour ago is not a memory bug. It is the array.
Model-driven, not a decision tree
The exam draws an explicit line between Claude reasoning about which tool to call next based on context, and a pre-configured decision tree or fixed tool sequence. The difference is not stylistic. A fixed sequence — always get_customer, then always lookup_order, then branch on an amount threshold — encodes your best guess at the shape of every case. Real support traffic does not have one shape. A customer who opens with an order number does not need a lookup by email. A customer disputing three charges needs three order lookups. A customer who says "just let me talk to someone" needs escalate_to_human immediately and nothing else.
In the loop above, all of those emerge without a line of routing code, because the model sees the conversation and picks. What you keep control of is the envelope: which tools exist at all, what their schemas permit, and — as Chapter 6 covers — which calls a hook is allowed to block. That is the right division. You constrain the space of legal actions; Claude chooses within it. Plan mode is that division made visible: it narrows the envelope to read-only tools for a stretch, and within the narrowed envelope Claude still decides everything.
None of which means sequencing is unenforceable. When ordering must be guaranteed rather than likely, you enforce it programmatically with a prerequisite gate, not by hard-coding the call order in the loop. Task statement 1.4 covers that, and its example is precisely this scenario: block process_refund until get_customer has returned a verified customer id.
Three anti-patterns the guide names
The skills section of task statement 1.1 lists three things to avoid. Expect all three as distractors.
Parsing natural language for termination
Python
# WRONG
text = "".join(b.text for b in response.content if b.type == "text")
if "let me know if" in text.lower() or "anything else" in text.lower():
return response
TypeScript
// WRONG
const text = response.content
.filter((b) => b.type === "text")
.map((b) => (b as Anthropic.TextBlock).text)
.join("");
if (/let me know if|anything else/i.test(text)) return response;
This fails in both directions. Claude says "let me know if you need anything else" while still holding a pending tool_use block, so you terminate early and never issue the refund. And it phrases a final answer as "Your refund of $84.99 has been submitted." with no matching phrase, so you loop again on a conversation that is complete. You are re-deriving, unreliably, a signal the API already gave you exactly.
An iteration cap as the stopping mechanism
Python
# WRONG
for _ in range(5):
response = client.messages.create(...)
messages.append({"role": "assistant", "content": response.content})
tool_uses = [b for b in response.content if b.type == "tool_use"]
if not tool_uses:
break
messages.append({"role": "user", "content": [execute_tool(b) for b in tool_uses]})
return response # after 5 turns, whatever we have
TypeScript
// WRONG
for (let i = 0; i < 5; i++) {
response = await client.messages.create({ /* ... */ });
messages.push({ role: "assistant", content: response.content });
const toolUses = response.content.filter((b) => b.type === "tool_use");
if (toolUses.length === 0) break;
messages.push({
role: "user",
content: await Promise.all(toolUses.map(executeTool)),
});
}
return response; // after 5 turns, whatever we have
The cap here is doing the job stop_reason should be doing, and the tell is the final return: a five-iteration case and a runaway case exit through the same door with no way to distinguish them. A three-issue billing dispute legitimately needs eight or nine turns and gets silently truncated. Keep the cap — the correct version above has one — but make exceeding it an error, not a return path.
Assistant text as a completion indicator
Python
# WRONG
if any(b.type == "text" for b in response.content):
return response
TypeScript
// WRONG
if (response.content.some((b) => b.type === "text")) return response;
This one is the most seductive because it sounds structural rather than heuristic. It is wrong because a tool_use response very often carries text as well — which you have seen a thousand times, in every Claude Code turn where a sentence of narration appears immediately above a tool call:
{
"stop_reason": "tool_use",
"content": [
{ "type": "text",
"text": "I've confirmed Dana's account. Let me pull up order SO-44120." },
{ "type": "tool_use", "id": "toolu_01Xy…", "name": "lookup_order",
"input": { "order_id": "SO-44120" } }
]
}
Text present, turn not finished. The presence of text tells you Claude explained itself. Only stop_reason tells you whether it is done.
The unifying rule is short enough to carry into the exam: anything that determines completion by something other than stop_reason is the wrong answer.
tool_choice shapes what the next response may contain, and therefore what stop_reason you can expect.
| Value |
Behaviour |
Loop impact |
{"type": "auto"} (default) |
Claude decides whether to call a tool. |
Either stop reason is possible. This is the agentic setting. |
{"type": "any"} |
Claude must call some tool, its choice which. |
Guarantees "tool_use" — the loop cannot terminate on this turn. Use it to stop the model answering conversationally when an action is required. |
{"type": "tool", "name": "get_customer"} |
Claude must call that named tool. |
Also guarantees "tool_use", with the tool fixed. |
Current docs add a fourth value, {"type": "none"}: Claude must not call any tool, which forces "end_turn" with a text-only reply — the mirror image of "any".
The forcing modes are turn-scoped instructions, not loop configuration, and treating them otherwise is how people accidentally build a decision tree. Set tool_choice to a named tool on iteration one to guarantee the case opens with get_customer, then drop back to auto for every subsequent iteration so the rest of the investigation stays model-driven. Leave it forced and the loop can never reach "end_turn" — Claude is obliged to call a tool forever.
A single assistant turn can contain several tool_use blocks. Claude 4 models do this readily: given a customer disputing two orders, one turn may carry two lookup_order calls with different ids. It is the same behaviour you see when Claude Code fires three Read calls at once instead of walking the files one at a time. The loop above already handles it — Promise.all in TypeScript, or asyncio.gather in Python for I/O-bound tools (the run_agent listing above runs them sequentially with a plain comprehension for clarity; swap in gather when the tools are slow enough that concurrency matters) — and it collects every result into one user message, which is what the API requires.
The caveat is that Claude only parallelises calls it believes are independent, and it can be wrong, particularly when a tool's input should have come from another tool's output. Two safeguards apply. Write tool descriptions that state prerequisites explicitly, so process_refund advertises that it needs a verified customer_id from get_customer; that shifts the model toward sequencing on its own. And where correctness cannot depend on the model getting it right, gate the dependent tool programmatically so a premature call fails with a structured error the agent can recover from. If you want to remove the possibility entirely for a stretch of the workflow, disable_parallel_tool_use alongside tool_choice caps the response at one tool call — at the cost of the latency parallelism was buying.
Preserving order within the results array is good hygiene but is not what makes the pairing valid. tool_use_id is what binds a result to its call.
Guardrails that are not termination logic
The distinction the exam cares about is between mechanisms that decide the task is complete and mechanisms that decide the loop must stop being allowed to run. The first is stop_reason and nothing else. The second is operational safety, and three pieces belong in any production loop.
A maximum iteration count bounds cost and catches genuine cycles, such as an agent retrying a tool that fails identically every time; exceeding it should raise, alert, and preserve the transcript, never return a partial answer as if it were final. A wall-clock budget covers what the iteration count misses, since a synchronous MCP call to a slow backend can burn ninety seconds inside a single iteration. And structured logging of iteration number, stop_reason, tool names, and durations is what turns "the agent behaved oddly on ticket 4471" into a readable trace — this is the same observability argument that Chapter 5 makes for routing all subagent traffic through a coordinator.
The same loop, at a higher level
The Claude Agent SDK collapses all of the above into a call. Its query() runs the loop internally and yields typed messages as the agent works. It is the same engine Claude Code runs on, exposed as a library.
One naming convention needs stating before the code makes sense. When you connect an MCP server to the SDK, its tools do not keep their bare names. They are namespaced as mcp__<server>__<tool>, where <server> is the key you gave the server in your configuration — not the server's internal name — and <tool> is the name the server advertises. So a server registered under the key support exposing a get_customer tool becomes mcp__support__get_customer. Double underscores, three segments. The namespacing exists because two servers can both offer a tool called search, and the loop needs to know which process to dispatch to. Rename the key in your config and every allowlist entry referring to it breaks, which is the usual cause of a tool that "exists but Claude never calls it".
Python
from claude_agent_sdk import query, ClaudeAgentOptions
options = ClaudeAgentOptions(
system_prompt=SUPPORT_SYSTEM_PROMPT,
mcp_servers={"support": {"command": "node", "args": ["./support-mcp.js"]}},
allowed_tools=[
"mcp__support__get_customer",
"mcp__support__lookup_order",
"mcp__support__process_refund",
"mcp__support__escalate_to_human",
],
)
async for message in query(
prompt="Dana ([email protected]) says order SO-44120 arrived damaged.",
options=options,
):
log.info("%s", message)
TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Dana ([email protected]) says order SO-44120 arrived damaged.",
options: {
systemPrompt: SUPPORT_SYSTEM_PROMPT,
mcpServers: {
support: { command: "node", args: ["./support-mcp.js"] },
},
allowedTools: [
"mcp__support__get_customer",
"mcp__support__lookup_order",
"mcp__support__process_refund",
"mcp__support__escalate_to_human",
],
},
})) {
log.info(message);
}
Behind that, the SDK is doing exactly what you wrote by hand: assembling the tools array from the connected MCP servers, branching on stop_reason, appending the assistant message, dispatching each tool_use to the right server, collecting the tool_result blocks into one user message, and iterating until "end_turn". It also layers on permission modes, hooks, and subagent spawning, which later chapters cover.
Two things follow from knowing what it hides. First, the SDK's failure modes are the raw loop's failure modes with a different surface: a custom tool handler that throws an uncaught exception kills the loop and Claude never sees the error, whereas one that returns an error result — isError set — puts the failure into history as data the agent can react to. That is the manual is_error flag, one level up. Second, the SDK does not enforce a turn budget unless you set one — maxTurns/max_turns exists but defaults to unlimited — so the iteration cap and wall-clock budget you wrote by hand are still your responsibility; retrying a failed request is the underlying client's concern, not something specific to the agentic loop.
What the exam tests
Task statement 1.1 is the direct target here, and the items written against it are almost all diagnostic: a loop misbehaves and you identify the cause. The three knowledge bullets — the lifecycle driven by stop_reason, tool results appended to conversation history so the model can reason about the next action, and model-driven decision-making versus pre-configured decision trees — map onto three recognisable question shapes, and the skills bullets name the three anti-patterns you will see as distractors. When an option determines completion by scanning text, by hitting an iteration cap, or by observing that the assistant produced prose, it is wrong; the only correct signal is stop_reason, continuing on "tool_use" and terminating on "end_turn". Expect at least one item that turns on a tool_use response also containing narration text, because that single fact invalidates the most plausible-sounding wrong answer. tool_choice appears under task statement 2.3, where the judgment call is choosing "any" to guarantee an action rather than conversational text, versus a forced named tool to pin the first step before returning to auto. Task statement 1.4's prerequisite gates are the counterweight to model-driven flow, and the exam wants you to know that hard-coding call order in the loop and enforcing it with a gate are not the same design. Streaming, token counting, and rate limits are explicitly out of scope, so a loop question will never hinge on incremental response handling.
Exercises
- Send one Messages API request with no tools at all — a system prompt and a single user message — and print the raw response object. Identify
role, content, the block types present, and stop_reason. Then add a single tool definition and send a prompt that requires it, and diff the two responses. This is five minutes and it makes every later exercise concrete.
- Implement
run_agent from scratch in the language you use less often, against stub implementations of get_customer, lookup_order, process_refund, and escalate_to_human. Do not consult the listing above until you have it running. Then diff yours against it and account for every difference.
- Break it deliberately, four ways, and record the exact failure: omit the assistant message before appending results; return results for only the first of two
tool_use blocks; mutate one tool_use_id by a single character; and set max_tokens to 64 so a response truncates mid-tool_use. Note which produce a 400, which produce a silent behavioural change, and which produce an infinite loop.
- Add a permission gate to your loop: before calling
execute_tool, prompt on stdin for any invocation of process_refund, and on refusal return a tool_result saying the action was denied by a human reviewer. Watch what Claude does next. You have just rebuilt Claude Code's permission prompt in about eight lines.
- Drive the same stub tools through a conversation that forces parallel calls — a customer disputing three separate orders in one message — and log how many
tool_use blocks arrive per turn. Then set disable_parallel_tool_use and compare total iterations and wall-clock time.
- Take the wrong-way iteration-cap example and write the smallest support conversation that makes it silently return an incomplete resolution. Keep that transcript; it is the clearest argument you will have for why a cap is a safety net rather than a termination condition.
You have watched Claude Code choose tools thousands of times. Ask it to find every caller of a function and it reaches for Grep; ask for the test files in a package and it reaches for Glob; ask it to change one line and it uses Edit rather than rewriting the file. Selection feels like something the model simply does, the way it feels like it simply understands your codebase. It is worth noticing that this is not luck. Claude Code's built-in tools are described with unusual care, and the reliability you experience daily is downstream of that care rather than of anything intrinsic to the model.
The moment you author tools yourself, the illusion breaks. You will write two tools you consider obviously distinct, watch the model pick between them at coin-flip rates, and reach for the wrong explanation — that the model is confused, that you need a routing layer, that a few examples in the system prompt will settle it. The exam is built around the correct explanation, and it is a causal claim rather than a stylistic preference: tool descriptions are the primary mechanism an LLM uses for tool selection, and minimal descriptions produce unreliable selection among similar tools.
Three fields. A name, a description, and an input_schema holding a JSON Schema object. That is the entire surface the model sees. Everything else you know about the tool — that lookup_order hits the fulfilment database, that get_customer requires a verified account ID, that the two return overlapping fields for historical reasons — exists nowhere in the model's context unless you wrote it into the description.
The consequence worth internalising before anything else: the description is prompt text. It is not documentation for a colleague who can also read the source. It is injected into the model's context at selection time and read the way any other instruction is read. A tool definition is a prompt wearing a schema, and if you treat it as an API contract you will write two lines where you needed twenty.
extract_data_points = {
"name": "extract_data_points",
"description": (
"Extract structured numeric and categorical data points from a research "
"document that has already been loaded. Returns a list of records, each with "
"a field name, a value, a unit where applicable, and the page and paragraph "
"the value was read from.\n\n"
"Input: document_id from load_document, plus the field names to extract.\n"
"Use this when you need specific values out of a document — revenue figures, "
"sample sizes, publication dates, dosage tables.\n"
"Do NOT use this to understand what a document argues; use summarize_content. "
"Do NOT use this to check whether an external claim is supported; use "
"verify_claim_against_source.\n"
"Edge case: if a requested field is absent, the record is returned with a null "
"value and a reason, rather than being omitted."
),
"input_schema": {
"type": "object",
"properties": {
"document_id": {
"type": "string",
"description": "Identifier returned by load_document.",
},
"field_names": {
"type": "array",
"items": {"type": "string"},
"description": "Field labels to extract, e.g. ['sample_size', 'p_value'].",
},
},
"required": ["document_id", "field_names"],
},
}
TypeScript
const extractDataPoints: Anthropic.Tool = {
name: "extract_data_points",
description: [
"Extract structured numeric and categorical data points from a research document",
"that has already been loaded. Returns a list of records, each with a field name, a",
"value, a unit where applicable, and the page and paragraph the value was read from.",
"",
"Input: document_id from load_document, plus the field names to extract.",
"Use this when you need specific values out of a document — revenue figures, sample",
"sizes, publication dates, dosage tables.",
"Do NOT use this to understand what a document argues; use summarize_content.",
"Do NOT use this to check whether an external claim is supported; use",
"verify_claim_against_source.",
"Edge case: if a requested field is absent, the record is returned with a null value",
"and a reason, rather than being omitted.",
].join("\n"),
input_schema: {
type: "object",
properties: {
document_id: { type: "string", description: "Identifier returned by load_document." },
field_names: {
type: "array",
items: { type: "string" },
description: "Field labels to extract, e.g. ['sample_size', 'p_value'].",
},
},
required: ["document_id", "field_names"],
},
};
Note the field name: input_schema. Anthropic's Messages API takes a flat list of tool objects, each with name, description, and input_schema holding a JSON Schema object directly. OpenAI's shape — a wrapper object with type: "function" and a nested function containing parameters — is not accepted, and the exam is happy to put parameters in a distractor.
The per-property description strings inside the schema are also prompt text, not decoration. They are where you put format constraints that are awkward to express in schema terms: that an order identifier is #-prefixed and eight digits, that a URL must point at a document rather than an arbitrary page.
What a good description contains
Five things, and you can audit any description against the list. It states what the tool does and what it returns, in terms of the shape of the result rather than the implementation. It states the input formats it accepts, including the ones that look valid but are not. It gives example queries in the user's language, so the model can pattern-match a request against the tool without reasoning from first principles. It names edge cases, particularly the difference between "I failed" and "I succeeded and found nothing". And it draws explicit boundaries against sibling tools by name.
That last one carries more weight than the other four combined when your inventory contains near-neighbours, because it is the only element giving the model information it cannot infer from the tool in isolation. Compare two versions of the same support tool.
Bad:
name: lookup_order
description: Retrieves order details.
Good:
name: lookup_order
description: >
Look up a single order by its order number and return status, line items,
fulfilment and delivery dates, carrier tracking, and the refund state of each
line. Accepts an order number with or without the leading '#' (e.g. "#48812",
"48812"). Does NOT accept email addresses, names, or account IDs.
Use for queries like "where is my order 48812", "was #48812 delivered",
"which items shipped in my last order".
Use get_customer instead when the request identifies a person rather than an
order ("look up my account", "what's on file for [email protected]") or when
you need the customer's order history rather than one order.
If the order number is well-formed but not found, returns an empty result with
found: false — this is a successful call, not an error, and usually means a
typo or an order belonging to a different account.
Line by line, the second version buys specific things. The return description stops the model calling get_customer first to find out whether tracking data exists. The accepted-format sentence, and especially the "does NOT accept" clause, stops the model passing an email address and burning a turn on a validation error. The example queries do the work that few-shot examples in the system prompt would do, at a fraction of the token cost and without needing to be maintained separately from the tool. The boundary sentence names get_customer explicitly, which is what converts two tools that overlap in the model's mind into two tools with a divide. And the last paragraph pre-empts a real reliability bug: an agent that treats an empty result as a failure and retries, or escalates, when the correct behaviour is to ask the customer to re-read the number.
A well-specified description running 150–250 tokens is normal, and cheap relative to a misrouted call plus the recovery turn behind it. This is the length Claude Code's own tools run to, which is why they work.
strict: true guarantees the schema, not the selection
Everything above fixes which tool gets called. A separate, narrower failure survives even a perfect description: Claude picks the right tool but supplies an argument that doesn't quite match the schema — a string where a number was declared, a required field left out. Current tool definitions can add "strict": true alongside input_schema to close that gap structurally, the same way strict JSON-schema output (Chapter 13) turns "usually valid JSON" into "guaranteed valid JSON." With strict: true, the input Claude produces is constrained at generation time to match the schema exactly, rather than validated after the fact and hoped for.
This doesn't replace anything in this chapter — a strict lookup_order still needs the boundary sentences and edge-case notes to get selected correctly in the first place — but it is worth adding to any tool whose input shape has real consequences downstream, such as process_refund's amount_cents, where a malformed argument is a worse failure than a misrouted call. Treat strict: true as the schema-side complement to the description work this chapter is otherwise entirely about.
An adjacent, lower-priority field on a tool definition, input_examples, lets you attach example input objects directly rather than folding "example queries in the user's language" (the third element of a good description, above) into prose. Where you already have concrete example calls, this is a more structured alternative to writing them out as sentences — though the prose form still carries information (when to use the tool) that a bare input example does not.
Diagnosing misrouting
Misrouting has a signature in the logs. The tool that gets called is not random; it is consistently the wrong member of a specific pair, on a consistent class of query. In a support agent, get_customer is called on order queries. In a research system, analyze_content and analyze_document — two tools whose descriptions are near-identical — are picked more or less interchangeably regardless of whether the input is a web result or a loaded PDF.
Diagnosis proceeds in three passes, in order of cost. First, put the two descriptions side by side and ask whether a competent new engineer, given only those strings, could pick correctly for every query in your failure sample. If not, the descriptions are the cause. Second, check whether the failures cluster around inputs the schema does not distinguish — if both tools accept a bare string called query, the schema is offering no signal either. Third, and only if the descriptions are already discriminating, read the system prompt for keyword-sensitive instructions. A line like "always begin by understanding the customer" will pull calls toward anything named get_customer regardless of what its description says, because the system prompt sits closer to the model's instruction-following than the tool block does. This is the failure mode where good descriptions get overridden, and it is invisible unless you go looking.
Resist two tempting non-fixes. A deterministic routing layer that parses the user's input and pre-selects a tool throws away the language understanding you are paying for, and it will be wrong on every phrasing you did not anticipate. Piling five to eight few-shot examples into the system prompt adds tokens on every request without touching the cause; the model still cannot tell the tools apart, it has just memorised a handful of surface patterns.
The three repair moves
Rename and rewrite to remove overlap. When two tools really do differ but are described in the same words, change the name so it encodes the difference and rewrite the description around that difference. analyze_content becomes extract_web_results, with a description specific to web search output: it takes result snippets and URLs from the search agent, returns extracted passages with source attribution, and says outright that documents already loaded into the workspace go to the document tools instead. The rename matters as much as the rewrite, because the name is what the model sees first and what it recalls when scanning a list.
Split a generic tool into purpose-specific tools. When one tool does several unrelated jobs, no description can make its selection reliable, because the ambiguity is in the tool, not the prose. A generic analyze_document that summarises, extracts, and fact-checks depending on how you phrase the request should become extract_data_points, summarize_content, and verify_claim_against_source — three tools with defined input and output contracts, each of which either applies to the current step or does not. The split also gives you somewhere to put the boundary sentences, since each of the three can name the other two.
Review the system prompt. The cheapest repair and the one most often skipped. Strip or reword instructions whose keywords collide with tool names, and move real sequencing requirements out of prose and into tool_choice or a programmatic prerequisite.
Names should follow one convention throughout an inventory: snake_case, verb plus resource, namespaced by service where you have more than one backend. billing_get_invoice and fulfilment_get_shipment read awkwardly to a human and are far easier for a model to keep apart than two tools both called get_record.
Everything above concerns the description of one tool. The same problem reappears at the level of the agent, where the variable is how many tools are in front of the model at once. The exam states the effect concretely: an agent holding 18 tools selects less reliably than one holding four or five, because every additional tool widens the decision and adds near-neighbours to be confused with. Selection reliability is not a property of a tool; it is a property of a tool set.
Worse, agents holding tools outside their specialisation reach for them. A synthesis agent given web search will attempt web searches — not because the description told it to, but because the tool is present and the task has a gap the tool superficially fills. The result is a synthesis agent doing mediocre research instead of good synthesis, and a coordinator that has lost track of who is responsible for what.
The default, then, is scoped tool access: each subagent gets only the tools its role requires. In the research system that means the search agent holds search and fetch tools, the document agent holds load_document and the three extraction tools, the synthesis agent holds none of them, and the report agent holds formatting and citation tools. The coordinator holds the delegation tools and nothing else.
Strict scoping has a cost, and the exam's verify_fact case is where you pay it. When the synthesis agent needs a claim checked, it returns control to the coordinator, which invokes the search agent, which returns, and the coordinator re-invokes synthesis — two to three extra round trips, and roughly 40% added latency. Evaluation shows 85% of those verifications are simple fact-checks (dates, names, statistics) and 15% need real investigation. The right move is a limited, scoped cross-role tool: give synthesis a verify_fact tool narrow enough to answer the 85% case and nothing more, and keep the 15% routed through the coordinator to the search agent. This is least privilege applied with a measurement behind it. Handing synthesis the full web search toolkit over-provisions it and dissolves the separation of concerns you built the architecture for; batching verification requests to the end of the pass creates blocking dependencies, since later synthesis steps often depend on facts verified earlier.
The same instinct applies to individual tools too broad for the role holding them. Replace the generic with the constrained alternative: fetch_url, which will happily retrieve anything, becomes load_document, which validates that the URL points at a document and rejects the rest. The agent keeps the capability it needs and loses the capability to wander.
Everything above treats "distribute tools across agents" as something you design by hand: you look at the roles, decide who needs what, and wire up separate tool lists per subagent. Current tool-use support offers a second lever for the same underlying problem — large tool inventories degrading selection — that works without redesigning your agent boundaries: the tool search tool (tool_search_tool_bm25_20251119 for keyword search over tool definitions, or tool_search_tool_regex_20251119 for pattern matching), which lets Claude defer loading most tool definitions and search for the relevant one on demand rather than holding all of them in context at once.
This doesn't invalidate the scoping argument above — a synthesis agent that shouldn't be doing web research is a separation-of-concerns problem, not a context-budget problem, and giving it search-on-demand access to a search tool doesn't fix that it shouldn't be reaching for one at all. But for the narrower case this chapter also describes — an agent legitimately needing to choose among many tools of the same kind, where the 18-tools-degrades-selection number applies mechanically rather than architecturally — tool search is a more direct fix than manually splitting an agent's role to shrink its list. Treat scoped access as the answer to "who should be able to do this" and tool search as the answer to "how do I keep a large, legitimate tool inventory usable."
tool_choice is the escape hatch from selection. Three settings, each answering a different question.
| Setting |
Behaviour |
Use when |
"auto" |
The model decides whether to call a tool at all and which one. May return conversational text. |
Ordinary agent turns where a text answer is a valid outcome. |
"any" |
The model must call some tool, but chooses which. |
You need structured output and a prose reply would be a failure. |
{"type": "tool", "name": "..."} |
A specific named tool is called. |
A particular tool must run first, before the model has discretion. |
Current docs add a fourth value, {"type": "none"}: no tool may be called and Claude must reply with text — useful when you need a plain-language answer and want to rule out an accidental or premature tool call.
The forced form is how you express sequencing that must not be probabilistic. If enrichment tools depend on metadata that extract_metadata produces, do not write "always call extract_metadata first" in the system prompt and hope; force it on the first turn, then hand control back with "auto" for the follow-up turns where the enrichment work happens.
Python
first = client.messages.create(
model="claude-sonnet-5",
max_tokens=2048,
tools=[extract_metadata, enrich_company, enrich_filing],
tool_choice={"type": "tool", "name": "extract_metadata"},
messages=[{"role": "user", "content": "Process the Q3 filing at doc_8814."}],
)
# ... append the tool_use block and its tool_result, then continue:
followup = client.messages.create(
model="claude-sonnet-5",
max_tokens=2048,
tools=[extract_metadata, enrich_company, enrich_filing],
tool_choice={"type": "auto"},
messages=conversation,
)
TypeScript
const first = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 2048,
tools: [extractMetadata, enrichCompany, enrichFiling],
tool_choice: { type: "tool", name: "extract_metadata" },
messages: [{ role: "user", content: "Process the Q3 filing at doc_8814." }],
});
const followup = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 2048,
tools: [extractMetadata, enrichCompany, enrichFiling],
tool_choice: { type: "auto" },
messages: conversation,
});
Note what tool_choice does not do. It cannot enforce a prerequisite across an open-ended multi-turn loop — it applies to one request. When a sequence must hold for business-critical reasons across a whole session, such as verifying a customer before processing a refund, the enforcement belongs in your own code: block the call until the prerequisite has returned. Prompt-based and choice-based mechanisms are both weaker than a programmatic gate when the failure has financial consequences. Chapter 6 develops programmatic gates properly; for now, keep the ranking in mind, because sample question 1 turns on it.
You know these from use. What follows is the vocabulary the exam uses for what you already do, and the distinctions it tests.
Grep searches file contents — function names, error message strings, import statements. Glob matches file paths and names — **/*.test.tsx, src/**/handler.py. That distinction is the whole of several questions: "find every caller of reconcileInvoice" is Grep; "find every test file in the payments package" is Glob.
Read and Write are whole-file operations. Edit is surgical: it replaces a unique anchor string in place, and it fails when the anchor appears more than once. The exam guide names Read followed by Write as the fallback when Edit cannot find unique text — load the whole file, reconstruct it with the change, write it back. Slower and more expensive in tokens, which is why it is the fallback rather than the default, but deterministic where Edit is not. Answer that on the exam. Be aware, though, that current Claude Code documents a different first move for a non-unique anchor: extend old_string with enough surrounding context to pin one occurrence, or set replace_all: true to change them all. Read + Write is the guide's answer, not the tool's documented behaviour. Bash is legitimate for running builds and tests, but shelling out to grep or cat where a dedicated tool exists is the wrong answer: you lose the structured results, the permission model, and the tool's own guardrails.
The selection question that matters most is not which tool but in what order. The strategy the exam describes is incremental: Grep for entry points, then Read to follow the imports and trace the flow, rather than reading everything upfront. Reading forty files to understand one code path spends the context window on material the model will never use, and pushes the relevant part into the middle where attention is weakest.
The related pattern is tracing a function through wrapper modules, where a single Grep for the original name misses everything because the wrappers re-export under new names. Identify the exported names first — Grep the barrel file or the module's export statements — then search for each exported name in turn. Two passes, complete coverage, no guessing about which alias a given consumer used.
What the exam tests
Task statement 2.1 is the backbone, and its central claim — that tool descriptions are the primary selection mechanism and that minimal descriptions produce unreliable selection among similar tools — is the correct answer to a whole family of questions. Expect to be shown a misrouting symptom, most likely get_customer versus lookup_order or analyze_content versus analyze_document, and asked for the most effective first step; the answer expands the descriptions to include input formats, example queries, edge cases, and boundaries against siblings, rather than adding few-shot examples, building a routing layer, or consolidating the tools. You should also be able to name the three repair moves and match each to its worked example: rename plus rewrite for overlap, split into purpose-specific tools with defined I/O contracts for a generic tool, and a system prompt review when descriptions are already good but keyword-sensitive instructions are creating unintended associations. Task statement 2.3 supplies the numbers and the architecture questions: 18 tools instead of four or five degrades selection, out-of-specialisation tools get misused, scoped access per role is the default, and a limited cross-role tool such as verify_fact is the right answer when a measured majority of cases are simple — with the complex remainder still routed through the coordinator. Know fetch_url to load_document as the canonical replacement of a generic tool with a constrained one, and know the three tool_choice values cold, including that forced selection is how you guarantee extract_metadata runs before enrichment and that "any" is how you guarantee a tool call instead of conversational text. Task statement 2.5 is mostly discrimination: Grep for contents, Glob for paths, Read and Write for whole files, Edit for unique-anchor changes with Read plus Write as the documented fallback, Bash only where no dedicated tool exists. The two strategies under 2.5 are worth memorising as strategies rather than facts — incremental exploration starting from Grep, and tracing usage across wrappers by identifying exported names before searching for each. Nothing here touches MCP server deployment, authentication protocols, or transport internals, all of which the guide's out-of-scope list removes.
Exercises
- Take a pair of near-neighbour tools — or write
get_customer and lookup_order from the support scenario — and give both the minimal one-line descriptions the exam quotes. Assemble twenty realistic user queries, ten for each tool, run them through the model with tool_choice: "auto", and record the selection accuracy. Now rewrite both descriptions to carry all five elements, including boundary sentences naming the sibling, and re-run the same twenty. Note not just the accuracy delta but which queries remain wrong, and whether the remaining failures point at the descriptions or at the schema.
- Implement the
analyze_document split. Write the single generic tool first, with a mode parameter, then decompose it into extract_data_points, summarize_content, and verify_claim_against_source with distinct input schemas and output shapes. Write all six definitions in both Python and TypeScript, and confirm that each of the three descriptions names the other two in its boundary section.
- Build a two-turn harness that forces
extract_metadata on the first request via {"type": "tool", "name": "extract_metadata"}, appends the tool_use and tool_result blocks, and issues the follow-up with tool_choice: "auto". Then change the follow-up to "any" and observe what happens when the model has nothing useful left to call — this tells you where "any" is a guarantee and where it is a trap.
- Read the description of any Claude Code built-in tool, then audit it against the five elements. Find the boundary sentence, the accepted-input clause, and the edge-case note. Then write a tool of your own to the same standard.
Chapter 4 — Errors, recovery, and telling the agent the truth
You have watched this happen dozens of times. Claude Code runs a Bash command, the command exits non-zero, stderr comes back saying the module is not installed or the flag is unrecognised, and Claude reads that text, adjusts, and tries something else. No one wrote a handler for that specific failure. There is no catch block anywhere that knows what command not found: uv means. The recovery worked because the failure arrived as content the model could read rather than as an exception that ended the turn.
That is the entire chapter in one observation, and it is worth sitting with, because the instincts you have built as a backend engineer are partly wrong here. Most error handling you have written in your career was written for a human being. A stack trace goes to a log aggregator, an alert fires, someone opens the dashboard on Monday. The failure is a signal to an operator, and the running program's only obligation is to fail without corrupting anything. That model breaks the moment your program contains a reasoning loop, because now there is a second consumer of every failure — one that is present at the instant it happens, capable of adapting, and completely blind to anything you do not put in front of it.
So state it bluntly: in an agentic system, an error is a message to the model, and the model is its primary consumer. Not the operator. Not the log. If a tool times out and you raise an exception that propagates out of the agent loop, you have removed the one participant in the system that could have decided to narrow the query, try a different source, or continue with what it already has. Return the same failure as a readable tool result and the model gets to reason about it — and a model reading "the pricing service did not respond within 30 seconds; this is usually transient" is often a better recovery planner than the branching logic you would have hand-written, because it holds the whole task in context and your error handler does not.
The reason this needs saying at all is that everything in your professional reflexes pulls the other way. Fail fast. Do not swallow exceptions. Let it crash and let the supervisor restart it. Good rules for a service whose failures are read by people; actively harmful for a tool whose failures are read by a model mid-task, where "fail fast" means "terminate the reasoning process that was about to solve this".
This is also the part of the Claude Certified Architect (Foundations) syllabus that appears twice. Task statement 2.2 covers structured error responses for MCP tools; task statement 5.3 covers error propagation across multi-agent systems. They share a spine — the same category taxonomy, the same distinction between an access failure and a valid empty result, the same insistence that partial results travel with the error. What differs is the audience. In 2.2 the recipient is the agent calling the tool. In 5.3 the recipient is a coordinator deciding what to do about a subagent that could not finish. Learn the spine once and both task statements follow.
One scoping note. The exam guide's appendix explicitly puts rate limiting, quotas, and pricing out of scope. Retry strategy is in scope as a design topic — both task statements demand it — so retries here are treated as engineering judgment about attempt budgets and idempotency rather than as protocol mechanics.
A layer worth naming explicitly, because it is easy to conflate with everything above: isError/is_error and the four categories that follow are about your tool failing while the Claude API itself is healthy. A separate failure mode exists one layer down, where the API call carrying the whole conversation cannot complete at all — overloaded_error (HTTP 529, capacity-constrained) or rate_limit_error (HTTP 429). Those never reach the model as a tool_result, because there is no model turn to reach; the SDK's own retry logic handles them beneath your loop, with a small default retry budget, exponential backoff, and respect for a retry-after header when the API supplies one. Don't build a second, competing retry policy for this layer inside your tool-error handling — it answers a different question than "was this specific tool call retryable," and the SDK default already answers it. A related, model-side failure worth keeping distinct from both: stop_reason: "model_context_window_exceeded" (Claude 4.5+) means the conversation itself ran out of room mid-generation, not that a tool or the API failed — the fix is trimming or compacting the message history your loop maintains, not a retry of any kind.
The two spellings of the same flag
You have installed MCP servers and used their tools from the client side. You have probably never written the return value of one, so the mechanism deserves an introduction from zero rather than a reminder.
When a model calls a tool, the runtime sends the call out, gets something back, and appends that something to the conversation as a result block before asking the model to continue. The result block is just content — text, usually — plus one boolean that says whether this content represents a failure. In MCP, that boolean is the isError flag on the tool result. In the Messages API, where you assemble the conversation yourself, the same boolean is the is_error field on a tool_result content block. Two spellings, one idea: a channel for saying "this did not work" that still delivers the explanation into the model's context rather than out to a log.
The important consequence is what isError is not. It is not a thrown exception. A tool that fails does not throw across the protocol boundary; it returns normally, with the flag set and content describing what went wrong, and the model reads that content as the outcome of the call it just made. The turn continues. The model can act.
That gives you the distinction the exam cares about, between a protocol error and a tool error. A protocol error means the MCP server itself is broken — a malformed request, a transport failure — and the model generally never sees it. A tool error means the server worked perfectly and the operation it was asked to perform did not succeed. A payments API returning an upstream failure, a document that does not exist, a customer ID that fails validation: all tool errors, all belonging in an isError result where the model can read them. Routing an ordinary tool failure out as a protocol error, or letting an unhandled exception kill the turn, is the most common way teams accidentally build agents that cannot recover from anything — the Claude Code Bash loop you have watched work, but broken.
Four categories, one retryable
The exam names four error categories, and the boundaries between them are the single most testable fact in this chapter. Transient errors are timeouts and service unavailability. Validation errors are invalid input. Business errors are policy violations. Permission errors are insufficient or expired access. Only the first is retryable, and retrying any of the others is pure waste — the same input will fail the same way, and every attempt burns tokens and latency for nothing.
| Category |
Typical cause |
Retryable |
What the agent should do |
transient |
Upstream timeout, service unavailable, connection reset |
Yes |
Retry within a bounded budget, ideally inside the subagent; escalate only if the budget is exhausted |
validation |
Malformed or out-of-range input, missing required field |
No |
Correct the arguments from the error description and call again with a materially different input |
business |
Policy violation — refund window elapsed, item non-returnable |
No |
Stop, and explain the policy to the user in the customer-friendly terms the error supplies |
permission |
Missing scope, expired credential, record outside the caller's tenant |
No |
Stop and escalate to a human or to a differently-scoped path; never loop |
Notice that "not retryable" is not the same as "hopeless". A validation error is a highly actionable failure: the agent should absolutely call the tool again, but with corrected input, which is a different act from a retry. The category tells the model which of those two moves is available.
A wrinkle worth carrying into the exam room. The guide is not perfectly self-consistent about this taxonomy. Its knowledge statements name the four categories above, but the skills bullet under task 2.2 enumerates errorCategory more narrowly as transient, validation, and permission — business is absent from that list, and instead the guide describes business-rule violations as the case where the payload should specify retriable: false alongside a customer-friendly message. Keep the four-category model as your mental default, since it is the one the knowledge statements assert and the one the categorisation questions will assume, but recognise the three-value enumeration if you meet it, and remember that business violations are the case the guide attaches retriable: false to by name. Both framings describe the same behaviour; only the labelling differs.
Why "Operation failed" is the expensive answer
Uniform, generic error text is the anti-pattern the guide calls out by name, and the reason it names it is economic. If every failure comes back as Operation failed, the model cannot distinguish a network blip from a policy refusal. It has exactly one strategy available — try again — so it will retry the policy refusal, and the refund that is out of policy on attempt one is still out of policy on attempt four. Structured metadata is what prevents wasted retry attempts, and that phrase is close to verbatim in the syllabus.
A well-shaped payload carries errorCategory, an isRetryable boolean, and a human-readable description written for a reader who has to decide what to do next. For business-rule violations, add retriable: false alongside a customer-friendly explanation, so the agent can relay the policy to the end user in language that makes sense rather than treating the refusal as an obstacle to route around.
Python
import json
from dataclasses import dataclass, asdict
@dataclass
class ToolFailure:
errorCategory: str # transient | validation | business | permission
isRetryable: bool
description: str # for the model: what happened, what to try
customerMessage: str | None = None # for business errors: safe to relay verbatim
def process_refund(order_id: str, amount_cents: int) -> dict:
order = orders.get(order_id)
if order is None:
return error_result(ToolFailure(
errorCategory="validation",
isRetryable=False,
description=(
f"No order matches id '{order_id}'. Order ids look like 'ORD-8842911'. "
"Confirm the id with the customer before calling again."
),
))
if order.days_since_delivery > 30:
return error_result(ToolFailure(
errorCategory="business",
isRetryable=False,
description="Refund window closed; policy REFUND-30D. Do not retry.",
customerMessage=(
"This order was delivered 47 days ago and our refund window is 30 days, "
"so I can't process a refund automatically — but I can raise it with a specialist."
),
))
...
def error_result(failure: ToolFailure) -> dict:
return {
"isError": True,
"content": [{"type": "text", "text": json.dumps(asdict(failure))}],
}
TypeScript
type ErrorCategory = "transient" | "validation" | "business" | "permission";
interface ToolFailure {
errorCategory: ErrorCategory;
isRetryable: boolean;
description: string;
customerMessage?: string;
}
const errorResult = (failure: ToolFailure) => ({
isError: true,
content: [{ type: "text" as const, text: JSON.stringify(failure) }],
});
export async function processRefund(orderId: string, amountCents: number) {
const order = await orders.get(orderId);
if (!order) {
return errorResult({
errorCategory: "validation",
isRetryable: false,
description:
`No order matches id '${orderId}'. Order ids look like 'ORD-8842911'. ` +
"Confirm the id with the customer before calling again.",
});
}
if (order.daysSinceDelivery > 30) {
return errorResult({
errorCategory: "business",
isRetryable: false,
description: "Refund window closed; policy REFUND-30D. Do not retry.",
customerMessage:
"This order was delivered 47 days ago and our refund window is 30 days, so I can't " +
"process a refund automatically — but I can raise it with a specialist.",
});
}
// ...
}
The description field is doing more work than it looks. It is not a log line; it is a prompt fragment, and it lands in context with the same weight as anything you write in a CLAUDE.md. Write it the way you would write an instruction, because that is how it will be read.
Access failure is not an empty result
This distinction appears in both Domain 2 and Domain 5, which tells you the exam considers it load-bearing. A tool that could not reach its data source has failed and needs a retry decision. A tool that reached its data source, ran the query successfully, and matched nothing has succeeded — the answer is simply "none". Collapsing these two into the same shape destroys information in both directions: mark the timeout as an empty success and the agent confidently reports that there are no matching records when it has no idea; mark the empty match as an error and the agent burns its retry budget re-asking a question that has already been definitively answered.
The fix is to make them structurally different in the payload, not merely different in wording.
Python
def search_incident_reports(query: str, since: str) -> dict:
try:
rows = incident_index.search(query, since=since, timeout=20)
except SearchTimeout:
return error_result(ToolFailure(
errorCategory="transient",
isRetryable=True,
description=f"Incident index timed out after 20s for query {query!r}.",
))
payload = {
"isError": False, # explicit, not the absence of the field
"resultCount": len(rows), # 0 is a real, trustworthy answer
"results": rows,
"queryExecuted": query,
}
return {"isError": False, "content": [{"type": "text", "text": json.dumps(payload)}]}
TypeScript
export async function searchIncidentReports(query: string, since: string) {
try {
const rows = await incidentIndex.search(query, { since, timeoutMs: 20_000 });
const payload = { isError: false, resultCount: rows.length, results: rows, queryExecuted: query };
return { isError: false, content: [{ type: "text" as const, text: JSON.stringify(payload) }] };
} catch (err) {
if (err instanceof SearchTimeout) {
return errorResult({
errorCategory: "transient",
isRetryable: true,
description: `Incident index timed out after 20s for query "${query}".`,
});
}
throw err;
}
}
resultCount: 0 with no error flag is an assertion the agent can build on. It means someone looked.
Local recovery, then honest escalation
You already use subagents in Claude Code as a product feature: dispatch a piece of work, get back a summary rather than the whole transcript. Domain 5 asks you to think about that boundary as a designer, and the first question at the boundary is what happens when the dispatched work fails.
In a coordinator-subagent system, the subagent is the right place to absorb transient failure. It is closest to the tool, it knows how long the call should take, and its retry does not cost the coordinator a round trip or a context refill. So: handle transient errors locally, and propagate to the coordinator only what you could not resolve yourself — always with partial results and a record of what was attempted.
That last clause is where task 5.3 becomes specific. Structured error context has four parts: the failure type, what was attempted, any partial results, and potential alternative approaches. Each part maps to a decision the coordinator can now make. Failure type tells it whether retrying is even coherent. What was attempted stops it reissuing the identical query. Partial results let it decide whether it already has enough to proceed. Alternatives give it somewhere to go that is not simply "give up".
Python
async def market_research_subagent(topic: str) -> dict:
attempts, partial = [], []
for query in expand_topic(topic): # e.g. three narrower sub-queries
attempts.append(query)
try:
partial.extend(await web_search(query, timeout=25, retries=2))
except SearchTimeout:
continue # local recovery: keep going
if not partial:
return {
"status": "failed",
"failureType": "transient", # timeout, not "no such topic"
"attempted": attempts,
"partialResults": [],
"alternatives": [
"Retry with a single narrowed query such as 'EU battery recycling mandates 2026'",
"Route to the internal-archive subagent, which has cached analyst notes",
],
}
if len(partial) < expected_minimum(topic):
return {
"status": "partial",
"failureType": "transient",
"attempted": attempts,
"partialResults": partial,
"coverageGaps": ["regulatory outlook — all three source queries timed out"],
"alternatives": ["Re-run only the regulatory sub-query"],
}
return {"status": "complete", "results": partial}
TypeScript
export async function marketResearchSubagent(topic: string) {
const attempted: string[] = [];
const partial: Finding[] = [];
for (const query of expandTopic(topic)) {
attempted.push(query);
try {
partial.push(...(await webSearch(query, { timeoutMs: 25_000, retries: 2 })));
} catch (err) {
if (!(err instanceof SearchTimeout)) throw err; // local recovery only for transient
}
}
if (partial.length === 0) {
return {
status: "failed",
failureType: "transient",
attempted,
partialResults: [],
alternatives: [
"Retry with a single narrowed query such as 'EU battery recycling mandates 2026'",
"Route to the internal-archive subagent, which has cached analyst notes",
],
};
}
if (partial.length < expectedMinimum(topic)) {
return {
status: "partial",
failureType: "transient",
attempted,
partialResults: partial,
coverageGaps: ["regulatory outlook — all three source queries timed out"],
alternatives: ["Re-run only the regulatory sub-query"],
};
}
return { status: "complete", results: partial };
}
Walking sample question 8
The guide's eighth sample question puts a web search subagent in exactly this position: it times out mid-research, and you must choose how the failure reaches the coordinator. The correct answer is the structured payload above — failure type, attempted query, partial results, alternatives — and the three distractors are each instructive.
Retrying with exponential backoff inside the subagent and then returning a generic "search unavailable" status is wrong, and it is worth being precise about why, because the retry logic itself is fine and belongs there. The defect is the last step: a generic status hides valuable context from the coordinator. Everything the subagent learned — which queries it tried, what it did manage to collect before the timeout — is discarded at the boundary, and the coordinator is left with a decision it cannot make well.
Catching the timeout and returning an empty result set marked successful is worse, because it is a lie with a long half-life. It suppresses the error entirely, so no recovery can be attempted, and the synthesis step downstream will treat the absence of findings as evidence of absence. Terminating the workflow from a top-level exception handler fails in the opposite direction: it is honest but disproportionate, killing an entire research run when a narrowed retry or an alternative source would very likely have succeeded. This is the fail-fast instinct doing damage — the reflex that is right for a payment service and wrong for a research agent.
Those last two are the two named anti-patterns of task 5.3 — silently suppressing errors by returning empty results as success, and terminating an entire workflow on a single subtask failure. If a question offers you either, it is a distractor.
Coverage annotations: partial is fine, undisclosed is not
Once subagents report partial results truthfully, the synthesis step inherits an obligation. Its output should carry coverage annotations that distinguish findings that are well supported by retrieved sources from topic areas that have gaps because sources were unavailable. A report that says "competitor pricing: three independent sources; regulatory outlook: no sources retrieved, all queries timed out" is a useful report. The same report with the regulatory section quietly omitted is a hazard, because the reader cannot tell the difference between a topic that was investigated and found empty and one that was never reached.
Partial results are acceptable. Undisclosed partial results are not. That is the whole rule, and it is the reliability principle that ties Domain 5 together.
Idempotency, and the refund you paid twice
There is one failure mode that makes retries dangerous rather than merely wasteful, and it deserves its own treatment because the categories above will not save you from it. Consider process_refund. It calls the payment provider, the provider posts the refund, and then the response times out on the way back. The tool sees a transient error and reports it as retryable — correctly, by every rule in this chapter. The agent retries. The customer is refunded twice.
The category was right; the tool was not safe to retry. Retryability is a property of the operation as well as the error, and for any tool that mutates state you make retries safe by making the operation idempotent. The client supplies a key derived from the intent of the request, not from the attempt, and the server treats a repeated key as a request to return the original outcome rather than perform the work again.
Python
def refund_result(refund_id: str, replayed: bool) -> dict:
payload = {"isError": False, "refundId": refund_id, "replayed": replayed}
return {"isError": False, "content": [{"type": "text", "text": json.dumps(payload)}]}
def process_refund(order_id: str, amount_cents: int, idempotency_key: str) -> dict:
prior = refund_ledger.get(idempotency_key)
if prior is not None:
return refund_result(prior.id, replayed=True)
receipt = payments.refund(order_id, amount_cents, key=idempotency_key)
refund_ledger.put(idempotency_key, receipt)
return refund_result(receipt.id, replayed=False)
TypeScript
const refundResult = (refundId: string, replayed: boolean) => {
const payload = { isError: false, refundId, replayed };
return { isError: false, content: [{ type: "text" as const, text: JSON.stringify(payload) }] };
};
export async function processRefund(
orderId: string, amountCents: number, idempotencyKey: string,
) {
const prior = await refundLedger.get(idempotencyKey);
if (prior) return refundResult(prior.id, true);
const receipt = await payments.refund(orderId, amountCents, { key: idempotencyKey });
await refundLedger.put(idempotencyKey, receipt);
return refundResult(receipt.id, false);
}
Make the key a required parameter in the tool's input schema and describe it as identifying the refund attempt, so the model reuses the same value when it retries rather than minting a fresh one. The schema description is the only thing standing between you and a duplicate payment, because the caller here is a model choosing arguments from prose, not a client library you control. A key the agent regenerates per call is no protection at all.
Retry strategy as a design decision
Retries are a budget, not a reflex. Exponential backoff with jitter is the sane default for transient failures — backoff so a struggling dependency gets room to recover, jitter so a fan-out of parallel subagents does not resynchronise into a thundering herd on every retry boundary. Bound the attempts: two or three for an interactive turn where a human is waiting, more for a batch run where latency is cheap.
The consideration that is specific to agentic systems, and easy to miss coming from ordinary service engineering, is that every retry the model can observe costs tokens. Each failed attempt that surfaces as a tool result occupies context for the rest of the session, and a subagent that loops ten times leaves ten error messages sitting between the coordinator and the answer. In a conventional service, a retry loop is invisible to everything above it; here it is visible, and it crowds out the material the model needs to finish the task. So absorb the noisy retries locally, inside the subagent, and surface one summarised outcome. Set a per-turn attempt budget alongside the per-call one, and when it is exhausted, escalate with the four-part payload rather than continuing to spend.
What the exam tests
Expect the four categories to be tested directly, and expect at least one question to hinge on the fact that only transient errors are retryable. Know the isError flag by name as the MCP mechanism for communicating tool failure back to the agent, know is_error as its Messages API spelling, and know that the purpose of both is to make the failure visible to the model rather than to the operator. The access-failure versus valid-empty-result distinction appears in both Domain 2 and Domain 5, so treat any answer that marks a timeout as an empty success, or an empty match as an error, as wrong on sight. For multi-agent scenarios, the four-part structured payload — failure type, what was attempted, partial results, alternative approaches — is the shape the exam rewards, and generic statuses like "search unavailable" are always the trap, even when the retry logic wrapped around them is sound. Both named anti-patterns, suppressing errors as empty successes and terminating a workflow over one subtask failure, are reliable distractors. Remember that local recovery inside the subagent is the preferred first move for transient failure and that escalation should carry partial results rather than replace them. Finally, coverage annotations in synthesis output are examinable under task 5.3: the exam's position is that partial results are acceptable and undisclosed partial results are not.
Exercises
-
Write your first MCP tool, and make it a failing one. Wrap any external service you already have credentials for, then enumerate every exit path and assign each one an errorCategory, an isRetryable value, and a description written for a model that must decide what to do next. Connect it to Claude Code, trigger each failure deliberately, and watch the transcript: the agent should retry the timeout and stop dead on the policy refusal. Then replace every description with Operation failed and watch what changes.
-
Build a search tool whose empty result and whose timeout are structurally distinguishable, then run an agent against it with the data source deliberately unreachable. Confirm the agent retries. Now make the source reachable but the query unmatchable, and confirm the agent reports "no matches" without a single retry. If it retries in the second case, your empty result is still wearing an error's clothes.
-
Implement the market-research subagent from this chapter and force one of its three sub-queries to time out permanently. Verify that the coordinator receives partial results plus a populated coverageGaps field, and that the final synthesis names the gap rather than quietly omitting the section. Then break the subagent deliberately — have it return {"status": "complete", "results": []} — and write down exactly what the synthesis output claims. That output is the anti-pattern the exam is asking you to recognise.
-
Add a required idempotency key to a mutating tool and simulate a response timeout after the write has committed. Confirm that the retry replays the original receipt rather than performing the mutation twice, then change the tool description so the key sounds like a per-call unique identifier and watch the model mint a fresh one on retry. The failure this produces is the reason the parameter's description matters as much as its presence.
Chapter 5 — Orchestration
You have spawned subagents in Claude Code without ever thinking about how the mechanism underneath works. You ask for something broad — "research this library's migration path and summarize the breaking changes" — and Claude Code decides, on its own, to fan the work out: one subagent reads the changelog, another greps the codebase for affected call sites, a third drafts the summary. What you experience is a single coherent answer arriving at the end. What actually happened is a small distributed system: a coordinator that decided to delegate, several isolated workers that each saw only what they were told, and a synthesis step that had to reconcile results that never talked to each other directly.
This chapter is about building that system yourself, deliberately, rather than benefiting from it as a product feature. The reason to learn it even though Claude Code already does it for you is the same reason Chapter 2 had you write the agentic loop by hand: once you can name the mechanism, you can diagnose it when it breaks, and you can build the same pattern into an application that has no Claude Code wrapped around it at all — an Agent SDK program you write, where you are the one deciding when to spawn a subagent, what to tell it, and how to combine what comes back.
The running example is the exam's Multi-Agent Research System: a coordinator that takes a research question, delegates to search subagents and document-analysis subagents, and hands their output to a synthesis subagent that produces the final report. It is a good primary example precisely because every failure mode in this chapter shows up in it cleanly — narrow decomposition, lost context, sequential delegation masquerading as parallel, and synthesis that reads like a template because nobody gave it anything to work with. The customer support scenario reappears as a secondary anchor wherever a single-agent pattern and a multi-agent pattern need to be told apart.
Why orchestration is a separate problem from the loop
Chapter 2 established the agentic loop as one conversation, one messages array, growing turn by turn as tool results are appended. Everything in that chapter assumes a single agent with a single, ever-growing context. Orchestration is what happens when one agent's context is not the right shape for the whole problem — when the task decomposes into pieces that benefit from being handled by differently-scoped agents rather than by one agent doing everything with every tool in front of it at once.
Chapter 3 already gave you half the reason: an agent holding eighteen tools selects among them worse than one holding four or five, and an agent given tools outside its specialization reaches for them anyway. A research system that hands one agent web search, document loading, three extraction tools, and a report formatter is an agent that will sometimes write a report by half-reading a document instead of delegating the reading to something built for it. The other half is that a single growing conversation accumulates everything indiscriminately — full document text, every search result, every intermediate draft — and by the time synthesis needs to happen, the useful signal is buried in tens of thousands of tokens the model has to re-attend to. Splitting work across agents with separate, bounded contexts is a context-management technique as much as a division-of-labor technique, and Chapter 14 returns to that framing directly. This chapter is about the mechanics of doing the splitting correctly.
Hub-and-spoke: the coordinator owns everything
The architecture the exam wants you to default to is hub-and-spoke: one coordinator agent sits at the center, and every subagent is a spoke that talks only to the coordinator, never to each other. The coordinator decomposes the incoming task, delegates pieces to subagents, and aggregates what comes back. A search subagent finishing its work does not hand anything to the synthesis subagent directly — it returns to the coordinator, which decides what synthesis gets to see.
This is a constraint, not an accident of implementation, and giving up direct subagent-to-subagent communication buys three things the exam names specifically. The first is observability: every piece of information passes through one place, so a single log of coordinator activity tells the entire story of what was asked, delegated, and returned. The second is consistent error handling: when a subagent fails, one place decides whether to retry, substitute another subagent, or surface a partial result, rather than each subagent needing its own bespoke failure logic. The third is controlled information flow: the coordinator can filter, summarize, or redact before information reaches the next subagent, which matters when a document-analysis subagent has pulled something sensitive that the report subagent has no business seeing.
The customer support scenario makes the same shape concrete outside research. A case needing both lookup_order history and a fraud check before process_refund can run should route both through a coordinator that decides what to hand to which specialist, rather than letting a fraud-check agent page directly into the order-history agent's output — one place to see the whole case, one place to decide what happens if the fraud check comes back inconclusive.
Subagents have no memory of the coordinator, or of each other
The single fact underneath most of this chapter's failure modes is this: a subagent's context is isolated. It does not automatically inherit the coordinator's conversation history, and separate invocations of what looks like "the same subagent" do not share memory between calls. This cuts against instinct. In Claude Code, spawning a subagent feels a little like calling a well-briefed colleague who already knows the whole story, and it is tempting to assume the subagent read the room. It did not. The Task tool — the mechanism, introduced properly in the next section, that Claude Code uses under the hood every time it spawns a subagent — starts that subagent with nothing but the prompt you write for it. Whatever the coordinator learned earlier, whatever a prior subagent returned, exists in the new subagent's world only if you typed it into this prompt.
The exam's clearest illustration of the failure this produces is a synthesis subagent whose entire prompt is "Synthesize the research findings into a report." That subagent has no research findings. It has no access to what the web-search subagent found or what the document-analysis subagent extracted, because none of that was in its prompt — it was in the coordinator's history, and the coordinator's history is not this subagent's history. The output you get back is generic and uncited, not because the model is bad at synthesis, but because there is nothing to synthesize. The fix is not a bigger model or a WebSearch tool bolted onto the synthesis subagent so it can go find its own material — that just relocates the research work into the wrong agent and abandons the separation of concerns the whole architecture exists for. The fix is to put the actual findings into the prompt.
Python
# WRONG — the coordinator assumes shared memory that doesn't exist
synthesis_prompt = "Synthesize the research findings into a report."
# RIGHT — the coordinator passes what it actually collected
synthesis_prompt = f"""
Write a research report answering: {research_question}
You have no access to the search or document tools. Work only from the
findings below. Cite every claim using the bracketed source ids.
## Web search findings
{format_search_findings(search_results)}
## Document analysis findings
{format_document_findings(document_results)}
Quality criteria: every factual claim must carry a citation id. Flag any
subtopic in the original question that the findings below do not cover.
"""
TypeScript
// WRONG — the coordinator assumes shared memory that doesn't exist
const synthesisPrompt = "Synthesize the research findings into a report.";
// RIGHT — the coordinator passes what it actually collected
const synthesisPrompt = `
Write a research report answering: ${researchQuestion}
You have no access to the search or document tools. Work only from the
findings below. Cite every claim using the bracketed source ids.
## Web search findings
${formatSearchFindings(searchResults)}
## Document analysis findings
${formatDocumentFindings(documentResults)}
Quality criteria: every factual claim must carry a citation id. Flag any
subtopic in the original question that the findings below do not cover.
`;
Notice the last paragraph in the corrected prompt: it states a research goal and a quality bar — every claim cited, coverage gaps flagged — rather than a numbered procedure. That is the second skill this task statement tests: a coordinator prompt specifying what a good result looks like, not the steps to get there, leaves the subagent free to structure the report however the findings warrant, rather than forcing a rigid outline onto material that might not fit it.
Structured data over prose blobs, so attribution survives the handoff
Once you accept that findings must be pasted into the next agent's prompt, a second problem appears: how you paste them matters. If the web-search subagent's raw output is a paragraph of prose mixing claims with the URLs they came from, dropping that paragraph wholesale into the synthesis prompt forces the synthesis subagent to re-parse attribution out of running text, and it will sometimes get it wrong or drop it entirely — which is how you end up with a report stating something confidently with no way to trace it back to a source.
The fix is to separate content from metadata at the point where a subagent returns its result, using a structured format rather than a narrative one. Every finding a search or document subagent hands back should carry its source URL, or document name and page number, as a field alongside the content, not folded into it.
Python
search_findings = [
{
"claim": "The library dropped support for callback-style APIs in v5.",
"source_url": "https://example.com/library/changelog/v5",
"retrieved_at": "2026-08-30",
},
{
"claim": "Migration guides recommend the async adapter for v4 codebases.",
"source_url": "https://example.com/library/migration-guide",
"retrieved_at": "2026-08-30",
},
]
document_findings = [
{
"claim": "Internal usage of the callback API appears in 14 call sites.",
"document_name": "internal-audit-q3.pdf",
"page": 6,
},
]
TypeScript
const searchFindings = [
{
claim: "The library dropped support for callback-style APIs in v5.",
sourceUrl: "https://example.com/library/changelog/v5",
retrievedAt: "2026-08-30",
},
{
claim: "Migration guides recommend the async adapter for v4 codebases.",
sourceUrl: "https://example.com/library/migration-guide",
retrievedAt: "2026-08-30",
},
];
const documentFindings = [
{
claim: "Internal usage of the callback API appears in 14 call sites.",
documentName: "internal-audit-q3.pdf",
page: 6,
},
];
The format_search_findings and format_document_findings helpers referenced above take this structured form and render it into the prompt with the metadata still attached to each claim, rather than flattening it into an unattributed paragraph. This is the same discipline Chapter 3 asked of tool outputs — keep content and metadata as distinct fields so downstream consumers do not have to guess — applied to the handoff between agents instead of between a tool and a model.
The mechanism underneath all of this is the Task tool. It is, structurally, a tool like any other in Chapter 2's sense — a name, a description, an input schema — but its effect when Claude calls it differs from every tool you have written so far: instead of running a function and returning a value, it starts an entirely new agentic loop, with its own messages array, and returns that loop's final result as the tool result. This is the mechanism Claude Code uses under the hood every time it spawns a subagent on your behalf; writing your own coordinator with the Agent SDK means wiring up the same capability by hand.
Two configuration facts gate whether this works at all. First, a coordinator can only invoke the Task tool if "Task" appears in its allowedTools — leave it out and the coordinator has no way to delegate, no matter how clearly its system prompt says it should; this is the first thing to check when a coordinator that should be spawning subagents simply never does, since it is not reasoning about whether to delegate, it structurally cannot. (A naming note that matters for your own code but not for the exam: Claude Code renamed the Task tool to Agent in v2.1.63, keeping Task working as an alias. The guide says Task throughout, and so does the exam — answer Task.) Second, each subagent type the coordinator may spawn is described by an AgentDefinition — configuration giving each role a name, a description of when the coordinator should reach for it, a system prompt scoping its behavior, and a tool restriction. This is the API-level analogue of the subagent definitions you write for Claude Code's own subagent feature: a search_agent restricted to search and fetch tools, a document_agent restricted to document-loading and extraction, a synthesis_agent with no tools at all because its job is reasoning over what it is given.
Python
from claude_agent_sdk import ClaudeAgentOptions, AgentDefinition
options = ClaudeAgentOptions(
system_prompt=COORDINATOR_SYSTEM_PROMPT,
allowed_tools=["Task"],
agents={
"search_agent": AgentDefinition(
description="Runs web searches for a specific subtopic and "
"returns findings with source URLs attached.",
prompt=SEARCH_AGENT_SYSTEM_PROMPT,
tools=["WebSearch", "WebFetch"],
),
"document_agent": AgentDefinition(
description="Loads and extracts claims from a specific set of "
"documents, returning findings with document name "
"and page number attached.",
prompt=DOCUMENT_AGENT_SYSTEM_PROMPT,
tools=["load_document", "extract_data_points"],
),
"synthesis_agent": AgentDefinition(
description="Writes a cited research report from findings "
"supplied in its prompt. Has no research tools.",
prompt=SYNTHESIS_AGENT_SYSTEM_PROMPT,
tools=[],
),
},
)
TypeScript
import { AgentDefinition } from "@anthropic-ai/claude-agent-sdk";
const options = {
systemPrompt: COORDINATOR_SYSTEM_PROMPT,
allowedTools: ["Task"],
agents: {
search_agent: {
description: "Runs web searches for a specific subtopic and returns " +
"findings with source URLs attached.",
prompt: SEARCH_AGENT_SYSTEM_PROMPT,
tools: ["WebSearch", "WebFetch"],
} satisfies AgentDefinition,
document_agent: {
description: "Loads and extracts claims from a specific set of " +
"documents, returning findings with document name and page " +
"number attached.",
prompt: DOCUMENT_AGENT_SYSTEM_PROMPT,
tools: ["load_document", "extract_data_points"],
} satisfies AgentDefinition,
synthesis_agent: {
description: "Writes a cited research report from findings supplied " +
"in its prompt. Has no research tools.",
prompt: SYNTHESIS_AGENT_SYSTEM_PROMPT,
tools: [],
} satisfies AgentDefinition,
},
};
A caveat on the allowed_tools=["Task"] line in that configuration, in the same spirit as the naming note above. The guide treats it as the gate that permits delegation, and that is the exam's answer. The SDK documents it as an auto-approve list — its own subagent examples annotate allowedTools with the comment "auto-approve these tools" — and states plainly that it "does not restrict Claude to only these tools." Omitting "Task" therefore will not reliably stop a coordinator from delegating in your own code, though it remains the correct answer to "why won't my coordinator spawn subagents?" on the exam. Note the contrast with AgentDefinition.tools in the same snippet, which is a real fence: a tool left out of that list "isn't in the subagent's session at all."
Read the description field on each AgentDefinition the way you read a tool description in Chapter 3: it is what the coordinator sees when deciding which subagent to invoke, and a vague description produces the same misrouting between subagent types that a vague tool description produces between tools. synthesis_agent holding no tools at all is not an oversight — it enforces, at the configuration level, the scoping argument from Chapter 3: this agent's job is reasoning over supplied material, and a search tool would let it wander into doing the search subagent's job badly instead of its own job well.
The three examples above show description, prompt, and tools because those are the fields the coordinator's routing decision depends on most directly, but current AgentDefinition carries more surface than that, and it's worth knowing exists even where the exam doesn't test it field-by-field. disallowedTools removes specific tools — including mcp__server or mcp__* patterns — from an otherwise-permissive role, so you can carve out exceptions without hand-listing everything else that role is allowed. model lets a subagent run on a different model than the coordinator: a full model ID, an alias, or the literal string 'inherit' to match whatever the coordinator itself is running on — useful when a search subagent doesn't need the coordinator's reasoning budget but a synthesis subagent does. skills, memory ('user', 'project', or 'local'), mcpServers, and initialPrompt give a subagent its own skill set, memory scope, MCP connections, and a seed message, independent of the coordinator's. maxTurns caps how long a subagent's own loop can run before it's forced to return whatever it has — the same bounded-loop discipline Chapter 4 asks of retries, applied to subagent depth instead of retry count — and a subagent that hits the cap returns a resumable partial result rather than simply failing outright. background lets a subagent run without blocking the coordinator's own turn, effort tunes how much a subagent reasons before answering, and permissionMode scopes what a subagent may do without asking, independently of the coordinator's own permission mode.
Resuming a subagent
Chapter 7 covers session resumption for a top-level conversation in full; the same underlying mechanism reaches down into subagents. A Task/Agent tool result carries a session_id (surfaced in current tooling as agentId alongside it) identifying that specific subagent's own transcript, not just its final answer. Capturing that id and passing it back as resume: sessionId on a later Task call continues that exact subagent's history, rather than starting a new one from nothing — useful when a document-analysis subagent has already done substantial work establishing context about a document set, and a later coordinator turn needs it to look at one more thing within that same set instead of re-establishing the context from zero. This is the subagent-level version of the same resume-versus-restart judgment call Chapter 7 makes for top-level sessions: resume when the subagent's prior context is still valid and worth what it cost to build, spawn a fresh Task call when it isn't.
Spawning in parallel: multiple Task calls, one turn
Chapter 2 established that a single assistant turn can carry several tool_use blocks, and that Claude parallelizes calls it believes are independent. The Task tool is subject to the same mechanic, and it is the whole answer to how a coordinator runs subagents concurrently rather than one after another: emit multiple Task tool calls in one coordinator response. Four Task calls in a single turn are four subagents running side by side. One Task call per turn, four turns in a row, is four subagents running in sequence, and the wall-clock cost of that difference is not small when each subagent is itself an agentic loop with several tool calls of its own inside it.
There is no separate parallelism flag to set for this. A distractor worth watching for is a plausible-sounding API parameter that actually governs something else, such as disable_parallel_tool_use from Chapter 2, which controls whether a single turn can batch ordinary tool calls at all and has nothing to do with subagent orchestration specifically. The thing that makes delegation parallel is structural: multiple Task calls, one coordinator turn.
This is a level below what the raw Messages API gives you directly: the coordinator's underlying turn contains four tool_use blocks, one per Task call, all in the same turn, exactly as Chapter 2 described for ordinary tools — but with the Agent SDK's query() running the loop, it is the SDK's harness, not your own code, that sees those four blocks and dispatches the four subagent loops concurrently. You never touch a tool_use block directly at this layer; you supply the options from the previous section and read typed messages back.
Python
from claude_agent_sdk import query
async for message in query(
prompt="Research the market impact of generative AI on creative "
"industries: visual arts, music, writing, and film/video, "
"each as a distinct workstream.",
options=options, # the ClaudeAgentOptions defined above
):
log.info("%s", message)
# Internally, the coordinator's turn carries four Task tool_use blocks
# naming "search_agent", each with a distinct, non-overlapping subtopic
# in its input — the SDK runs the four resulting subagent loops concurrently.
TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Research the market impact of generative AI on creative " +
"industries: visual arts, music, writing, and film/video, each as " +
"a distinct workstream.",
options, // the ClaudeAgentOptions defined above
})) {
log.info(message);
}
// Internally, the coordinator's turn carries four Task tool_use blocks
// naming "search_agent", each with a distinct, non-overlapping subtopic
// in its input — the SDK runs the four resulting subagent loops concurrently.
Parallel delegation is not unbounded in practice, and current tooling guards against runaway fan-out with defaults worth knowing exist even if you never hit them: CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH caps how many levels deep a subagent may itself spawn further subagents (default 3), and CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS caps how many subagent loops may run at once (default 20). A separate maxBudgetUsd option (max_budget_usd in Python) caps total spend across a coordinator's whole delegation tree; a run that would exceed it stops and reports an error_max_budget_usd result subtype rather than silently continuing to spend past the ceiling. These exist for the same reason Chapter 4 wants an explicit cap on any retry loop: a mechanism that can spawn more of itself needs a hard ceiling, not an assumption that the model will decide on its own to stop.
Partitioning scope without narrowing the topic
The prompt in that last example names the failure mode this chapter has been building toward. "Creative industries" is broad. A coordinator that decomposes it into four subagents each assigned a distinct, named workstream — visual arts, music, writing, film/video — has partitioned scope correctly: no duplication, because no two subagents are researching the same slice, and full coverage, because the four slices add up to the whole topic. That is the skill the exam calls partitioning research scope to minimize duplication, and it looks, on paper, exactly like good decomposition.
Now change one thing: instead of visual arts, music, writing, and film, the coordinator decomposes "creative industries" into three subtasks that are all, in substance, about visual arts — gallery representation, digital art marketplaces, and museum curation practices. Every one of those three subagents will succeed and return well-researched, well-cited findings about its slice. The synthesis subagent, working faithfully from what it was given, will produce a coherent, confident report that covers about a quarter of what "creative industries" actually means, because music, writing, and film never got assigned to anyone. Nothing failed. Every log entry is green. And the aggregate answer is wrong.
This is the failure mode to hold onto as a single sentence: when every subagent executes correctly and the aggregate coverage is still wrong, the defect is not downstream — it is in how the coordinator decomposed the topic before any subagent ran. It is easy to miss because nothing in the execution trace looks like an error; error handling has nothing to catch, because nothing errored. The only way to catch it is to evaluate the shape of the decomposition itself against the shape of the original question, before delegation, and again against the synthesized output, after — which is exactly what the next section builds a loop around.
Iterative refinement: the coordinator checks its own coverage
A coordinator that decomposes once, delegates once, and synthesizes once has no mechanism for catching the narrow-decomposition failure after the fact, because by the time synthesis runs the damage is already baked into what the subagents were asked to look for. The correction is to make delegation-then-synthesis a loop rather than a pipeline: the coordinator evaluates the synthesis output for gaps against the original question, re-delegates to search or document subagents with targeted queries aimed at any gap it finds, then re-invokes synthesis with the enlarged findings set — repeating until coverage looks sufficient against the original scope, not against whatever the first pass happened to produce.
Python
def run_research_coordinator(question, max_refinement_rounds=3):
findings = delegate_initial_research(question) # parallel Task calls
report = invoke_synthesis(question, findings)
for round_num in range(max_refinement_rounds):
gaps = evaluate_coverage(question, report) # coordinator's own check
if not gaps:
return report
log.info("refinement round=%d gaps=%s", round_num, gaps)
targeted_findings = delegate_targeted_research(gaps) # more Task calls
findings = findings + targeted_findings
report = invoke_synthesis(question, findings)
report["coverage_warning"] = (
"Refinement rounds exhausted; known gaps may remain unresolved."
)
return report
TypeScript
async function runResearchCoordinator(
question: string,
maxRefinementRounds = 3,
) {
let findings = await delegateInitialResearch(question); // parallel Task calls
let report = await invokeSynthesis(question, findings);
for (let round = 0; round < maxRefinementRounds; round++) {
const gaps = await evaluateCoverage(question, report); // coordinator's own check
if (gaps.length === 0) return report;
log.info({ round, gaps });
const targetedFindings = await delegateTargetedResearch(gaps); // more Task calls
findings = [...findings, ...targetedFindings];
report = await invokeSynthesis(question, findings);
}
report.coverageWarning =
"Refinement rounds exhausted; known gaps may remain unresolved.";
return report;
}
evaluate_coverage is doing the real work here: the coordinator compares the breadth of the original question against the breadth of what was actually delegated and synthesized, looking for named subtopics that never got a subagent, not just checking that the subagents that did run produced non-empty output. That distinction is what would have caught the "creative industries" failure rather than rubber-stamping it, since the three visual-arts subagents in that failure all returned perfectly good, non-empty findings. Note also the bounded loop and the explicit warning on exhaustion — the same discipline Chapter 4 asks of any retry mechanism: a cap that fails loud, never one that silently returns an incomplete result as though it were complete.
Query complexity: not every question needs the full pipeline
The flip side of under-decomposing a broad topic is over-processing a narrow one. Running "what year was a well-known data protection regulation enacted" through web search, document analysis, and synthesis in sequence produces a right answer while burning three agent invocations' worth of latency and cost on a question a single well-scoped lookup answers completely. The coordinator's job includes deciding which subagents a given query actually needs, not mechanically routing every query through the full pipeline — a judgment its own system prompt has to make room for explicitly: assess whether the question is a simple lookup, a comparison, or an open-ended investigation, and invoke only the subagents that assessment calls for. Left to a default of "always run the full team," a coordinator will happily spend four invocations answering a question one could answer alone. The support scenario gives the mirror image: a case that is unambiguously "customer wants an existing order's status" does not need a fraud-check subagent invoked alongside the order lookup.
Fork-based sessions, briefly
One more mechanism belongs in this chapter's vocabulary even though its full treatment lives elsewhere: fork-based session management, which lets you branch an agent's session into independent copies from a shared baseline, so two divergent approaches can be explored from the same accumulated analysis without either disturbing the other or the original. In the research system, this looks like reaching a well-developed shared understanding of a topic and then exploring two different framings of the final report from that same baseline, without making the second framing pay the cost of re-deriving what the first already established. It is a session-management mechanism rather than a coordinator-subagent one — the chapter on sessions covers resumption and forking in full. Here, know only that it exists and what problem it solves.
Scaling past hub-and-spoke, and the default subagent
Everything above assumes a coordinator that decomposes a task into a handful of named roles and delegates within its own conversation. That stops scaling somewhere between a handful of subagents and the dozens-to-hundreds an exam scenario might describe for a genuinely large migration or audit. For that scale, current tooling adds a Workflow tool, distinct from Task/Agent, that orchestrates many agents outside the coordinator's own conversation context entirely, rather than adding more tool_use blocks to one growing turn. Hub-and-spoke, as taught in this chapter, keeps every delegation and every result inside the coordinator's own context — which is exactly what buys the observability and controlled-information-flow properties from the first section — and Workflow exists for scale that would blow that context up long before the work finishes. Treat the two as answers to different problem sizes, not as a replacement of one by the other: nothing about this chapter's hub-and-spoke argument stops applying at small-to-moderate scale.
One more mechanical fact worth having: a coordinator can invoke a subagent named general-purpose even with zero agents defined in its own configuration — a built-in fallback role that's always available unless explicitly disabled with CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS=1. This is why a coordinator with an empty or minimal agents map can still delegate at all: it is never delegating into a void, it falls through to this default role whenever nothing more specific was defined for the task at hand.
What the exam tests
Task statements 1.2 and 1.3 are both diagnostic, in the register of Chapter 2's task statement 1.1: a multi-agent system exhibits a symptom and you name the cause. The two symptoms to have reflexively ready are a coordinator that cannot spawn subagents at all — check allowedTools for "Task" — and a subagent whose output is generic or uncited, almost always solved by putting the missing findings directly into that subagent's prompt rather than by adding tools or restructuring turns. Expect a question on parallel delegation where the answer is "multiple Task calls in a single coordinator response," with distractors offering real-sounding API parameters that govern something adjacent but different. Expect the narrow-decomposition trap in a form where every subagent's execution log is clean and the defect is only visible by comparing the original question's breadth against what was actually delegated — the exam wants the defect located upstream, in decomposition, not in any subagent's execution. Also expect a question on routing every query through the full pipeline versus letting the coordinator assess complexity first, and one distinguishing hub-and-spoke's benefits — observability, consistent error handling, controlled information flow — from an architecture where subagents communicate directly. Structured data with separated content and metadata for attribution, and coordinator prompts stating goals and quality criteria rather than procedure, both show up as the "right for a reason" choice against mechanical-sounding distractors.
Exercises
- Build the Multi-Agent Research System coordinator with three
AgentDefinition entries — search_agent, document_agent, synthesis_agent — scoped tools per Chapter 3's rules, and deliberately write the synthesis prompt as just "Synthesize the research findings into a report." Run it, observe the generic uncited output, then fix the prompt to include structured findings with attribution and re-run. Keep both outputs side by side.
- Take a deliberately broad question — "the effects of remote work on urban commercial real estate" is a reasonable stand-in for "creative industries" — and have a coordinator decompose it into subagents twice: once narrowly, on purpose, choosing three subtopics that are all really the same angle, and once broadly, choosing subtopics that partition the real breadth of the question. Compare the two synthesized reports and write down, in one sentence, what a reader of the narrow one would wrongly conclude is complete coverage.
- Implement the iterative refinement loop from this chapter against exercise 2's narrow decomposition, with
evaluate_coverage doing an honest comparison against the original question's breadth. Confirm it detects the gap, re-delegates with a targeted query for the missing subtopics, and produces a second synthesis pass that actually closes the gap — then remove the loop's cap and watch what happens if evaluate_coverage is too strict ever to be satisfied.
- Modify the coordinator's system prompt to assess query complexity before delegating, and drive it with three questions of deliberately different weight: a single-fact lookup, a two-way comparison, and an open-ended investigation. Log which subagents get invoked for each and confirm the simple lookup does not trigger the full pipeline.
Chapter 6 — Hooks and gates
Every day, Claude Code stops before running a command that touches your filesystem or your shell, and asks you to approve it. You have never thought of that prompt as an architectural pattern, because it feels like a courtesy rather than one. But strip away the UI and look at the mechanism: the model emitted a tool_use block, and before that call reached the real world, code you didn't write inspected it and had the power to refuse. Claude was never asked nicely not to run rm -rf. The loop was stopped from outside the model's own reasoning, at a point the model has no say over.
That mechanism — inspecting a call before it executes, or a result after it returns, and doing something deterministic about it — is the subject of this chapter, and the exam guide calls it arguably the single highest-yield idea in the syllabus, for a blunt reason: prompt instructions have a non-zero failure rate. Write "always verify the customer before processing a refund" into a system prompt as carefully as you like, and across enough conversations, enough context pressure, enough adversarial phrasing, some fraction of runs will process the refund anyway. That is not a hypothesis about model quality; it is a structural fact about instructions living in the same probabilistic medium as everything else the model reasons over. An instruction competes with every other token in context for the model's attention, and competition sometimes loses. A gate does not compete — it is code that runs, checks a condition, and returns a fixed answer regardless of what the model was thinking when it asked.
The customer support resolution scenario is where this distinction earns its keep, because it is the scenario where a prompt failure has a dollar amount attached. An agent with get_customer, lookup_order, process_refund, and escalate_to_human handles the ordinary case of a support conversation fine on its own — which order, which policy, which explanation to give the customer are things the model decides well turn by turn, the way Chapter 2 described the loop as model-driven rather than a decision tree. But two properties of that same agent are not "the model usually gets this right" properties. They are "this must never be false" properties: a refund must never be issued against an unverified identity, and a refund must never exceed the policy threshold without a human in the loop. Those two sentences are the spine of this chapter.
You already have the vocabulary for this from Claude Code itself. PreToolUse and PostToolUse are events Claude Code's own hook system exposes around exactly these two seams — before a tool call executes, and after a tool result comes back — and the permission prompt you approve or deny every day is a PreToolUse hook wearing a UI. This chapter builds the same two seams by hand, at the level where you are the one writing the agent rather than configuring the product. Claude Code's hook configuration and event catalogue belong to the chapters on configuring and extending Claude Code; what belongs here is the pattern underneath both surfaces, since the exam tests the pattern, not the config file.
The enforcement ladder
Line up the three ways to make an agent do something in a fixed order, and what each one actually guarantees becomes concrete rather than a matter of taste.
| Mechanism |
What it constrains |
Scope |
Guarantee |
| System prompt instruction |
What the model is told to prefer |
Every turn, informally |
None — the model can be wrong or overridden by competing context |
tool_choice forcing |
Which tool the next response must call |
One request |
Strong, but only for a single turn |
| Programmatic gate or hook |
Whether a call is allowed to execute, or what a result looks like once it returns |
The whole session, or however long your code holds the state |
Absolute — the check runs in your process, not the model's |
Chapter 3 already promised programmatic gates their proper treatment here, and the reason tool_choice cannot do the job alone is worth restating, since it is the trap the exam sets. Forcing {"type": "tool", "name": "get_customer"} guarantees only that the next response calls get_customer. It says nothing about whether process_refund gets called three turns later without a verified identity in between, because by then you are back on "auto" and the forcing has expired. A prerequisite that must hold across an open-ended, multi-turn conversation cannot live in a parameter describing only the very next response — it has to live in code that persists for the life of the conversation and inspects every call as it goes by, which is what a gate is.
The instinct to reach for a stronger prompt instead is understandable, because it is cheap and it usually works. That "usually" is the whole argument: a support agent handling ten thousand conversations a month does not get to rely on "usually" for the operation that moves money out of the company's account. Choose deterministic enforcement specifically when the consequence is financial, compliance-related, or security-related, or when correctness depends on an ordering that must never be skipped — identity before payment being the canonical case. Everything short of that consequence tier is fair game for a good prompt, and reaching for a hook there is itself a mistake this chapter comes back to.
Prerequisite gates: blocking the call, not asking nicely
A prerequisite gate is code that sits between the model asking for a tool and that tool actually running, checking whether some earlier condition has been satisfied, and refusing the call if it has not. The canonical instance, named directly in the exam guide, is blocking process_refund until get_customer has returned a verified customer ID. Nothing exotic about the implementation — this is a dictionary lookup and an if statement — but two details separate a gate that works from one that looks like it works and quietly doesn't.
The first is state scope: the gate has to track "has this conversation verified a customer" per conversation, not as a process-global flag. A single agent process serving concurrent support sessions with one shared boolean will let session B's refund through on the strength of session A's verification — a worse bug than no gate at all, because it fails silently and looks like success. Key the verification state by the same conversation identifier you'd use for persisted history.
The second is what the gate returns when it fires, and this is where Chapter 4's contract about errors reaching the model as data, not as an exception that kills the turn, applies directly. A gate that raises when it blocks a call terminates the loop the same way an uncaught tool exception does — the model never finds out why, and the support engineer sees a stack trace instead of a resolved ticket. A gate that returns a normal tool_result with is_error set, and a description explaining what is missing, lets the model recover the way it recovers from every other tool failure: call get_customer first, then retry. The gate should feel like just another fact about the world, delivered through the channel the model already knows how to read.
Python
verified_customers: dict[str, str] = {} # conversation_id -> customer_id
def gated_process_refund(conversation_id: str, order_id: str,
amount_cents: int, reason: str) -> dict:
customer_id = verified_customers.get(conversation_id)
if customer_id is None:
return {
"is_error": True,
"content": (
"process_refund blocked: no verified customer on this "
"conversation. Call get_customer first, then retry with "
"the returned customer_id."
),
}
return process_refund(customer_id, order_id, amount_cents, reason)
def gated_get_customer(conversation_id: str, identifier: str) -> dict:
result = get_customer(identifier)
if result.get("verified"):
verified_customers[conversation_id] = result["customer_id"]
return result
TypeScript
const verifiedCustomers = new Map<string, string>(); // conversationId -> customerId
async function gatedProcessRefund(
conversationId: string, orderId: string, amountCents: number, reason: string,
) {
const customerId = verifiedCustomers.get(conversationId);
if (!customerId) {
return {
is_error: true,
content:
"process_refund blocked: no verified customer on this conversation. " +
"Call get_customer first, then retry with the returned customer_id.",
};
}
return processRefund(customerId, orderId, amountCents, reason);
}
async function gatedGetCustomer(conversationId: string, identifier: string) {
const result = await getCustomer(identifier);
if (result.verified) verifiedCustomers.set(conversationId, result.customerId);
return result;
}
The third detail is scope of the gate itself: gate the mutation, not every neighbouring read. process_refund changes money; it needs the gate. lookup_order reads a record, and gating it behind the same verification buys you nothing but a support agent that cannot tell a customer what's in their cart before confirming identity — worse for the majority of contacts that never touch a refund. A sample question on this exact scenario offers, as a wrong answer, a design that gates both lookup_order and process_refund — wrong not because over-gating is unsafe, but because it is disproportionate. Match the gate to the operation whose failure mode you're actually defending against; every gated call is a code path with its own bugs, and a gate placed where it isn't needed is friction with no safety gain.
Direction is not a detail: interception versus PostToolUse
This next distinction is flagged as an easy distractor, and it earns that reputation because the two mechanisms look similar in a diagram and are opposite in what they can do. A hook that inspects a tool's result after it comes back — the PostToolUse pattern — is looking at something that already happened. A hook that inspects a tool call before it is dispatched — interception, sometimes called a pre-call or PreToolUse hook — is looking at something that has not happened yet, and can still say no.
For a refund, that difference is not academic. By the time process_refund has returned a result, the payment provider has already moved money. A PostToolUse hook reading that result can log it, flag it for audit, page someone — all useful — but it cannot un-refund the customer. If the business rule is "refunds over $500 must never go out without a human," the only place that rule can live is before the call executes, which means interception.
Python
REFUND_THRESHOLD_CENTS = 50_000
def intercept_process_refund(conversation_id: str, order_id: str,
amount_cents: int, reason: str) -> dict | None:
"""Return a blocking result if the call must not proceed, else None."""
if amount_cents > REFUND_THRESHOLD_CENTS:
return {
"is_error": True,
"content": (
f"process_refund blocked: ${amount_cents/100:.2f} exceeds the "
"$500 auto-approval threshold. Call escalate_to_human with a "
"root cause and recommended action instead."
),
}
return None
def dispatch_process_refund(conversation_id, order_id, amount_cents, reason):
blocked = intercept_process_refund(conversation_id, order_id, amount_cents, reason)
if blocked is not None:
return blocked
return gated_process_refund(conversation_id, order_id, amount_cents, reason)
TypeScript
const REFUND_THRESHOLD_CENTS = 50_000;
function interceptProcessRefund(
conversationId: string, orderId: string, amountCents: number, reason: string,
) {
if (amountCents > REFUND_THRESHOLD_CENTS) {
return {
is_error: true,
content:
`process_refund blocked: $${(amountCents / 100).toFixed(2)} exceeds the ` +
"$500 auto-approval threshold. Call escalate_to_human with a root cause " +
"and recommended action instead.",
};
}
return null;
}
async function dispatchProcessRefund(
conversationId: string, orderId: string, amountCents: number, reason: string,
) {
const blocked = interceptProcessRefund(conversationId, orderId, amountCents, reason);
if (blocked) return blocked;
return gatedProcessRefund(conversationId, orderId, amountCents, reason);
}
Two things about that example carry weight. The blocked result redirects rather than merely refusing — it names escalate_to_human explicitly, so the agent has somewhere productive to go rather than retrying the same blocked call or giving up on the customer. And this sits in exactly the seam Chapter 2 pointed at in Claude Code's own permission prompt: the gap between the model emitting a tool_use block and your code executing it, where nothing has run yet and a fixed rule can stand in for the human who'd otherwise decide.
A refund-threshold question typically offers four options: a system prompt instruction telling the model to escalate refunds over $500; a few-shot example of the model escalating a $600 refund; interception that blocks calls above $500 and redirects to escalation; and a PostToolUse hook that flags refunds above $500 for after-the-fact audit. The first two are probabilistic — exactly the prompt-based guidance this chapter argues is insufficient for a financial consequence. The fourth is the direction trap: the right hook family, aimed at the right threshold, and it will still let every over-threshold refund go out, because by the time it fires the money has moved. Only interception blocks the call before it happens.
PostToolUse: normalizing what comes back
None of this makes PostToolUse a lesser mechanism — it solves a different problem, and the problem it solves well is data quality rather than compliance. Support tooling in a real organization is rarely one system: lookup_order might hit a fulfilment service returning Unix epoch timestamps, get_customer might come from an identity service returning ISO 8601 strings, and a third integration might represent account status as a numeric code — 0, 1, 2 — with the meaning living in a lookup table nobody attached to the response. None of these formats is wrong for the system that produces it; all of them are a problem for the model reasoning across all three in one turn, doing format translation as a side effect of its actual job — exactly the kind of mechanical task a probabilistic reader gets right most of the time and wrong on the input that looks slightly different from the others.
A PostToolUse hook intercepts the result coming back from any of these tools and rewrites it into one shape before the model ever sees it — same field names, same date format, same status vocabulary — regardless of which backend produced the raw response.
Python
from datetime import datetime, timezone
STATUS_CODES = {0: "active", 1: "suspended", 2: "closed"}
def normalize_tool_result(tool_name: str, raw: dict) -> dict:
result = dict(raw)
if tool_name == "lookup_order" and isinstance(result.get("ship_date"), int):
result["ship_date"] = datetime.fromtimestamp(
result["ship_date"], tz=timezone.utc
).date().isoformat()
if tool_name == "get_customer" and isinstance(result.get("status"), int):
result["status"] = STATUS_CODES.get(result["status"], "unknown")
return result
TypeScript
const STATUS_CODES: Record<number, string> = { 0: "active", 1: "suspended", 2: "closed" };
function normalizeToolResult(toolName: string, raw: Record<string, unknown>) {
const result = { ...raw };
if (toolName === "lookup_order" && typeof result.ship_date === "number") {
result.ship_date = new Date(result.ship_date * 1000).toISOString().slice(0, 10);
}
if (toolName === "get_customer" && typeof result.status === "number") {
result.status = STATUS_CODES[result.status as number] ?? "unknown";
}
return result;
}
Wire this in at the same place execute_tool builds the tool_result block from Chapter 2's loop: call the tool, pass the raw output through normalize_tool_result, then serialize the normalized version into the content field. The model never sees a Unix timestamp or a bare status code across the whole session.
Building it by hand this way is the point of the exercise, but it is worth knowing what the Agent SDK calls the same two seams, since task statement 1.5 is phrased in SDK terms. A PreToolUse callback returns a hookSpecificOutput object carrying permissionDecision ("allow", "deny", "ask", or "defer") with a permissionDecisionReason, and can rewrite the call's arguments with updatedInput instead of blocking outright. A PostToolUse callback returns additionalContext to append to a result, or updatedToolOutput to replace the tool's output entirely before Claude sees it — which is the field that makes the normalization pattern above a real SDK capability rather than a hand-rolled convention.
The reason this is a hook problem rather than a prompt problem is the same non-zero-failure-rate argument, applied to a different failure mode. Writing "dates may arrive as Unix timestamps or ISO strings; interpret carefully" into the system prompt works most of the time, at the cost of tokens spent every turn re-deriving a fact that is fixed and knowable in advance — and it is unfixable for the cases that matter most: if lookup_order is a third-party MCP server you did not write, no amount of prompting the model changes what that server returns. You can only change what the model is shown, which means intercepting the result on the way in. A PostToolUse hook does the conversion exactly once, and every subsequent turn benefits from already-clean data.
The counterweight: not every rule needs a hook
Everything above argues for programmatic enforcement, and it would be a mistake to close this section without the argument's limit. A hook is code, and code has bugs, needs maintaining, and adds a seam that can fail in ways a prompt cannot. If the rule in question is a soft preference — phrase refunds politely, prefer store credit before cash, mention the loyalty program when relevant — a hook is over-engineering; a well-written system prompt handles all three adequately, the cost of an occasional miss is low, and a hook enforcing "must mention the loyalty program" is a brittle thing to write for a stylistic nicety.
The test to apply is not "could I write a hook for this" — you can write one for almost anything — but "does the consequence of a probabilistic miss justify code that can never be skipped." Money moving, an identity check being skipped, a compliance rule being violated: yes. Tone, phrasing, which of two acceptable orderings the model prefers: no. Over-gating a support agent until every soft preference is a hard check reintroduces the rigidity that made an agent worth building in the first place — the same complaint Chapter 2 levels at hard-coded decision trees, just one layer lower.
Multi-concern decomposition: one customer, several problems
A support conversation is frequently not one request. "My order SO-44120 arrived damaged, and while I have you, I was also charged twice for last month's subscription" is two concerns wearing one message, and the weakest handling is sequential: resolve the damaged order, then tell the customer to open a second ticket for the billing issue, or handle the two one after another when nothing about them is actually dependent. Both are named failure patterns, and both leave first-contact resolution — the headline metric for this scenario — worse than it needed to be.
The pattern to use instead decomposes the message into distinct items, investigates each in parallel using shared context, and synthesizes one unified resolution. "Shared context" is the part that is easy, and expensive, to skip: fetch the customer record once, and hand that same verified customer_id to both investigations, rather than each branch independently calling get_customer and burning a call — and, if the gate above is doing its job, needlessly re-verifying — for information already in context. Chapter 2 showed the loop readily emitting several tool_use blocks in one turn when it judges the calls independent; a customer disputing an order and a billing charge in the same message is exactly that case, since the two lookups have no dependency on each other once the shared customer record is in hand.
Python
async def resolve_multi_concern(conversation_id: str, customer_identifier: str,
concerns: list[dict]) -> dict:
customer = gated_get_customer(conversation_id, customer_identifier)
findings = await asyncio.gather(*(
investigate(concern, customer_id=customer["customer_id"])
for concern in concerns
))
return synthesize_resolution(customer, concerns, findings)
TypeScript
async function resolveMultiConcern(
conversationId: string, customerIdentifier: string, concerns: Concern[],
) {
const customer = await gatedGetCustomer(conversationId, customerIdentifier);
const findings = await Promise.all(
concerns.map((concern) => investigate(concern, customer.customerId)),
);
return synthesizeResolution(customer, concerns, findings);
}
The synthesis step is where the two investigations become one answer rather than two stapled together: a single reply addressing both the shipment and the duplicate charge, each with its own resolution, in one coherent message. This is orchestration mechanics in miniature — the fuller treatment of fan-out and result aggregation belongs to the chapter on orchestration — but the judgment call specific to this scenario is recognizing multi-concern messages as a decomposition problem in the first place, rather than reflexively escalating just because the message was compound: an agent capable of handling both concerns should handle both, not hand the customer to a human for that reason alone.
Structured handoff: what the human on the other end actually needs
Every enforcement mechanism in this chapter eventually produces the same outcome for the cases it correctly refuses to let through: a human takes over. escalate_to_human fires when interception blocks a refund over threshold, when policy is silent on a request, when the customer asks for a person outright. What happens at that handoff is its own design problem, because the naive version — dump the transcript on the human and let them read it — fails the person you just handed the case to.
The human agent on the other end of escalate_to_human has no access to the conversation that got the case to them; they are opening a ticket cold, not reading the same context window the model was. A raw transcript dump asks them to re-derive everything the model already worked out — which customer, what actually went wrong, what would fix it — under time pressure, which looks like help while actually being a research assignment. The structured handoff protocol instead names four fields a handoff must carry: customer ID, root cause, refund amount, and recommended action. Each answers a question the human would otherwise reconstruct from scratch — who is this, what happened, how much money is on the table, and what should happen next.
Python
def escalate_to_human(customer_id: str, root_cause: str, amount_cents: int,
recommended_action: str) -> dict:
return {
"is_error": False,
"content": json.dumps({
"customer_id": customer_id,
"root_cause": root_cause,
"refund_amount_cents": amount_cents,
"recommended_action": recommended_action,
}),
}
TypeScript
function escalateToHuman(
customerId: string, rootCause: string, amountCents: number, recommendedAction: string,
) {
return {
is_error: false,
content: JSON.stringify({
customerId,
rootCause,
refundAmountCents: amountCents,
recommendedAction,
}),
};
}
A select-two question on this handoff is worth internalizing by its wrong answers as much as its right ones. The raw transcript is the pattern this structure exists to replace, not a supplement to it. The agent's self-reported confidence in its own escalation decision is the same unreliable signal rejected elsewhere for autonomous escalation — including it in the handoff doesn't make it trustworthy, it just relocates an already-bad signal. A list of every tool called with timings is operational telemetry, useful for debugging the agent later, but not what the human needs to act on the case right now. Root cause and recommended action carry judgment the human would otherwise redo; customer ID and refund amount are facts they cannot act without.
What the exam tests
Task statements 1.4 and 1.5 share a spine: prompt instructions have a non-zero failure rate, so wherever a miss is financial, compliance-related, or security-related, enforcement belongs in code rather than prose, and identity verification before a financial operation is the exam's standing example. Expect a scenario where an agent occasionally skips a mandatory step, with the correct fix being a programmatic prerequisite gate rather than a stronger instruction, more few-shot examples, or an iteration-cap workaround. Direction is the single most tested distractor under 1.5: PostToolUse transforms a result that has already come back and cannot stop an action that already happened, while interception inspects the call before dispatch and can block it outright — a question offering PostToolUse as the fix for a refund-threshold problem is the right hook family and the wrong direction. Know the redirect pattern for interception, blocking an over-threshold refund and routing to escalate_to_human rather than issuing a bare denial, and know PostToolUse normalization as the answer whenever the failure is heterogeneous data formats — Unix timestamps, ISO 8601, numeric status codes — rather than a prompt instruction to "interpret carefully." Multi-concern decomposition is its own judgment call: a compound message should be split into distinct items, investigated in parallel on shared context, and synthesized into one resolution — not handled serially, and not escalated wholesale merely for being compound. Know the four named handoff fields — customer ID, root cause, refund amount, recommended action — well enough to reject a raw transcript, a self-reported confidence score, or tool-call telemetry as substitutes. And hold the counterweight: a hook is for a guarantee, not a preference, and over-gating a workflow — verifying identity before a read-only lookup, say — is disproportionate even where a gate elsewhere in the same flow is clearly warranted.
Exercises
-
Take the run_agent loop from Chapter 2 and add a verified_customers map exactly as shown above. Wire gated_get_customer and gated_process_refund into execute_tool, then drive a conversation where the model tries to call process_refund before get_customer — prompt it to do so directly if it won't on its own — and confirm the blocked result comes back as a normal tool_result the model reads and recovers from, rather than an exception that kills the loop.
-
Implement the interception function for the $500 refund threshold and connect it in front of gated_process_refund, so a blocked call is also a gate failure if identity was never verified, and a gate pass still gets threshold-checked. Drive two conversations — one requesting a $200 refund, one requesting a $650 refund — and confirm the second one is blocked and redirected to escalate_to_human while the first proceeds untouched.
-
Build two stub versions of lookup_order, one returning a Unix timestamp for ship_date and one returning an ISO 8601 string, and write the normalize_tool_result hook so both produce identical output before the model ever sees either. Then delete the hook and replace it with a system prompt instruction asking the model to "interpret dates carefully," and run twenty varied conversations against both backends. Compare how often the model's reasoning about delivery windows is correct under each approach.
-
Write resolve_multi_concern against two stub investigations — a damaged-shipment lookup and a duplicate-charge lookup — and confirm both run without either one re-fetching the customer record. Then break the shared-context passing deliberately, so each investigation calls get_customer independently, and observe the extra round trip the gate now forces on the second call, since it re-verifies a customer the first call already verified.
Chapter 7 — Sessions
You already know the feeling this chapter is about, even if you have never named it. You resume a Claude Code session from two days ago with --resume, the conversation picks up exactly where it left off, and for the first few turns everything is fine — until Claude confidently tells you something about a file that you rewrote yesterday afternoon. It isn't hallucinating. It is reading its own memory correctly. The memory is just old.
That gap between "the conversation history is intact" and "the conversation history is true" is the entire subject of task statement 1.7, and it is worth taking seriously precisely because the two properties feel like the same thing until they aren't. A session is a sequence of turns, and every turn a tool was called during is a permanent record of what that tool returned — permanent in the sense that it sits in context and will be read again, not permanent in the sense that it stays correct. Nothing in the mechanism of resumption re-checks any of it. Claude Code will happily hand a two-day-old file listing back to the model as if it were current, because from the transport's point of view that listing is just more conversation.
This chapter is short because the task statement is narrow, but the judgment call inside it is one of the sharper ones on the exam: when is resuming the right move, when should you fork instead, and when is resuming actively worse than starting over. All three answers turn on the same fact about how a session is built, so we start there.
What a session actually is
You have used --resume <session-name> enough times to have an intuition for what it does at the product level: it hands Claude Code back the transcript of a named prior conversation and lets you keep talking as if no time had passed. What you have not had reason to think about is what that transcript is made of underneath.
Chapter 2, on the agentic loop, described a tool result as the content block the runtime appends to the conversation after a tool call returns — text (or structured content) marked as either an ordinary result or an error, which the model then reads as the outcome of the call it just made. A session, from the API's point of view, is nothing more than the running list of those blocks: user turns, the model's own text and tool calls, and the tool results that came back for each one, all stored and replayed back to the model on every subsequent turn so it has continuity. Resuming a session is not a special operation that revalidates anything — it is loading that list back in and letting the loop continue from where it stopped. Every tool result in it is treated by the model as still describing the world, because nothing tells it otherwise.
This is exactly the mechanism you already trust when you resume a long refactor in Claude Code and it remembers the plan, the file layout, the decisions you made about naming. It is a good mechanism. It is also, without qualification, a cache — and like any cache, its correctness depends entirely on whether the world underneath it has moved since it was written.
Resuming a named session
The skill the exam wants you to have is using --resume with a session name to continue a specific, identifiable investigation across work sessions — not the most recent session, a particular one you can name, because in practice you have several running lines of work and need to pick up the one that matters right now. Picture a multi-day code-generation task: on Monday you had Claude explore a legacy billing module, produce a design for splitting process_refund out of a monolith, and start drafting the new module. You stop for the day. On Tuesday, resuming that named session is obviously correct if nothing in the repository moved overnight — the exploration, the design rationale, the half-written code are all still an accurate description of the world, and re-deriving any of it from scratch would be pure waste.
The knowledge statement worth holding onto is the condition, not the command: resume when the prior context is mostly still valid. The command is trivial once you know the condition, which is exactly why the exam tests the condition and not the flag syntax.
resume versus continue
Two mechanisms sit next to each other in the SDK and are easy to conflate under exam pressure precisely because they sound like they do the same thing. resume (Python: resume=<session_id>) takes a specific session id and picks up that exact conversation, named and unambiguous — this is the mechanism the rest of this chapter assumes. continue (TypeScript: continue: true; Python: continue_conversation=True) takes no id at all: it resumes whatever session was most recently active in the current working directory. That difference matters the moment you have more than one running line of work in the same project — continue picks up whichever one you touched last, which may not be the investigation you meant to return to, where resume picks up precisely the one you name. Read a scenario that says "the most recent session" as a continue answer and one that says "a specific investigation from earlier" as a resume answer; the two are not interchangeable conveniences, they answer different questions about which session you mean.
When resuming lies to you
Now change one fact about Tuesday morning: overnight, a teammate merged a change that rewrote six files in the authentication module your session had already analyzed. You resume the same named session. The transcript still contains, verbatim, the file contents and the analysis Claude produced from them on Monday — and Claude has no way to know that any of it changed, because a resumed session carries no timestamp of staleness. The tool results in that transcript were true when fetched. Nothing marks them as anything other than true now.
This is the same failure mode Chapter 4 described for tool errors, aimed at a different target. There, an unlabeled failure looked like a valid empty result and the model trusted it. Here, a stale result looks like a current one and the model trusts it just the same, for the identical reason: the model only knows what the content in front of it says, and nothing in a resumed session's transcript distinguishes "this was true" from "this is true." If you resume and say nothing, Claude will reason from Monday's file contents as if they were still on disk, and every downstream decision — where to add the new refund path, which function signature to preserve — inherits that error silently.
The fix the exam wants is not "never resume after code changes." It is: when you do resume, tell the agent specifically what changed. Naming the six rewritten files by path is enough to make the resumption safe again, because it converts an invisible staleness problem into an explicit, boundable one — Claude re-reads exactly those six files and nothing else, rather than either trusting the stale versions or throwing away Monday's design work to re-explore the whole module from zero. This is the skill listed as "informing a resumed session about specific file changes for targeted re-analysis rather than requiring full re-exploration," and it is doing real work: it lets you keep the two days of accumulated reasoning while discarding only the two days of accumulated fact that actually went bad.
Python
resume_prompt = """
Resuming the refund-module refactor session.
Since we last worked on this, the following files were rewritten
upstream and your prior analysis of them is stale — re-read and
re-analyze these before continuing, everything else in your prior
context still holds:
- src/billing/auth_context.py
- src/billing/session_guard.py
- src/billing/token_refresh.py
- src/billing/scopes.py
- src/billing/middleware.py
- src/billing/audit_hooks.py
Do not rely on your previous reading of these six files.
"""
TypeScript
const resumePrompt = `
Resuming the refund-module refactor session.
Since we last worked on this, the following files were rewritten
upstream and your prior analysis of them is stale — re-read and
re-analyze these before continuing, everything else in your prior
context still holds:
- src/billing/auth-context.ts
- src/billing/session-guard.ts
- src/billing/token-refresh.ts
- src/billing/scopes.ts
- src/billing/middleware.ts
- src/billing/audit-hooks.ts
Do not rely on your previous reading of these six files.
`;
Notice the shape of that instruction: it names the durable part (the rest of the session, the design decisions, the plan) as still valid, and it names the specific stale part with enough precision that Claude can act on it without guessing. That precision is the whole skill. "Some files changed, please double-check things" produces exactly the vague, unbounded re-exploration you were trying to avoid by resuming in the first place.
Six rewritten files out of a large module is a targeted fix. Now push the scenario further: you come back to a research session — a multi-agent research system that spent an afternoon crawling a competitor's pricing pages, a regulatory filing site, and a set of news sources — and by the time you're ready to continue, most of what it fetched is no longer trustworthy. Prices have updated, the filing has been amended, three of the news links now 404. This is not "six files changed"; it is "the majority of what this session knows is now suspect," and no amount of "here's what changed" framing rescues a transcript where almost every tool result needs re-verification.
This is the knowledge statement the exam states most bluntly, and it is worth repeating in exactly its own terms because it cuts against the instinct to always resume when you can: starting a new session with a structured summary is more reliable than resuming with stale tool results. The reason is not merely economic — though a transcript full of stale fetches is also just wasted context — it is that a resumed session's history is made of tool_result blocks that were true when fetched and carries no signal that any of them decayed. A fresh session has no such history to misread. If you seed it instead with a written summary containing only the facts you have deliberately re-confirmed as durable — the research questions, the conclusions that don't depend on today's prices, the sources that are still worth revisiting — the model is reasoning from something you vouched for a moment ago, not from a pile of tool output it has no reason to doubt and every reason to. Current Agent SDK guidance reaches this same conclusion independently, from the platform side rather than this book's argument: rather than leaning on session resume once a session's tool results have plausibly gone stale, capture what you've learned as application state and pass it into a fresh session's prompt — which is exactly the structured-summary pattern below, stated as the platform's own recommendation rather than just a workaround this chapter invented.
Python
structured_summary = """
Prior research session (competitor pricing analysis) concluded with
these durable findings — re-verify nothing below unless noted:
- Research questions: (1) how does competitor X price the enterprise
tier, (2) what regulatory constraints apply in the EU market.
- Methodology that worked: querying the vendor's public pricing page
plus the EU filing register directly outperformed general web search.
- Durable conclusion: competitor X's enterprise tier is seat-based,
not usage-based — this is a structural fact unlikely to have moved.
Treat all specific prices, filing statuses, and news article contents
from the prior session as UNVERIFIED. Re-fetch before citing any figure.
"""
TypeScript
const structuredSummary = `
Prior research session (competitor pricing analysis) concluded with
these durable findings — re-verify nothing below unless noted:
- Research questions: (1) how does competitor X price the enterprise
tier, (2) what regulatory constraints apply in the EU market.
- Methodology that worked: querying the vendor's public pricing page
plus the EU filing register directly outperformed general web search.
- Durable conclusion: competitor X's enterprise tier is seat-based,
not usage-based — this is a structural fact unlikely to have moved.
Treat all specific prices, filing statuses, and news article contents
from the prior session as UNVERIFIED. Re-fetch before citing any figure.
`;
The line between "resume and tell it what changed" and "start fresh with a summary" is a matter of degree, and the exam expects you to place a given scenario on the right side of it rather than memorize a threshold number of files. Six known files out of a module is targeted enough to patch in place. A session whose tool results are stale across the board — because a wide crawl aged out, because the underlying data source rotated, because enough time passed that you can no longer enumerate what changed — is a session you should not try to patch, because the patching instruction itself would have to be as long and uncertain as just re-doing the research. The summary is more reliable exactly because it is smaller and hand-checked, where the resumed transcript is large and unchecked.
Forking: one baseline, divergent explorations
Resumption and the fresh-summary alternative both answer "how do I continue," assuming there is one line of work to continue. fork_session answers a different question: you have a shared baseline you already paid to establish, and you want to explore two or more paths from it without either path polluting the other, and without re-paying for the baseline twice.
Take the code-generation scenario again. You've spent real turns getting Claude to build a full understanding of a service's test setup — its fixtures, its mocking conventions, its flaky spots. That exploration is expensive and, crucially, it is shared: both a "migrate to contract tests" strategy and a "add property-based tests to the existing suite" strategy need the same understanding of the current test suite before they diverge into different work. Doing that exploration once and then continuing in a single session to compare both approaches means the context for strategy A and the context for strategy B sit in the same transcript, so anything Claude tries and later reconsiders for strategy A is still sitting there when it reasons about strategy B — the sunk exploration of one approach quietly warps the reasoning about the other. Running the exploration twice, once per new session, avoids the pollution but throws away half the value of having done it carefully in the first place.
fork_session is the third option: it creates independent branches from that one shared analysis baseline, so each branch inherits the exploration exactly once and then diverges cleanly. Whatever strategy-A branch does next — trying contract tests, hitting a dead end, backtracking — never appears in strategy B's context, and vice versa, because they are different sessions from the fork point forward. The same shape applies directly to the multi-agent research scenario: fork after the shared literature review to send one branch deep into a regulatory angle and another into a competitive-pricing angle, and compare the two write-ups afterward with neither one's assumptions bleeding into the other's.
Mechanically, forking is a resume with a flag: you point at the baseline session by id and tell the SDK not to continue it in place, but to branch from it.
Python
from claude_agent_sdk import ClaudeAgentOptions, query
contract_tests_branch = query(
prompt="Propose a migration to contract tests for this service.",
options=ClaudeAgentOptions(resume=baseline_session_id, fork_session=True),
)
incremental_branch = query(
prompt="Propose adding property-based tests to the existing suite.",
options=ClaudeAgentOptions(resume=baseline_session_id, fork_session=True),
)
TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
const contractTestsBranch = query({
prompt: "Propose a migration to contract tests for this service.",
options: { resume: baselineSessionId, forkSession: true },
});
const incrementalBranch = query({
prompt: "Propose adding property-based tests to the existing suite.",
options: { resume: baselineSessionId, forkSession: true },
});
Both calls resume the same baseline_session_id, but fork_session=True tells the SDK to branch rather than continue: each call gets its own new session that starts as a copy of the baseline's history and then diverges, instead of both prompts landing in the one growing transcript that plain --resume would produce.
(For multi-turn conversations inside a single running process, Python's ClaudeSDKClient wraps this same resume/fork machinery in a session-holding client object, so most real applications never touch raw session ids directly. Worth knowing that ergonomic layer exists, even though this chapter — like the exam — stays at the id-passing level underneath it.)
The pattern to recognize on the exam is "two (or more) things to compare, one already-built shared foundation." Running --resume twice against the same session name does not give you that — it gives you one growing transcript that both follow-up prompts share, which is the pollution case, not the branching case. Two fresh sessions each seeded with a copy of the summary avoids pollution but discards the depth of the original tool-backed exploration, keeping only what fit in the summary. fork_session is the only option that keeps the full baseline and still isolates the branches, which is exactly why it's the answer whenever the scenario says "compare divergent approaches from a shared analysis."
| Situation |
Right mechanism |
Why |
| Continuing one investigation, prior context still holds |
--resume <session-name> |
Reuses valid context at no cost |
| Continuing one investigation, a known subset of files changed |
--resume, plus an explicit list of what changed |
Targets re-analysis instead of trusting stale results or re-exploring everything |
| Continuing one investigation, most tool results are stale or unenumerable |
Fresh session seeded with a structured, hand-verified summary |
Avoids re-presenting decayed tool output as current fact |
| Comparing two or more approaches from one shared baseline |
fork_session |
Keeps the full baseline once, isolates each branch's exploration from the others |
What the exam tests
Task statement 1.7 is tested through short scenario stems that ask you to pick one of these four mechanisms, and the trap options are built to look reasonable in isolation. Expect a stem where a handful of files changed since a prior session and the tempting-but-wrong answers are either resuming and saying nothing, which lets a stale tool result be read as current, or discarding the session entirely and starting over, which throws away valid context to fix a narrow problem. The correct answer in that shape is almost always resume-plus-inform, naming the changed files. Expect a separate stem where the scenario signals broad staleness — a long gap, a wide crawl, external data that plausibly moved everywhere — and here the trap flips: resuming with a targeted note is not enough, because there is no small list of "what changed" to give, and the right call is a fresh session with an injected summary. Be able to state the reason for that call, not just recognize it: a resumed session's history is made of tool_result blocks that were true when fetched, carries no marker of decay, and the model has no way to tell old from current except what you tell it. Finally, expect at least one stem asking you to distinguish fork_session from plain resumption when the scenario is explicitly comparative — two testing strategies, two refactoring approaches, two research angles — from a shared baseline; the giveaway phrase is "compare" or "divergent approaches" paired with "same starting point," and the wrong answers will be repeated --resume calls against one growing transcript or duplicated fresh sessions that discard the shared analysis.
Exercises
-
In Claude Code, run a substantial multi-file exploration of a real repository under a named session, then make a small, deliberate edit to two or three of the files it read. Resume the named session and, in one attempt, say nothing about the edit; in a second attempt (from the same starting point), name the exact files that changed. Compare how each transcript reasons about those files afterward.
-
Take that same completed exploration and use fork_session to create two branches: have one propose an aggressive refactor and the other propose a minimal, incremental one. Confirm in each branch's transcript that it has no visibility into the other branch's reasoning, and write down what you would have lost by instead running both proposals as follow-up prompts in the single original session.
-
Simulate the "broad staleness" case: take a session that did a wide web-research pass, wait (or pretend enough time has passed that the fetched pages would plausibly have changed), and write a structured summary containing only what you'd trust without re-checking. Start a fresh session seeded with that summary and compare its first few turns against resuming the original session unmodified — look specifically for the point where the resumed session cites a fetched detail as current fact.
Chapter 8 — MCP
You have added MCP servers to your own Claude Code configuration more times than you can count. You have pasted a block into .mcp.json, watched Claude Code restart its connections, and then used tools that did not exist in the product yesterday — a Jira search, a database schema browser, an internal deploy trigger. From the consumer side, MCP looks like a plugin system: drop in a config entry, get new tools. That impression is not wrong, but it stops one layer short of what the exam wants, which is the layer where you are the one deciding what goes in the config, who else on the team sees it, and whether the tool you are about to write should exist at all.
The Model Context Protocol solves a problem that predates Claude Code entirely: every agent that wants to call an external tool has historically needed that tool's integration written into the agent itself. A support bot that needs Jira access gets a bespoke Jira client bolted onto its tool-use loop; a research agent that needs a different ticketing system gets a different bespoke client; neither integration is reusable by the other agent, and neither survives a rewrite of the host application. MCP standardizes the interface between an agent and an external capability — a tool, or a piece of read-only content — so that the same server can be plugged into Claude Code, into a custom agent built on the Agent SDK, or into a completely different host, without the server author caring which one is asking. You have already benefited from that decoupling every time you added a community server to your own config: someone wrote a GitHub MCP server once, and it works identically whether it is Claude Code, another IDE integration, or a hand-rolled agent loop making the calls.
This chapter is scoped narrowly, and it is worth being explicit about the boundary before going further, because the exam is equally explicit about it. Task statement 2.4 covers integrating MCP servers into Claude Code and agent workflows — configuration, scoping, discovery, descriptions, and the build-versus-buy call. It does not cover how a server is transported (stdio versus a network transport), how it is hosted, containerized, or put behind a load balancer, or how OAuth flows authenticate a client to a remote server. Those are infrastructure questions and the exam guide places them out of scope for the same reason a certification for architects doesn't ask you to configure a reverse proxy. Everything below stays at the level you actually operate at: the JSON you write, the scope you choose, and the judgment calls about what to expose and how to describe it.
What MCP actually standardizes
Strip away the protocol name and MCP is doing two things for an agent that the exam cares about, and they are worth separating cleanly because the exam separates them cleanly. The first is exposing tools — actions the agent can invoke that do something, in the same sense that process_refund or escalate_to_human do something. The second is exposing resources — read-only content the agent can browse, more like a directory listing or a document than an action. You have almost certainly interacted with both without labeling them: a Jira MCP server's search_issues and create_issue are tools; a documentation MCP server that lets Claude Code see a table of contents before deciding what to read is exposing a resource. The chapter's judgment calls below are entirely about this tools-versus-resources line.
Current MCP documentation names a third primitive alongside these two — prompts, reusable prompt templates a server ships so a server author can standardize how a particular task should be phrased rather than leaving every client to invent its own wording. It sees far less use in a Claude-Code-shaped workflow than tools and resources do, and the exam's own scope stays on the tools-versus-resources line throughout, but it's worth being able to name all three primitives if a question distinguishes the full protocol surface rather than just the two this chapter builds judgment calls around.
The distinction matters because the two solve different reliability problems, and conflating them is a design mistake the exam will test directly. A tool is for when the agent needs to cause an effect or fetch something specific it already knows how to ask for — look up this ticket, run this query, file this issue. A resource is for when the agent doesn't yet know what exists and would otherwise have to find out by guessing and calling tools speculatively. Think about the Developer Productivity scenario's issue tracker: without a resource, an agent investigating a bug has to call search_issues two or three times with different guessed keywords just to learn what components and labels exist in the project, burning tool calls on discovery before it does any real work. A resource that exposes the label taxonomy, the component list, or a summary of open epics lets the agent read that catalog once, the way you'd skim a table of contents before deciding which chapter to open, and then make a single well-targeted tool call instead of three exploratory ones. The same logic applies to a Multi-Agent Research System pointed at an internal document store: a resource exposing the folder hierarchy and document titles up front is what keeps the research agent from calling a list_documents tool repeatedly at different path prefixes to map out territory it could have seen in one read.
You will not be asked to implement the resource side of the MCP protocol by hand for the exam — that is server-authoring detail the certification treats as background, not a task statement. What you need is the judgment: when you are deciding what an MCP server should expose, content that an agent needs to browse or orient itself with belongs in a resource, and capabilities that do something belong in a tool. Getting this right is what separates a Jira integration that costs three tool calls to answer "what's in the current sprint" from one that costs one.
Scoping: local, project, and user
You have seen at least two of these files. Claude Code actually draws a three-way distinction, not a two-way one, and the distinction is about who shares the configuration, not about anything the model does at inference time — holding that line precisely is where the exam plants its distractors.
Project-scoped configuration lives in .mcp.json at the root of a repository, and it is checked into version control alongside the code. Anything you put there is shared with everyone who clones the project: the whole team gets the same Jira server, the same internal deploy tool, the same document store connector, configured identically, the moment they open Claude Code in that directory. This is the right home for tooling that is part of the team's workflow rather than part of any one person's habits — the servers a new hire should have on day one without being told to set anything up.
User-scoped configuration lives in ~/.claude.json, outside any repository, and it is personal: whatever you put there follows you across every project rather than living in one repository. It's the right place for a personal productivity integration you want available everywhere you use Claude Code — a personal calendar or notes server with no particular tie to any one codebase.
Current Claude Code draws a third scope beyond those two: local scope, also stored under ~/.claude.json but keyed to the current project rather than global — private to you, not git-tracked, and, unlike user scope, not shared across your other projects either. This is genuinely easy to confuse with .claude/settings.local.json's general local settings, which is a different, unrelated meaning of "local" in the same product — worth flagging explicitly so the two don't blur into one concept in your head. Local MCP scope is the right home for exactly the case an earlier edition of this section pointed at user scope: a server you're experimenting with, a half-finished server you're authoring yourself, or anything carrying credentials you don't want committed but that only makes sense for this one project. The my-experimental-notes-server example below is a case in point — "experimental" and "tied to one project" describe local scope more precisely than "follows me everywhere," so it's plausibly better placed there now. Practically, the safest way to set any of the three scopes correctly is the CLI itself — claude mcp add <name> --scope local|project|user ... — rather than hand-editing ~/.claude.json and hoping you found the right section.
// .mcp.json — project scope, committed, shared with the team
{
"mcpServers": {
"jira": {
"command": "npx",
"args": ["-y", "@some-vendor/jira-mcp-server"],
"env": {
"JIRA_API_TOKEN": "${JIRA_API_TOKEN}",
"JIRA_BASE_URL": "https://acme.atlassian.net"
}
}
}
}
// ~/.claude.json — user scope, personal, not committed anywhere
{
"mcpServers": {
"my-experimental-notes-server": {
"command": "node",
"args": ["/Users/paulo/dev/notes-mcp/index.js"]
}
}
}
Notice ${JIRA_API_TOKEN} in the project-scoped entry. This is environment variable expansion, and it is the mechanism that lets you commit a shared server configuration without committing a credential. The literal string ${JIRA_API_TOKEN} in .mcp.json is not the token; it is a placeholder Claude Code resolves against the environment of whoever is running it, at connection time. Everyone on the team gets the same server pointed at the same Jira instance, but each engineer's own token stays in their own shell environment or .env file, never in git history. Hardcoding a real token into .mcp.json is always the wrong answer on this exam, for the same reason it would get flagged in code review: a project-scoped file is committed, and a committed secret is a leaked secret the moment the repository is cloned anywhere untrusted.
The same expansion also accepts a fallback: ${JIRA_BASE_URL:-https://acme.atlassian.net} supplies a default when the variable isn't set in the resolving environment. Expansion isn't limited to env blocks, either — it applies inside command and args for stdio servers, and inside url and headers for HTTP-transport servers, wherever a config value needs to vary per machine without being hardcoded.
Now the point the exam is most likely to test as a pair of separately-turning distractors, so it deserves its own paragraph rather than a folded-in mention. Scope controls who has the server configured — team versus individual. It says nothing about which tools the model prefers, which server's tools get called first, or whether a project-scoped server's tools outrank a user-scoped server's tools in some priority order. There is no such order. Tools from every configured MCP server, project-scoped and user-scoped alike, are discovered at connection time and become available to the agent simultaneously, sitting in the same tool inventory as Claude Code's built-ins. If you have a project-scoped Jira server and a personal user-scoped GitHub server both configured, Claude Code doesn't ask which one you trust more or which one "owns" this session — it has one flat list of tools, drawn from every source, and the model picks among all of them the same way it picks between Grep and Glob. Scoping is a sharing and secrets-management decision made once, at configuration time, by a human. Tool selection is a per-turn decision made by the model, from the union of everything currently connected. An exam question that describes a project-scoped server "taking priority" over a user-scoped one, or a user-scoped server's tools being hidden from view because the project also has servers configured, is describing something that does not happen — both are simply merged into the one inventory the model sees.
Narrow that claim by exactly one case, because current tooling adds a wrinkle worth keeping separate from it. "No priority order" is true for which tool the model picks among distinct, differently-named servers — that part of the argument above is unchanged. It is not true for what happens when two scopes configure a server under the same name. If a jira server is defined at both, say, project and local scope, Claude Code resolves that naming collision with a fixed precedence — local, then project, then user, then a plugin-provided server, then a claude.ai connector — and connects once, using the highest-priority definition it finds. That's scope resolution: a one-time decision about which single configuration wins a name collision. It is not tool selection, which remains the flat, priority-free merge described above once every server that survived resolution is actually connected. Keep the two questions apart: "which server answers to this name" has an order; "which tool does the model reach for" does not.
Here is a failure you may have watched happen without naming it: you install an MCP server that gives Claude Code direct, structured access to something — a code search index, a documentation store, a ticket system — and the agent uses the built-in Grep or WebSearch instead, even though your server's tool would have returned a better, more structured answer in one call. This is not a bug in Claude Code and it is not the model failing to notice your server connected. It is the same mechanism the chapter on designing tools spends its whole argument on: a tool description is prompt text, read at selection time, not documentation that the model consults after already understanding the tool. Grep's description is written with the care Anthropic puts into every built-in tool — it says plainly what it searches and when to reach for it. If your MCP tool's description is a terse one-liner — "Search Jira issues" — while Grep's is a paragraph, the model has more evidence for Grep every time, regardless of which tool would actually produce a better result.
The fix is exactly the fix from that chapter, applied to a server you did not write the internals of but do control the description for. If you are configuring or authoring a search_issues tool against your Jira MCP server, a description like the one below is what stops the model defaulting to a generic web or file search when it should be hitting the ticket system directly.
Python
search_issues = {
"name": "search_issues",
"description": (
"Search Jira issues by text, status, assignee, or label within the "
"acme-platform project. Returns issue key, summary, status, assignee, "
"labels, and the last three comments for each match — structured data "
"pulled live from Jira, not a text search over a local checkout.\n\n"
"Use this for any question about tickets, bugs, or work items: "
"'what's blocking the payments epic', 'find open bugs tagged "
"checkout', 'who is assigned OPS-4471'.\n"
"Do NOT use Grep or a general web search for these queries — issue "
"text lives in Jira, not in the repository, and this tool returns "
"current status and assignment that a code search cannot see.\n"
"If no issues match the filters, returns an empty list; this is a "
"successful call, not an error, and usually means the filters are "
"too narrow rather than that Jira is unreachable."
),
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Free-text search, e.g. 'refund timeout'."},
"status": {"type": "string", "description": "Optional status filter, e.g. 'In Progress'."},
},
"required": ["query"],
},
}
TypeScript
const searchIssues: Anthropic.Tool = {
name: "search_issues",
description: [
"Search Jira issues by text, status, assignee, or label within the",
"acme-platform project. Returns issue key, summary, status, assignee,",
"labels, and the last three comments for each match — structured data",
"pulled live from Jira, not a text search over a local checkout.",
"",
"Use this for any question about tickets, bugs, or work items: 'what's",
"blocking the payments epic', 'find open bugs tagged checkout', 'who is",
"assigned OPS-4471'.",
"Do NOT use Grep or a general web search for these queries — issue text",
"lives in Jira, not in the repository, and this tool returns current",
"status and assignment that a code search cannot see.",
"If no issues match the filters, returns an empty list; this is a",
"successful call, not an error, and usually means the filters are too",
"narrow rather than that Jira is unreachable.",
].join("\n"),
input_schema: {
type: "object",
properties: {
query: { type: "string", description: "Free-text search, e.g. 'refund timeout'." },
status: { type: "string", description: "Optional status filter, e.g. 'In Progress'." },
},
required: ["query"],
},
};
The load-bearing sentence is the "do NOT use Grep" line, and it is worth noticing why it works: it names the competing built-in tool explicitly, in the same way the tool-design chapter's boundary sentences name a sibling tool by name rather than trusting the model to infer the boundary from two descriptions read in isolation. An MCP tool competes for selection against Grep, WebSearch, and every other built-in exactly the way two of your own custom tools compete against each other, and it needs the same defensive writing to win that competition when it is the better tool for the job. A community-authored MCP server will sometimes ship with a description this thin, and part of your job as the person configuring it for a team is noticing that and either wrapping it or asking whether a fuller description is configurable, rather than assuming a well-built server always ships with a well-built description — those are different engineering disciplines and not every author who gets the protocol right gets the prompt right.
Build versus buy
Every MCP server you might want falls into one of two categories, and the exam wants you to sort correctly between them under time pressure. There is a whole ecosystem of community and vendor-maintained servers for standard integrations — Jira, GitHub, Slack, Google Drive, common databases — and there is the long tail of team-specific workflows that nothing off-the-shelf will ever cover, because they encode internal process rather than a public API.
For the first category, the default is to use an existing server rather than write one. This isn't a shortcut taken to save a sprint; it's the correct engineering call for the same reason you wouldn't write your own OAuth library. A community Jira server has already solved pagination, field mapping across custom Jira configurations, rate limiting, and the dozen edge cases in Jira's API that only show up after months of production use across many different installations. Writing your own gets you a server that handles the cases you personally tested and silently breaks on the ones you didn't, for a problem hundreds of other engineers have already solved and hardened. The judgment call the exam is checking here is whether you reach for "use @some-vendor/jira-mcp-server" or "let's build a Jira MCP server" when a scenario asks for standard ticketing integration — and the correct answer is almost always the former, configured project-scoped with the token expanded from the environment, exactly as shown above.
Custom authorship earns its cost when the integration really is team-specific: a workflow that wraps three internal systems behind one tool call, a resource that exposes your team's own document taxonomy in a shape no generic connector would know to produce, or a tool that encodes business rules — which refund reasons require a supervisor tool call, which ticket labels trigger an automatic escalation — that live nowhere outside your organization. Nothing generic can offer that, because a generic server has no way to know your rules exist. This is also where the tool-design discipline from the earlier chapter applies in full: if you're authoring the server, you own the description quality problem from first principles rather than inheriting whatever a vendor wrote, which is one more reason custom servers should be reserved for cases that actually need the customization rather than reached for by default.
A quick way to hold both judgment calls at once, since the exam likes to test them together: scope and priority are a configuration question with one answer each (local, project, or user; tool selection with no priority order, scope resolution with one), while build-versus-buy and description quality are judgment questions with a "usually, but not always" answer that depends on the scenario. Losing track of which category a question is testing is the most common way to miss a 2.4 item that otherwise looks easy.
What the exam tests
Task statement 2.4 is the entire scope of this chapter, and it separates cleanly into a knowledge half and a skills half. On knowledge, expect direct recall questions on the scope table — project-scoped .mcp.json for shared team tooling versus user-scoped ~/.claude.json for personal or experimental servers — paired with an environment-variable-expansion question where the wrong answer is always the one that hardcodes a token into a committed file. (The exam guide is written against the project-versus-user two-scope model; current Claude Code's third, local scope is real and worth knowing for your own use, but treat project-versus-user as the pair the exam itself is drawing on unless a question explicitly names local scope.) Expect at least one question built specifically to separate scoping from tool discovery: a scenario describing project- and user-scoped servers both configured, asking what happens to tool availability or priority, where the correct answer is that every server's tools are discovered at connection time and sit together in one inventory with no scope-based priority at all — the same-name scope-resolution wrinkle above is a real product behavior, not something the exam guide's stated scope currently tests. Expect a resources-versus-tools question framed around a content catalog — an issue summary list, a documentation hierarchy, a database schema — where the correct answer identifies a resource as the fix for an agent burning tool calls on exploratory discovery, not a new tool and not a bigger system prompt. On skills, expect a misrouting scenario, structurally identical to the ones in the tool-design chapter but specifically an MCP tool losing out to a built-in like Grep, where the fix is enhancing the MCP tool's description rather than reconfiguring scope, restricting the built-in, or writing a routing rule. And expect a build-versus-buy scenario naming a standard integration like Jira or GitHub, where the correct answer reaches for an existing community server and reserves custom authorship for the team-specific case the scenario deliberately does not describe. Nothing in this task statement touches transport mechanics, hosting, or authentication protocol detail — if an answer choice describes stdio versus a network transport, or a Docker container, or an OAuth redirect flow, it is decoration for a wrong answer, not the tested concept.
Exercises
- Take a repository you actually work in and write a project-scoped
.mcp.json entry for a real community MCP server relevant to your team (Jira, GitHub, or similar), using ${VARIABLE} expansion for every credential it needs. Then write a second entry in your own ~/.claude.json for a server you'd want personally but wouldn't want the whole team defaulted into. Explain in one sentence each why each entry belongs where you put it.
- Pick an MCP server you have installed as a consumer and inspect the description text of one of its tools — most clients will let you see the raw tool list. Audit it against the tool-design chapter's five elements (what it does and returns, accepted input formats, example queries, edge cases, boundaries against siblings). If it's thin, rewrite it as if you were the one configuring the wrapper, specifically adding a sentence naming the built-in tool (Grep, WebSearch) it should beat for a query type where it currently might lose.
- For the Developer Productivity scenario, design the tool-versus-resource split for an internal issue tracker integration: decide which capabilities belong as callable tools (create a ticket, transition a status, assign an owner) and which belong as a resource (the label taxonomy, the component list, the current sprint's issue summaries), and justify each placement in terms of whether it's an action or a catalog an agent needs to browse before acting.
- Write out a short decision memo, two or three sentences, for a hypothetical team-specific workflow — say, a tool that files a ticket, tags it per your team's internal escalation rules, and notifies the on-call channel in one call — arguing why this one should be custom-authored rather than assembled from off-the-shelf Jira and Slack servers, and identify exactly which piece of business logic a generic server could never have known to include.
Chapter 9 — Configuring Claude Code
You have a CLAUDE.md in most of your repositories already. You have probably split one that grew too long, argued with a teammate about whether a convention belongs at the root or in a subpackage, and been surprised at least once when an instruction you were sure you'd written didn't seem to apply. None of that is new territory — you have been doing configuration hierarchy and scoping by feel for as long as you've used the tool. What the exam wants is the feel converted into precise vocabulary: which file wins when two disagree, what "shared" actually means for each level, and — the sharpest edge in this whole domain — the difference between a rule that loads because of where a file sits in the tree and a rule that loads because of what kind of file it is, regardless of where it sits.
That last distinction is worth calling out before anything else, because it is the one daily use tends to blur. You reach for a subdirectory CLAUDE.md because it's the tool you already know, even in cases where a directory can't actually express the condition you want. Test files scattered across forty packages are the canonical case: there is no directory whose boundary is "every test file," so a directory-scoped mechanism structurally cannot solve it, no matter how you carve up the tree. The fix isn't a smarter directory layout. It's a different axis of scoping entirely — glob patterns keyed to file type — and Claude Code ships exactly that mechanism in .claude/rules/. This chapter is about getting that axis, and the plain hierarchy questions around it, exactly right.
Two things this chapter is deliberately not about. It does not cover slash commands or skills — those are a different configuration surface, with their own scoping and their own frontmatter fields, and belong to the chapter on extending Claude Code. It also does not cover plan mode, which you already use as a workflow feature rather than a configuration mechanism, and which belongs elsewhere too. Everything here is about the memory-file hierarchy and the two ways Claude Code decides which written instructions apply to a given moment of work: location in the tree, and pattern match on the file being touched.
The CLAUDE.md hierarchy
CLAUDE.md files exist at three levels that do the load-bearing work day to day, and the exam wants you to name the path for each without hesitation. The user level lives at ~/.claude/CLAUDE.md — one file per human, read on every session that user starts, on every machine and every repository they touch, because it lives in their home directory rather than in any project. The project level lives at .claude/CLAUDE.md or a root-level CLAUDE.md file — either is valid, both are read the same way, and this is the file that gets committed to version control and travels with the repository to every clone. The directory level is a CLAUDE.md placed in a subdirectory — inside a specific package, service, or module — and it applies only to work touching that subtree.
Current Claude Code adds two more levels around those three, both worth knowing by name even though neither is where most day-to-day configuration lives. Above user and project sits a managed/enterprise policy CLAUDE.md — an org-controlled file at a fixed OS path (/Library/Application Support/ClaudeCode/CLAUDE.md on macOS, /etc/claude-code/CLAUDE.md on Linux/WSL, C:\Program Files\ClaudeCode\CLAUDE.md on Windows) that an individual engineer can't delete or exclude and that loads before anything else. It exists for organization-wide policy IT or platform teams need to guarantee applies regardless of what any one repository or home directory contains. Below project sits CLAUDE.local.md — a gitignored file at the project root, personal the way ~/.claude/CLAUDE.md is personal, but scoped to this one project rather than every project you touch, and loaded immediately after the project's own CLAUDE.md. It's the right home for something like "I always run this repo's tests with -k slow locally," a preference that's yours alone but only makes sense in this one codebase — content that doesn't belong in the shared project file and isn't relevant to your other projects either, so it doesn't belong in ~/.claude/CLAUDE.md.
The property that actually matters, more than the paths themselves, is who else sees the file. A project-level CLAUDE.md is a shared artifact: it's checked into the repository, so it reaches every teammate who clones it and every CI job that checks it out, and it's the correct place for anything you want the whole team to follow. A user-level CLAUDE.md is the opposite — it is personal configuration sitting in your home directory, never pushed anywhere, invisible to version control by construction. It is exactly the right place for your own idiosyncratic preferences — how verbose you like commit messages, whether you want explanations before code changes — and exactly the wrong place for anything a teammate needs to also follow.
This is where the hierarchy stops being a filing question and becomes a diagnostic one, and it's the shape the exam tests most directly under this task statement: a new engineer joins the team, clones the repository, and Claude Code behaves as if half the team's conventions don't exist for them, even though "everyone else" seems to get them applied automatically. The instructions in question are sitting in the tech lead's ~/.claude/CLAUDE.md, written months ago when the convention was still personal habit rather than team policy, and never promoted to the project. Every other engineer who onboarded by copying dotfiles from a senior teammate inherited them by accident; the new hire, who set up Claude Code cleanly, did not. The fix is not more documentation elsewhere and not a bigger onboarding checklist — it's moving the actual content from the user-level file into the project-level file, where version control does the distribution work for you. Any scenario on the exam that describes instructions working "for me" but not "for the team" is describing this exact misplacement, and the answer is always the same move: promote from ~/.claude/CLAUDE.md to .claude/CLAUDE.md or root CLAUDE.md.
Directory-level CLAUDE.md files sit a level further down and solve a narrower problem: conventions that belong to one subtree and nowhere else. A monorepo with a Python service and a TypeScript frontend can carry a root CLAUDE.md with organization-wide conventions — how services talk to each other, how incidents get filed — plus a CLAUDE.md inside services/billing/ describing that service's specific database access patterns, and a separate one inside apps/web/ describing frontend component conventions. Each is read only when work touches that subtree, which keeps the billing team's Django-specific instructions out of the frontend engineer's context and vice versa. The scoping is directory-bound by definition: it activates because of where the file being edited lives in the tree, not because of any property of the file itself. Hold onto that phrasing — directory-bound, activating on location — because the next section exists specifically to contrast it with a mechanism that activates on something else entirely.
Modular organization: @import and .claude/rules/
A CLAUDE.md that has grown into a two-thousand-line wall of unrelated conventions is a familiar failure, and Claude Code gives you two distinct tools to break it apart, tested as two distinct facts.
The first is the @import syntax, which lets a CLAUDE.md reference an external file by path rather than inlining its content. This is how you keep a root CLAUDE.md thin while still pulling in exactly the standards a given package needs, and it puts the decision about which standards apply in the hands of the person who actually knows the package — the maintainer, not whoever last edited the root file. A monorepo might have a docs/standards/ directory holding python-style.md, terraform-conventions.md, and api-design.md, and each service's own CLAUDE.md imports only the ones relevant to it:
# services/billing/CLAUDE.md
This service handles payment processing and refund issuance.
@docs/standards/python-style.md
@docs/standards/api-design.md
Refunds above $500 require the process_refund tool's manual-review path;
see the runbook in docs/refunds.md before touching that code path.
A Terraform-only infrastructure package would import terraform-conventions.md instead, and never load the Python style guide it has no use for. The mechanism is selective inclusion driven by domain knowledge: the billing maintainer knows their service needs API design conventions and doesn't need Terraform conventions, and @import lets that judgment live where the judgment is made, instead of forcing one root file to either contain everything or omit things some packages actually need.
The same interop applies if a team arrived with an AGENTS.md from a different tool already in the repository: Claude Code reads CLAUDE.md, not AGENTS.md, so the file doesn't get picked up on its own. The fix is the same mechanism, pointed the other way — either @AGENTS.md inside a CLAUDE.md, or a symlink from one filename to the other — rather than maintaining two parallel instruction files that drift apart.
A newer setting worth knowing alongside @import is claudeMdExcludes, a settings.json key available at any layer (user, project, local, or managed) whose arrays merge across layers. It excludes specific CLAUDE.md or rules files by glob, matched against absolute paths — the counterpart to .claude/rules/ for a monorepo that wants a subpackage to not inherit a rule that would otherwise apply to it, without having to restructure where that rule file lives.
The second tool is the .claude/rules/ directory, and it addresses a different symptom of the same monolith problem — not "this file needs content from elsewhere" but "this file is doing too many unrelated jobs at once." Instead of one CLAUDE.md covering testing conventions, API design, and deployment procedure in one undifferentiated scroll, you split it into topic-specific files: .claude/rules/testing.md, .claude/rules/api-conventions.md, .claude/rules/deployment.md. Each file is focused enough that a maintainer can own it, review changes to it, and reason about whether it's still accurate, in a way that's much harder to do against a single sprawling document with a testing section that begins line 40 and ends who-knows-where.
.claude/
CLAUDE.md # thin: project overview, links to rules
rules/
testing.md # pytest conventions, fixture policy
api-conventions.md # REST error shapes, versioning policy
deployment.md # rollout steps, rollback procedure
At this point .claude/rules/ is just organizational hygiene — a way of splitting one file into several topic-scoped ones. The next section is where it becomes something structurally different, because those rule files can carry YAML frontmatter that changes when they load at all.
Diagnosing with /memory and /context
Because CLAUDE.md content can come from several hierarchy levels plus imports plus an arbitrary number of rule files, "why did Claude Code do that" stops being answerable by memory alone fairly quickly — you wrote instructions somewhere, but which file, and did it actually get loaded for this session, in this directory, editing this file? Two commands answer that, and it's worth being precise about which does which, since they're easy to conflate and the exam trades on exactly that confusion. /memory lists and opens the CLAUDE.md and rules file locations Claude Code knows about at every level — including ones that don't exist yet, which makes it the right tool for browsing and editing your configuration surface, not for confirming what actually loaded into the current session. /context is the command that answers that second question: what's actually loaded right now, in this session, for this directory and this file. If a convention seems to apply inconsistently between sessions, or you're certain you wrote something down but it isn't influencing behavior, /context is the first move — it shows the current, live picture, where /memory shows you where to go looking and editing. Once /context tells you a file didn't load, /memory is where you go to open it and find out why: the wrong hierarchy level, a stale @import path, or a path-scoped rule whose glob doesn't match the file you're editing.
Path-specific rules: scoping by file type, not by location
Here is the mechanism the last section deferred. A file in .claude/rules/ can carry YAML frontmatter with a paths field, and the value is a list of glob patterns. When present, that rule loads only when Claude Code is working on a file matching one of those patterns — not on every session, not for every file in some directory, but specifically when the file under edit matches the glob.
---
paths: ["terraform/**/*"]
---
# Terraform conventions
All resources must be tagged with `team` and `cost-center`. Modules under
terraform/modules/ are versioned by git tag, not by branch — never reference
a module by branch name in a `source` argument. Run `terraform fmt` before
every commit; CI will reject unformatted plans.
.claude/rules/ files are discovered recursively — a rule can sit in a subdirectory of rules/ (.claude/rules/frontend/testing.md) and still be found — and can be symlinked in from elsewhere, which is how a shared rule gets reused across repositories without copy-pasting it into each one. The same directory also exists one level up: ~/.claude/rules/ applies across every project a user touches, loaded before that project's own .claude/rules/, so a project-level rule wins if the two ever disagree — the same precedence logic as the CLAUDE.md hierarchy, one level down. The paths glob syntax itself supports brace expansion (**/*.{ts,tsx} matches both extensions in one pattern rather than requiring two), bounded by a budget of 1000 expanded patterns and 4MiB of expansion — generous for any rule file a human would actually write by hand, but worth knowing exists if a generated or templated paths list ever gets large enough to hit it.
That example is directory-shaped — everything under terraform/ — and you could arguably express it with a directory-level CLAUDE.md too, since the convention happens to correlate with a single subtree. The case a directory-level file cannot express is the one built on file type rather than file location, and it's worth working through concretely because it's the exact shape the exam's sample question puts in front of you.
Picture a codebase where components and their tests live side by side rather than segregated into a parallel test/ tree: Button.tsx sits next to Button.test.tsx, useAuth.ts sits next to useAuth.test.ts, repeated across forty component directories spread through the whole frontend. You want one rule — every test file uses the project's custom render wrapper instead of raw render from the testing library, every test file follows the arrange-act-assert comment convention, every test file mocks the API client the same way. There is no subtree you can drop a directory-level CLAUDE.md into that covers "every test file," because the test files are not gathered into a subtree at all — they're interleaved with the source files they test, one directory at a time, forty times over. Put a CLAUDE.md at the root and it applies to component and test files alike, forcing you to write the rule as an if-this-is-a-test-file aside inside a file that's otherwise about component conventions, and relying on the model to infer from prose which files the aside actually governs. Put a CLAUDE.md in each component's directory and you've multiplied the same content forty times with no shared source of truth, and you've done nothing about the underlying problem: the file that needs the rule is defined by its suffix, not by its address.
A glob-pattern rule sidesteps the problem instead of working around it, because it scopes on the one property directory-based mechanisms can't see — the shape of the filename itself:
---
paths: ["**/*.test.tsx", "**/*.test.ts"]
---
# Frontend test conventions
Use `renderWithProviders` from src/test-utils, never the raw `render` export
from the testing library — our components depend on theme and auth context
that raw render does not supply. Structure test bodies as arrange, act,
assert, with a blank line between each section. Mock the API client via
`mockApiClient()` from src/test-utils; do not construct ad hoc fetch mocks.
Wherever Button.test.tsx lives, editing it loads this rule. Wherever useAuth.test.ts lives, same thing. The rule follows the file type across the entire tree, which is exactly the property a directory boundary cannot express by construction — a directory is a location, and "every file matching this suffix" is not a location, it's a category that happens to be scattered across all of them.
The table below is worth holding as a fixed reference, because the exam's discriminator question is precisely a request to pick the row that fits the scenario described.
| Scoping question |
Right mechanism |
Why |
| "Everyone on the team needs this instruction." |
Project-level CLAUDE.md (.claude/CLAUDE.md or root CLAUDE.md) |
Committed to version control, reaches every clone. |
| "Only I want this instruction, on every project I touch." |
User-level CLAUDE.md (~/.claude/CLAUDE.md) |
Personal, never shared, not version-controlled. |
| "This convention applies to one subtree and nowhere else." |
Directory-level CLAUDE.md |
Directory-bound: loads based on where the edited file lives. |
| "This convention applies to a file type, wherever it appears in the tree." |
.claude/rules/*.md with a paths glob |
Type-bound: loads based on what the edited file is, regardless of location. |
| "This file is too big; I want to pull in only the standards relevant to this package." |
@import inside that package's CLAUDE.md |
Selective inclusion driven by maintainer judgment. |
| "This file is too big; I want to split it by topic without changing when anything loads." |
Plain files under .claude/rules/ (no paths frontmatter) |
Organizational split; always loaded, just not monolithic. |
The reason path-scoped rules matter beyond just solving the scattered-tests case is the second half of task statement 3.3: token economy. A rule that only loads when its glob matches means Claude Code isn't carrying Terraform tagging conventions into context while you're editing a React component, and isn't carrying frontend testing conventions into context while you're writing infrastructure code. Compare that to a root CLAUDE.md that tries to be comprehensive — every convention for every file type sits in context on every single turn, whether or not the current file has anything to do with most of it. Conditional loading via paths is what keeps a large, well-organized .claude/rules/ directory cheap instead of turning into a second monolith that just happens to be split across files.
It's worth being explicit about the failure mode of choosing the wrong tool in the wrong direction, too, since the exam likes to offer both wrong answers as distractors alongside the right one. Given the scattered-tests scenario, "consolidate everything into one root CLAUDE.md with a testing section" is wrong because it relies on the model inferring scope from prose structure rather than having an explicit, mechanical match — nothing stops that section's advice from leaking into how a non-test file gets written, or from being skipped when the model's attention is elsewhere in a long file. And "put it in a skill" is wrong for a more basic reason: skills are invoked on demand, either by an explicit command or by the model's own judgment that a skill applies, and a testing convention that must apply automatically, unconditionally, every time a test file is touched, is precisely the case where you don't want to depend on invocation happening at all. Path-scoped rules are automatic and mechanical in the way skills are not, and directory-agnostic in the way plain CLAUDE.md is not — that combination is the whole reason the mechanism exists.
Choosing between subdirectory CLAUDE.md and glob rules
Given how much of this task statement is a discrimination test, it's worth stating the rule of thumb in one sentence you can apply without re-deriving it each time: if the convention's boundary is a place in the tree, use a subdirectory CLAUDE.md; if the convention's boundary is a kind of file that shows up in many places, use a glob rule in .claude/rules/. Most real conventions sort cleanly once you ask which axis they actually vary along. "This service talks to Kafka differently than the others" varies by location — one service, one directory, one CLAUDE.md. "Every file ending in .tf gets tagged the same way" varies by type, wherever those files happen to sit. "Generated code under src/generated/ should never be hand-edited" is location again. "Every snapshot test file needs its snapshots reviewed before merge" is type again, because snapshot tests are not gathered into one directory any more than the earlier .test.tsx files were.
The two mechanisms compose rather than compete, which is the last piece worth internalizing. A large real project typically runs a thin root CLAUDE.md for orientation and org-wide policy, directory-level CLAUDE.md files for the handful of conventions that really are about location — a specific service's data access pattern, a specific package's build quirk — and a .claude/rules/ directory full of glob-scoped files for the conventions that follow file type across the whole tree: testing conventions, migration-file conventions, generated-code markers, infrastructure-as-code conventions. None of these levels replaces the others; each is the right tool for a different shape of "where does this convention apply."
Auto memory: a second system, distinct from CLAUDE.md
Everything so far is content you write. Current Claude Code also ships a second, separate persistence system that Claude itself writes: auto memory, stored per project under ~/.claude/projects/<project>/memory/, indexed by a MEMORY.md file, and loaded automatically at the start of every session in that project. The distinction to hold onto is exactly that authorship split — CLAUDE.md is instructions you deliberately put in front of the model; auto memory is Claude's own running notes about the user, the project, feedback it's received, and reference pointers to other systems, organized into typed entries (user, feedback, project, reference) and capped at roughly 200 lines and 25KB so it can't grow into the same unbounded-context problem CLAUDE.md itself is prone to.
The two systems don't compete for the same job. CLAUDE.md is the place for conventions you want to guarantee apply, reviewed and versioned the way any other project artifact is. Auto memory is closer to institutional knowledge Claude accumulates about how you like to work — that you prefer terse commit messages, that a particular refactor pattern was already tried and rejected, that a given teammate is the person to ask about the deploy pipeline — surfaced automatically without you having to write it down yourself. A scenario that describes Claude "remembering" something from a past session that was never explicitly written into any CLAUDE.md file is describing auto memory, not a hierarchy level you missed.
What the exam tests
Task statement 3.1 tests whether you can name the CLAUDE.md levels and their exact paths without hedging — user, project, and directory carry the everyday weight, with managed/enterprise policy above them and CLAUDE.local.md below the project level rounding out the full picture — and — more importantly — whether you can diagnose the specific failure where a new team member doesn't receive instructions because they live in ~/.claude/CLAUDE.md rather than the project file; the fix is always promotion to the shared level, never a workaround elsewhere. It also tests the two modularization tools as distinct facts that must not be conflated: @import pulls specific external standards files into a CLAUDE.md, selected by whoever maintains that package, while .claude/rules/ is a directory for splitting a monolith into topic-focused files. /context is tested as the diagnostic command for verifying which memory files are actually loaded into the current session, and you should reach for it first whenever behavior across sessions is inconsistent with what you believe you configured; /memory is the related but distinct command for browsing and editing the CLAUDE.md/rules locations themselves, not for confirming what's live right now. Task statement 3.3 is almost entirely the discriminator: subdirectory CLAUDE.md is directory-bound, activating by where a file lives; glob-pattern rules under .claude/rules/ with a paths field are type-bound, activating by what kind of file is being edited regardless of location. Expect a scenario built around conventions scattered across many directories — test files interleaved with source, migration files spread through many services — where a directory-level CLAUDE.md structurally cannot express the rule and a glob pattern can; the correct answer names the glob mechanism and explicitly rejects consolidating into root CLAUDE.md (relies on inference, not explicit matching) and rejects skills (requires invocation, not automatic). You should also be able to state the secondary benefit of path-scoped loading — it keeps irrelevant conventions out of context and off the token budget rather than loading everything unconditionally on every turn.
Exercises
- Take a repository you actually maintain and run
/context. Note every memory file it reports as loaded, then trace each one back to its level — managed, user, project, CLAUDE.local.md, or directory — and to any @import or glob-scoped rule responsible for it being there. If anything surprises you, that surprise is the exercise: figure out why before moving on.
- Design the
.claude/rules/ split for a repository with at least three unrelated conventions (testing, API error shapes, deployment steps). Write each as its own file, and for at least one of them, add paths frontmatter scoping it to a realistic glob — **/*.test.ts, **/*.tf, **/migrations/*.py — rather than leaving it unconditionally loaded.
- Write out, in your own words, a scenario where a subdirectory CLAUDE.md is clearly the right choice and a second scenario where it clearly is not, using the location-versus-type test from this chapter. For the second scenario, write the glob rule that replaces it.
- Deliberately misplace an instruction in
~/.claude/CLAUDE.md that should be a team-wide convention, and confirm via /context in a fresh clone (or a colleague's machine, or a scratch home directory) that it does not appear. Then promote it to the project-level file and confirm it does.
Chapter 10 — Extending Claude Code
You already know how to make Claude Code do something once. You type a request, it explores, it edits, it runs your tests. This chapter covers how you make it do the same thing the same way every time — for yourself, and for a team — and how you decide whether a given task deserves a plan before it deserves an edit.
Both halves of this chapter are about codifying judgment you already exercise informally. Every senior engineer who has used Claude Code for more than a week has a personal repertoire: a phrase typed before a big refactor, a habit of asking for a plan on anything touching more than two files. The exam is not testing whether you have these habits. It is testing whether you know the exact mechanism Claude Code gives you to make a habit durable and shareable — the file, the directory, the frontmatter field — and whether you can tell, from how a task is described, which mechanism and which mode it calls for.
Slash commands and skills solve the durability half: turning a one-off prompt into an artifact that lives in a file, gets version-controlled, and reappears identically the next time anyone types it. Plan mode solves the sequencing half: deciding, before you write a line of a system prompt or touch a file, whether this particular task needs a design phase that Claude Code can enforce for you. Neither is exotic. Both are things you have clicked through as a consumer. What follows is the vocabulary and the decision procedure the exam wants back.
Slash commands: where the file lives decides who gets it
A custom slash command is a markdown file whose body becomes the prompt Claude receives when you type its name. You have used /review or a project's own /deploy-check without necessarily noticing that the entire command is just a file under .claude/commands/ — no compiled artifact, no registration step, nothing but a markdown file whose name becomes the invocation.
The one fact the exam wants you to have automatic is where that file lives, because the location is the whole answer to "who can use this command." A command defined in .claude/commands/ inside the project repository is project-scoped: it is checked into version control alongside the code, and it reaches every teammate the moment they clone or pull. A command defined in ~/.claude/commands/, under your home directory, is user-scoped: it is yours alone, invisible to anyone else on the team, and it survives across every project you open because it lives outside any of them.
That distinction resolves a whole class of exam question that reads like a support ticket: "we want /review to run the same way for every developer the moment they clone the repo." The wrong answers include putting the command in CLAUDE.md (that carries context and standards, not invocable named actions) and inventing a separate registration file listing which commands exist (Claude Code needs no such registry — a command's presence in .claude/commands/ is itself the registration, discovered by the file simply being there). The only answer that satisfies "every developer, on clone" is .claude/commands/ in the repository, because version control is the distribution mechanism — there is no install step, no sync process. If the file is tracked and the developer has the repo, they have the command.
The inverse question is just as testable: a command that should not affect teammates — your own shorthand for restating a bug in your preferred format, say — belongs in ~/.claude/commands/, precisely because putting it in the project would hand your personal habit to everyone who pulls the branch, whether they want it or not.
.claude/commands/review.md # project-scoped — every teammate gets /review
~/.claude/commands/my-triage.md # user-scoped — only you get /my-triage
Skills: commands that carry configuration
A skill is the same idea — a named, invocable markdown file — extended with a frontmatter block that configures how it runs. Where a slash command is a name and a body, a skill is a name, a body, and a small set of declared behaviors that Claude Code enforces around the invocation rather than leaving to the prompt text to request. You have consumed skills already, whenever you typed a slash-prefixed skill name and watched it run inline or spin off into a visibly separate sub-task. This section covers the exact frontmatter fields that produce that behavior.
Current Claude Code treats commands and skills as the same underlying mechanism rather than two separate ones — .claude/commands/foo.md and .claude/skills/foo/SKILL.md both produce a /foo invocation, and a skill is best understood as a command that happens to carry extra frontmatter and its own directory, not a competing concept. The chapter still treats them as two headings because the frontmatter surface below is genuinely skill-specific, but don't read the split as two different invocation systems under the hood — it's one system, and a skill is the superset.
A skill lives at .claude/skills/<skill-name>/SKILL.md for a project-scoped skill, or ~/.claude/skills/<skill-name>/SKILL.md for a personal one — the same project-versus-user split that governs commands, and it governs skills for the identical reason: version control is what makes a project skill everyone's skill, and the home directory is what keeps a personal skill personal. The full precedence order runs wider than just those two: an enterprise/managed level above personal skills (org-controlled, for skills every developer must have regardless of what any project or home directory contains), personal (~/.claude/skills/) and project (.claude/skills/) in the middle as already described, and a plugin level below that (<plugin>/skills/, invoked with a plugin:skill name to avoid colliding with a same-named project or personal skill). Skills also discover in nested project directories, not just at the repository root — a skill under apps/web/.claude/skills/deploy/ is invoked as /apps/web:deploy, which matters for a monorepo where "the frontend team's deploy skill" and "the backend team's deploy skill" need to coexist under one name without a naming collision.
The frontmatter block sits at the top of SKILL.md, and three fields are the ones the exam holds you to; the full frontmatter surface is larger (when_to_use, arguments/$name substitution, model, effort, background, agent, hooks, shell, metadata, license, compatibility all exist), but those three are the ones worth having automatic.
argument-hint tells Claude Code what to prompt the developer for when the skill is invoked with no arguments. If a skill cannot do its job without knowing, say, which service to migrate or which ticket to work against, argument-hint turns a bare invocation into a request for the missing piece instead of a guess — a small field with an outsized effect, since without it a skill invoked bare either fails silently or the model infers intent from nothing, and both are worse than asking.
allowed-tools restricts which tools are available while the skill is executing. This is the field that turns a skill from "a prompt with a name" into something with an actual safety boundary. A skill meant to write a migration report has no legitimate reason to invoke a tool that deletes files or runs arbitrary shell commands, and allowed-tools makes that a property of the skill's configuration rather than a hope resting on prompt wording. A skill that generates release notes from commit history should declare allowed-tools limited to read and file-write operations, so that even if the model reasons its way toward "let me also clean up this stale branch while I'm here," the tool to do so is not on the table. This is the same underlying idea as scoping a tool's blast radius from the chapter on designing tools — here the scoping knob is a frontmatter line instead of a tool definition.
One caveat that will bite you in your own repositories, though not on the exam. The guide describes allowed-tools as restricting tool access, and that is the answer to give. Current Claude Code documents it differently: allowed-tools lists tools Claude may use without asking permission for the turn that invoked the skill — a pre-approval grant, not a fence. The field that actually removes tools from the pool while a skill is active is disallowed-tools. So if you build the read-only audit skill described here and rely on allowed-tools alone to keep it from deleting anything, it will not do that. Reach for disallowed-tools when you want the guarantee, and keep allowed-tools in your head as the exam's answer for "restrict tool access during skill execution."
context: fork is the field with the most conceptual weight. Setting it runs the skill in an isolated sub-agent context rather than inline in your current conversation. The skill gets its own context window, does its work there, and returns a result to the main conversation — the verbose intermediate steps never appear in your session's transcript at all. This is the mechanism, not a side effect of it, that keeps a skill's internal noise from polluting the conversation you're actually trying to have.
Reach for context: fork in exactly two situations, and both come up on the exam framed as scenarios rather than named directly. The first is a skill whose job is inherently verbose — a codebase analysis skill that opens dozens of files and reasons through what it found before producing a three-paragraph summary. Without forking, that reading and reasoning lands in your main session, and the next ten turns of your actual conversation compete for space against a transcript of files you never asked to see. The second is a skill whose job is exploratory in a way you don't want anchoring the rest of the conversation — a brainstorming skill that proposes and discards several alternatives before landing on a recommendation. You want the recommendation, not the rejected alternatives sitting in context and subtly biasing every later turn toward re-litigating options you already moved past. context: fork gives you the summary without the scaffolding that produced it.
---
name: analyze-refund-patterns
description: Scans recent process_refund calls for anomalies and reports a summary.
context: fork
allowed-tools: Read, Grep, Glob
argument-hint: "[date-range, e.g. 2026-08-01..2026-08-31]"
---
That example is deliberately unglamorous: a read-only, forked, argument-hinted skill for a support-engineering scenario, built out of exactly the three frontmatter fields the exam tests and nothing else. Everything past the frontmatter is markdown prose describing the task, the same way a CLAUDE.md paragraph describes a standard — skills are not a new templating language, they are a slash command with three extra configuration knobs.
Those three fields govern what a skill can do and what it needs from you once invoked. A separate pair of fields governs who is allowed to invoke it in the first place, which the fields above don't touch. disable-model-invocation blocks the model from triggering the skill on its own judgment — the skill only runs when a person explicitly types its name — the right setting for a skill with real side effects, like a /deploy that should never fire because the model decided a deploy seemed like a reasonable next step. user-invocable: false is the mirror image: it removes the skill from the human-typed command list entirely, leaving it invocable only by the model's own judgment, the right setting for a background-knowledge skill that exists to be reached for automatically rather than typed by name. A skill can also carry paths frontmatter, the same glob-scoping mechanism as .claude/rules/ files, to be considered for auto-invocation only when the file currently being worked on matches the pattern.
Claude Code also ships a set of bundled skills out of the box — /doctor, /code-review, /run, /verify, /loop, /debug, and others — available without any setup, with disableBundledSkills and skillOverrides settings for a team that wants to turn one off or replace it with a project-specific version under the same name.
Personal variants without stepping on teammates
The other configuration skill the exam names directly is what to do when you want a different behavior than the team's shared skill without changing the team's shared skill. The mechanism is not a flag or an override file — it is simply a second skill, under a different name, in ~/.claude/skills/. If the project ships .claude/skills/extract-invoice/SKILL.md tuned to the team's schema conventions, and you personally want a variant that also normalizes currency codes, you do not edit the shared file. You create ~/.claude/skills/extract-invoice-mine/SKILL.md — a new name, in your personal scope — and invoke that one when you want your variant. The team's skill is untouched, every teammate keeps getting the behavior they expect, and your customization lives entirely where version control never sees it. The exam's answer to "customize without anyone noticing" is always this rename-and-relocate move, never an in-place edit of a shared file.
Skills versus CLAUDE.md
The last piece of 3.2 is a comparison you are expected to make instantly: skills are for on-demand invocation of a task-specific workflow, and CLAUDE.md is for standards that should be loaded on every single turn without anyone asking for them. The test is not "is this important" — plenty of things in a skill are important — it is whether the content applies universally to every interaction or only to the specific, nameable task the skill performs. Your team's commit message convention belongs in CLAUDE.md, because it should shape every commit Claude Code ever writes here, invoked or not. Your team's procedure for auditing process_refund call sites for a compliance question belongs in a skill, because most turns have nothing to do with that audit, and loading its instructions into every conversation would be pure context cost for no benefit on the turns that don't need it. If a scenario describes something that should apply always, to everyone, without being asked, it is a CLAUDE.md question dressed up as a skills question, and the correct answer skips the skill entirely.
Plan mode versus direct execution
You have used plan mode as a button: you press it, Claude explores and proposes before touching anything, you approve or redirect, then it executes. The exam does not test the mechanics of pressing the button. It tests whether you can look at a task description — before any exploration has happened — and correctly predict whether it needed that button pressed at all.
The underlying reason plan mode exists is that some classes of change have more than one legitimate implementation, and the expensive mistake is not writing bad code, it is committing to the wrong shape of change before you understood the alternatives. Plan mode buys you safe exploration of a codebase and a proposed design that a human reviews before a single file is touched, and the value is specifically in preventing rework that would otherwise cost far more than the planning phase did. That tells you exactly what plan mode is for: tasks with real architectural stakes, multiple valid approaches, and a blast radius wide enough that discovering you chose wrong on file thirty of forty-five is a very bad afternoon. The canonical shapes are restructuring a monolith into microservices, migrating forty-five files to a new library version, and choosing between two integration approaches that carry different infrastructure requirements — each has more than one defensible answer, and the cost of picking wrong is measured in days of rework, not a failed test you re-run in ten seconds.
Direct execution is correct for exactly the tasks that don't have that shape: a single-file bug fix where a stack trace already tells you the faulty line, or adding one validation conditional to a function whose contract you already understand. There is one way to fix a null check a stack trace pinpoints. Planning it first does not produce a better fix — it produces a plan whose content is "go add the null check," reviewed and approved, arriving at exactly the change direct execution would have made three steps earlier. Plan mode's ceremony has a real cost in time and a review step, and spending it on a task with only one reasonable shape is not caution, it is friction with no safety benefit.
The judgment the exam actually wants — and the point its own materials flag as the one people get backwards — is when you make this call. The complexity that argues for plan mode is not something you discover partway through a direct-execution attempt that has started going sideways. It is something the request already told you, in its first sentence, before you opened a single file. "Restructure this into microservices" and "migrate these forty-five files to the new library" do not start out looking simple and reveal their complexity later; they announce it up front. The wrong move — and a distractor the exam will offer directly — is starting in direct execution "to see how far you get" and switching to plan mode only once things get messy. By then you have already made irreversible choices across however many files you touched before the mess became visible, which is precisely the rework plan mode exists to prevent. The signal to reach for plan mode is in the request itself, read at face value, not in what you observe once you're three files into ignoring it.
Put the two side by side against the same underlying question — does this task have one obviously correct shape, or several plausible ones that diverge in ways expensive to walk back — and the choice falls out immediately:
|
Plan mode |
Direct execution |
| Number of legitimate approaches |
Several, actively competing |
One, or near enough that debate is wasted motion |
| Where the complexity signal appears |
Stated in the request itself, at the start |
Not applicable — there isn't hidden complexity to surface |
| Representative case |
Monolith to microservices; 45-file library migration; choosing between integration approaches with different infra needs |
Single-file fix against a clear stack trace; adding one validation conditional |
| What you're protecting against |
Costly rework from committing to the wrong design early |
Nothing unusual — normal execution risk only |
| Cost of using it anyway on the wrong case |
Ceremony and delay with no safety payoff |
Irreversible missteps across many files before anyone notices |
None of this forbids using both in the same task, and the exam explicitly rewards recognizing when to. A library migration is frequently planned once and executed many times: you run plan mode to work out the target API shape, the migration order, and the call sites needing special-casing, then drop into direct execution to make the forty-five edits it specifies. The plan is not a one-time gate you pass through — it is a durable spec the execution phase consumes. Treating this as a single either/or choice for the whole task, rather than a sequencing decision within it, is the trap; the composed answer is almost always what real migrations look like in practice.
The Explore subagent
Plan mode's investigation phase has its own failure mode worth naming separately: discovery that runs long enough to consume the context budget the rest of the task needs. Mapping forty-five call sites of a library you're migrating, or surveying every service boundary candidate in a monolith, produces a lot of transcript — file reads, greps, dead ends — most of which nobody needs to see once the investigation concludes. The Explore subagent exists for exactly this problem: you hand it the discovery question, it does the verbose legwork in its own isolated context, and it hands back a summary, leaving your main conversation holding the conclusion instead of the archaeology that produced it.
This is the same architectural move as context: fork on a skill — isolate the noisy part, keep the main thread holding only what matters — worth naming explicitly because the exam draws on the general subagent mechanics from the chapter on orchestration without re-teaching them here. What's specific to this chapter is the use case: Explore is what you reach for during the investigation half of a multi-phase task, to prevent exploration from exhausting the context window before you've even reached the phase where you act on what it found. A plan for a forty-five-file migration that had to read all forty-five files to produce itself is a plan you can no longer discuss productively in the same session — unless the reading happened somewhere else and only the plan came back.
What the exam tests
This chapter covers task statements 3.2 and 3.4. For 3.2, expect questions that hinge on exact placement: a command or skill that must reach every developer on clone goes in the project's .claude/commands/ or .claude/skills/, tracked in version control, while a personal variant that must not affect teammates goes in the user's home directory under a new name rather than an edit to the shared file. Expect the three SKILL.md frontmatter fields — context: fork, allowed-tools, argument-hint — tested by scenario rather than by name: a skill that floods the conversation with codebase-analysis noise or brainstorming detours wants context: fork; a skill that must not delete or destroy anything wants a tightly scoped allowed-tools; a skill invoked with no parameters wants argument-hint. Expect a skills-versus-CLAUDE.md question framed as "should this apply to every conversation or only when explicitly invoked," where the always-loaded answer is CLAUDE.md and the on-demand answer is a skill, regardless of how important the content feels. For 3.4, expect scenarios that describe a task's scope up front — a monolith-to-microservices rewrite, a forty-five-file migration, a choice between integration approaches with different infrastructure implications — where the correct read is that the complexity is already stated, so plan mode is chosen at the outset, not adopted midway after a direct-execution attempt has gone wrong. Expect the inverse case, a single-file fix against a clear stack trace or one added conditional, where plan mode is unnecessary ceremony. And expect a combined case, plan mode for investigation feeding direct execution for implementation, alongside a case where the Explore subagent's role is to keep verbose discovery output from exhausting context during a multi-phase task.
Exercises
- Write a project-scoped skill at
.claude/skills/audit-refund-flow/SKILL.md that reviews recent code touching process_refund for missing identity-verification checks. Give it context: fork, allowed-tools restricted to read-only operations, and no argument-hint — then write a second version that takes a date range and add the appropriate argument-hint. Invoke both and confirm the forked run's intermediate exploration never appears in your main transcript.
- Take a real refactor from your backlog that you would normally just start typing into Claude Code directly. Before touching it, write one sentence stating whether its complexity is declared up front or only apparent once you're inside it. If the former, run it through plan mode and compare the approved plan against what direct execution would have produced for the first few files.
- Create a personal skill variant: pick any project skill your team already has, copy it under a new name into
~/.claude/skills/, and modify it to fit a personal preference. Invoke the project skill by its original name in a fresh session and confirm it still behaves exactly as your teammates expect — your renamed copy should have had no effect on it.
- Simulate the 45-file migration scenario at small scale: pick a project with several call sites for one function you could plausibly restructure. Run plan mode first to produce an explicit ordered plan across every call site, then execute it directly. Note anywhere the plan turned out wrong once you were executing it, and consider whether an Explore subagent pass, run before planning, would have caught it.
Chapter 11 — Claude Code in CI
You already run Claude Code the way you'd run any other tool in your terminal: you type a prompt, it thinks out loud, it asks before it does something destructive, and if it gets stuck it waits for you. That last behavior is exactly the one that breaks the moment you put Claude Code into a pipeline. A GitHub Actions job, a Jenkins stage, a required status check — none of them have a human sitting at a terminal ready to answer a prompt. If Claude Code pauses to ask "should I proceed?" in that context, there is no one there to say yes, and the job hangs until the runner times out and the check goes red for a reason that has nothing to do with your code.
This chapter is about the small set of adjustments that make Claude Code behave like a CI participant instead of an interactive assistant: a flag that guarantees it never waits for input, a pair of flags that turn its output into something a bot can parse and post as PR comments, and the CLAUDE.md-based mechanism for telling a CI-invoked instance what "good" looks like when nobody is there to correct it in the moment. The running example is the one the exam leans on hardest: a PR review bot that reads a diff, checks it against your team's standards, and posts findings as inline comments. A second example — generating tests for changed code — shows up because it shares the same context-provisioning logic, just aimed at avoiding low-value output instead of duplicate comments.
None of this is new agent theory. It's the same Claude Code you use interactively, invoked in a way that respects the constraints of an unattended process: no stdin, a hard requirement for machine-readable output, and a script on the other end deciding whether to fail the build.
The flag that prevents the hang: -p
Run claude with no flags in a terminal and you get the interactive REPL — the experience you already know, where Claude can ask clarifying questions and wait for you to answer them. That mode is unusable inside a pipeline step, and not because pipelines can't run arbitrary CLI tools; it's that a pipeline step has no concept of "wait for a human to type something," so any process that blocks on stdin either hangs until the job's timeout kills it or, worse, sits there burning compute while nothing happens.
The flag that fixes this is -p, spelled out as --print. It puts Claude Code into non-interactive, single-shot mode: it takes the prompt you give it, does the work, writes the result to stdout, and exits. There is no back-and-forth, no follow-up question, no risk of blocking on input that will never arrive. This is the one flag in this chapter that shows up verbatim in the exam's own sample material, framed exactly as "a CI job is hanging waiting for interactive input — what fixes it."
claude -p "Review the diff between origin/main and HEAD for correctness issues, \
security concerns, and violations of the standards in CLAUDE.md" \
--output-format json \
--json-schema "$(cat ./ci/review-schema.json)"
Two names you may see offered as answers to that same question are worth naming and rejecting explicitly, because the exam's working notes flag them as a deliberate distractor: CLAUDE_HEADLESS and --batch do not exist. There is no environment variable called CLAUDE_HEADLESS and no --batch flag anywhere in Claude Code. They sound exactly like the kind of thing a CI tool would have — which is precisely why they're effective distractors — but they are invented, and if either appears as an answer option, it is wrong regardless of how the rest of the question is worded. A third tempting-but-wrong answer is redirecting stdin from /dev/null (claude "review this" < /dev/null). That's a general Unix trick for feeding an empty input stream to something that reads stdin, and it happens to stop some blocking behavior, but it doesn't address the actual command syntax Claude Code expects for non-interactive operation, and it doesn't give you the exit-and-write-stdout contract that -p guarantees. -p is the only real answer.
Everything below builds these flags by hand into a subprocess call, which is the right level of detail for understanding the mechanism, but current Anthropic tooling also ships anthropics/claude-code-action, an official GitHub Action that wraps a headless claude -p invocation for you — it accepts a claude_args input for passing arbitrary CLI flags (including --json-schema) and surfaces structured_output directly as a GitHub Actions output, rather than something your workflow has to parse out of stdout itself. For a GitHub Actions scenario specifically, the maintained action is the more idiomatic answer than hand-rolled subprocess/execFile calls; the code in this chapter is what that action is doing under the hood, and worth knowing at that level regardless of which one you'd actually reach for.
-p solves the hang. It does not, by itself, solve the next problem, which is that the thing reading Claude's output is not a person scanning a terminal — it's a script that has to turn "Claude's opinion about this diff" into inline PR comments at specific file-and-line locations, or into a pass/fail decision for a required status check. Free-form prose is exactly the wrong shape for that job, for the same reason a hand-parsed log line is a bad interface between two programs: it works until someone changes a word, and then your PR-comment bot either crashes or silently stops posting.
--output-format json switches Claude Code's output from prose to JSON. --json-schema goes further and lets you hand it a schema that constrains the shape of that JSON — field names, types, which fields are required — so the output isn't merely valid JSON, it's JSON your bot can deserialize into a known type without a validation step of its own. The two flags are meant to be used together for this use case: --output-format json alone gets you machine-readable output, but pairing it with --json-schema is what the exam's own materials point to when the requirement is "produce machine-parseable structured findings for automated posting as inline PR comments." Reaching for one without the other is the kind of half-answer that shows up as a distractor — an option that names --output-format json alone, or that skips the schema and proposes parsing prose with a regex, is not the pairing the exam rewards.
One mechanical detail the exam guide does not state but the CLI enforces: --json-schema takes the schema inline, as a JSON string, not a path to a schema file. Passing --json-schema ./ci/review-schema.json fails immediately with --json-schema is not valid JSON. Keep the schema in a file for version control by all means, but read it and pass its contents — "$(cat ./ci/review-schema.json)" in shell, or the string you get from reading the file, as the examples below do.
The response shape has a matching gotcha, and it's worth being precise about which flag produces which shape, since the two are easy to mix up under exam pressure. --output-format json hands you a single JSON object — fields include result, session_id, usage, total_cost_usd, and, when --json-schema is also passed, structured_output sitting directly on that object. The array-of-events shape (system, assistant, user, result entries, one per turn) belongs to a different flag, --output-format stream-json — a newline-delimited event stream meant for live progress display, not the structured-findings use case this chapter is building toward, and one that needs --verbose --include-partial-messages to get token-level streaming out of it. For the review-bot pattern, the deserialize step is json.loads(stdout)["structured_output"], not an event array indexed by position. Getting this wrong — treating --output-format json's output as an array — is a TypeError on the first CI run, and the helper below exists so the rest of the examples can stay readable.
A finding schema for the review bot might look like this:
{
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file": { "type": "string" },
"line": { "type": "integer" },
"severity": { "type": "string", "enum": ["blocking", "warning", "nit"] },
"message": { "type": "string" },
"issueId": { "type": "string" }
},
"required": ["file", "line", "severity", "message", "issueId"]
}
}
},
"required": ["findings"]
}
That issueId field is not decoration — it's what makes the "avoid duplicate comments on re-review" workflow below possible, so build it into the schema from the start rather than retrofitting it later. The chapter on structured output goes deeper into schema design generally; here the point is narrower: the schema is what turns "Claude reviewed the PR" into "a bot posted three inline comments, one of them blocking," with nothing brittle in between.
Python
import json
import subprocess
def claude_structured(prompt: str, schema_path: str) -> dict:
"""Run Claude Code non-interactively and return the structured payload."""
schema = open(schema_path).read()
completed = subprocess.run(
["claude", "-p", prompt, "--output-format", "json", "--json-schema", schema],
capture_output=True, text=True, check=True,
)
return json.loads(completed.stdout)["structured_output"]
def run_review(schema_path: str, base_ref: str = "origin/main") -> dict:
return claude_structured(
f"Review the diff between {base_ref} and HEAD against the standards "
"in CLAUDE.md. Report only issues you are confident about.",
schema_path,
)
def post_inline_comments(findings: dict, pr_number: int) -> None:
for finding in findings["findings"]:
github_client.create_review_comment(
pr_number,
path=finding["file"],
line=finding["line"],
body=f"**{finding['severity']}**: {finding['message']}",
)
TypeScript
import { execFile } from "node:child_process";
import { promises as fs } from "node:fs";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
async function claudeStructured<T>(prompt: string, schemaPath: string): Promise<T> {
const schema = await fs.readFile(schemaPath, "utf-8");
const { stdout } = await execFileAsync("claude", [
"-p", prompt, "--output-format", "json", "--json-schema", schema,
]);
const response = JSON.parse(stdout) as { structured_output: T };
return response.structured_output;
}
interface Finding {
file: string;
line: number;
severity: "blocking" | "warning" | "nit";
message: string;
issueId: string;
}
async function runReview(schemaPath: string, baseRef = "origin/main"): Promise<{ findings: Finding[] }> {
return claudeStructured(
`Review the diff between ${baseRef} and HEAD against the standards in CLAUDE.md. ` +
"Report only issues you are confident about.",
schemaPath,
);
}
async function postInlineComments(findings: { findings: Finding[] }, prNumber: number) {
for (const finding of findings.findings) {
await githubClient.createReviewComment(prNumber, {
path: finding.file,
line: finding.line,
body: `**${finding.severity}**: ${finding.message}`,
});
}
}
Notice the prompt text itself: "report only issues you are confident about." The exam scenario for this pattern names minimizing false positives as an explicit design goal, and that's not a throwaway line — a review bot that posts a comment on every stylistic quibble trains the team to ignore it within a week, the same way a monitoring system that pages on everything gets its pages muted. The schema constrains the shape of what Claude says; the prompt and the CLAUDE.md content below constrain what's worth saying at all.
Non-interactive execution: --bare, permissions, and no one left to ask
-p gets you a process that returns instead of blocking, but a truly unattended run needs two more things settled: that it doesn't pick up context you didn't intend for it to have, and that it doesn't stop to ask permission for an action with no one there to grant it.
Current Claude Code documents --bare as the recommended mode for CI and scripting specifically because it skips automatic discovery of hooks, skills, commands, subagents, plugins, MCP servers, CLAUDE.md, and auto memory — everything that would otherwise get picked up implicitly depending on what happens to be sitting in the environment the job runs in. That's a genuine tension with this chapter's own thesis that CLAUDE.md is the CI context mechanism: if you run with --bare for the determinism it buys you, CLAUDE.md doesn't load automatically either, and a review bot that wants both reproducibility across runners and your team's standards has to supply that content explicitly — passing it via --append-system-prompt-file, for instance, rather than relying on --bare's automatic discovery to find it the way an interactive session would.
The second piece is permission prompts. Nothing in the examples so far addresses what happens when Claude Code wants to run a command or edit a file and would normally pause to ask — in a pipeline, that pause is another hang. --permission-mode (auto, dontAsk, acceptEdits) and --allowedTools are the flags that settle this ahead of time rather than leaving it to be discovered mid-run: --allowedTools names specific tools as pre-approved for the invocation, and a permission mode sets the default stance for anything not explicitly listed. A review bot that only reads a diff and writes structured output to stdout needs very little of this; a test-generation pass that's expected to write new test files needs the write tool pre-approved, or it stalls exactly like an unanswered stdin prompt would.
CLAUDE.md as the CI context mechanism
You already know CLAUDE.md as the place project conventions live so that every interactive session starts with the same shared understanding instead of you re-explaining it each time. Nothing changes about that mechanism when the invoking process is a CI job instead of you at a keyboard — CLAUDE.md is loaded the same way, and that consistency is exactly the point. A CI-invoked Claude Code instance has no memory of the conversations you've had with it interactively, no accumulated sense of "this team doesn't like magic numbers" or "we always use the Fixtures.customer() builder instead of constructing objects by hand." All of that has to be written down somewhere the CI invocation will actually read, and CLAUDE.md is that somewhere.
For the review bot, that means testing standards, fixture conventions, and review criteria belong in CLAUDE.md, in specifics rather than platitudes. "Follow best practices" tells a reviewing Claude nothing it doesn't already default to; "flag any process_refund call that doesn't pass an idempotency key" tells it exactly what your team has been burned by before. For the test-generation angle, the same file is where you document what makes a test worth generating in the first place — which is a separate, and separately testable, skill from the review bot.
Test generation: documenting what a valuable test looks like
Point Claude Code at a diff and ask it to "write tests for this" and you'll get tests — plenty of them, some useful and a lot of them restating what a type checker already guarantees or re-testing a getter. The exam's Code Generation with Claude Code scenario names this directly: the fix is documenting testing standards, valuable-test criteria, and available fixtures in CLAUDE.md, so a CI-invoked generation pass has the same judgment about what's worth testing that a senior engineer on the team would apply by hand.
Concretely, that's a section of CLAUDE.md that says something like: prefer one test per distinct business rule over one test per code path; a test that only exercises a constructor or a trivial getter is not valuable; use the Fixtures module for customer and order data rather than constructing literals inline; edge cases around refund windows and partial refunds are always worth covering, formatting variations in a description field are not. That's specific enough to change what gets generated, which is the whole test of whether a CLAUDE.md section is doing its job.
The second lever is showing Claude Code the existing test suite, not just the changed source file, when you ask it to generate tests. Without that, a generation pass has no way to know test_refund_after_window_closed already exists, and it will cheerfully propose a near-duplicate under a slightly different name. With the existing test files in context, it can check its own output against what's already covered and generate only what's missing.
Python
def generate_tests_for_pr(changed_file: str, existing_test_file: str, schema_path: str) -> dict:
existing_tests = open(existing_test_file).read()
return claude_structured(
f"Generate tests for the changes in {changed_file}. "
"Testing standards and valuable-test criteria are in CLAUDE.md. "
"Here are the existing tests for this module — do not propose scenarios "
f"already covered:\n\n{existing_tests}",
schema_path,
)
TypeScript
async function generateTestsForPr(
changedFile: string, existingTestFile: string, schemaPath: string,
): Promise<{ tests: unknown[] }> {
const existingTests = await fs.readFile(existingTestFile, "utf-8");
return claudeStructured(
`Generate tests for the changes in ${changedFile}. ` +
"Testing standards and valuable-test criteria are in CLAUDE.md. " +
"Here are the existing tests for this module — do not propose scenarios " +
`already covered:\n\n${existingTests}`,
schemaPath,
);
}
Why the session that wrote the code is a poor judge of it
Here's a temptation worth naming directly: the same Claude Code session that just implemented escalate_to_human or refactored extract_invoice is sitting right there with full context on the change, so why not just ask it to review its own diff before opening the PR? It would save a second invocation.
The exam's position is that this doesn't work as well as an independent instance, and the reasoning is worth having rather than just memorizing. A session that generated the code has already made a sequence of small judgment calls while writing it — this edge case is unlikely enough to skip, this naming is close enough, this shortcut is fine given the time budget — and each of those calls felt reasonable at the moment it was made, in the context of everything else going on in that session. Ask that same session to review its own work and you're asking it to grade decisions it just finished rationalizing to itself. It has momentum toward "yes, this is fine" baked into the same context window that produced the code, and self-consistency works against you here: a session tends to stay consistent with judgments it already made rather than reopening them from scratch.
A fresh Claude Code invocation — a new process, a new session, no memory of why any particular shortcut got taken — has none of that momentum. It reads the diff cold, against CLAUDE.md's stated standards, with no investment in any of the choices that produced it. That's a structurally better position for catching the case where a null check got skipped, or a fixture got misused, or a comment doesn't match what the code actually does. This is a specific instance of a broader theme — how provenance and independence shape what a review catches — that the chapter on escalation, review, and provenance develops in full; the takeaway here is narrower and purely operational: your CI pipeline should invoke a separate claude -p review step rather than having the generating session mark its own homework, and if an exam question offers "have the same session that wrote the code review it" as an option, treat it as the trap it's designed to be.
A PR review bot that re-runs on every push has an obvious failure mode: push a new commit, the bot reviews the whole diff again, and it posts the same three comments it posted last time, because nothing told it those issues were already flagged. That's noise, and noisy bots get their comments collapsed and ignored, which defeats the entire point of running one.
The fix follows directly from the fact that a fresh claude -p invocation has no memory of the last run — it isn't the same session, so nothing carries over automatically. You have to hand it the prior findings explicitly, as part of the prompt, and instruct it to report only what's new or still unaddressed. That issueId field from the schema earlier is what makes "still unaddressed" checkable: the bot's own state (a file, a PR-comment thread, a small database row) holds the ids from the last run, and this run's prompt includes them.
Python
def run_incremental_review(schema_path: str, prior_findings: list[dict], base_ref: str) -> dict:
prior_summary = json.dumps(prior_findings)
return claude_structured(
f"Review the diff between {base_ref} and HEAD against CLAUDE.md standards. "
f"Here are findings from the previous review of this PR: {prior_summary}. "
"Report only issues that are new since that review, or that were previously "
"flagged and remain unaddressed in the current diff. Reuse the same issueId "
"for a still-unaddressed issue rather than minting a new one.",
schema_path,
)
TypeScript
async function runIncrementalReview(
schemaPath: string, priorFindings: Finding[], baseRef: string,
): Promise<{ findings: Finding[] }> {
return claudeStructured(
`Review the diff between ${baseRef} and HEAD against CLAUDE.md standards. ` +
`Here are findings from the previous review of this PR: ${JSON.stringify(priorFindings)}. ` +
"Report only issues that are new since that review, or that were previously " +
"flagged and remain unaddressed in the current diff. Reuse the same issueId " +
"for a still-unaddressed issue rather than minting a new one.",
schemaPath,
);
}
Two weaker alternatives are worth ruling out by name, because they surface as distractors. Simply not re-running review after new commits abandons the feature — you lose the safety net for exactly the case where a fix introduces a new problem. Diffing the raw comment text between runs to suppress "repeats" is fragile in the other direction: rewording a message slightly, or Claude phrasing the same issue differently on a second pass, breaks the match and the duplicate reappears. Passing prior findings and asking for a judgment call about "new or unaddressed" is the one approach that survives both a slightly reworded message and a fixed issue dropping off the list.
What the exam tests
This chapter covers task statement 3.6 in full, and the exam treats it as a compact, flag-and-mechanism-heavy topic rather than a design-judgment one, so expect direct recall questions alongside scenario questions. Know -p (--print) as the only real fix for a CI job hanging on interactive input, and be ready to reject CLAUDE_HEADLESS and --batch by name as invented options and /dev/null redirection as a workaround that doesn't address the actual command contract — this exact setup is the exam's own sample question 10. Know that --output-format json and --json-schema are meant to be used together, not separately, to produce structured findings suitable for posting as inline PR comments — and know that the resulting payload is a single object with structured_output on it, not an array of turn events (that shape belongs to --output-format stream-json, a different flag for a different job). Know that CLAUDE.md is the mechanism for supplying a CI-invoked instance with testing standards, fixture conventions, and review criteria it has no other way of knowing, and that --bare — the recommended flag for deterministic CI runs — skips CLAUDE.md's automatic loading along with everything else auto-discovered, so a scenario combining "fully reproducible across runners" with "must follow our CLAUDE.md standards" wants CLAUDE.md content supplied explicitly, not left to autodiscovery. --permission-mode and --allowedTools are the mechanism for pre-clearing actions so a pipeline doesn't stall on an unanswered permission prompt the way it would on unanswered stdin. Be able to explain, not just state, why an independent review instance outperforms the generating session reviewing its own work — the momentum and self-consistency argument, not just the conclusion — since the exam asks this as a "why" as often as a "what," and connect it in your own head (though not necessarily on the exam) to the deeper treatment in the chapter on escalation, review, and provenance. Finally, know both avoid-duplicate-work skills as a pair: prior findings plus an instruction to report only new-or-unaddressed issues for re-review, and existing test files in context to keep generated tests from duplicating coverage that already exists.
Exercises
- Wire a
claude -p review step into a real CI job — a GitHub Actions workflow is the fastest path — that runs on every pull request, produces --output-format json output validated against a --json-schema you write yourself, and posts each finding as an inline comment via the GitHub API. Confirm the job never hangs, even on a PR with a large diff.
- Write a CLAUDE.md section describing your review criteria and testing standards as specifically as you can — real fixture names, real "not worth flagging" examples — then run the same diff through the review bot with and without that section present. Compare the findings and note what changed, including anything that disappeared as a false positive rather than something new that appeared.
- Build the incremental-review flow: run the bot once, capture its findings with their
issueIds, fix one issue and leave another untouched, push a new commit, and re-run with the prior findings included in the prompt. Confirm the fixed issue drops off and the unaddressed one persists with the same id rather than being reported as new.
- Take a session that just implemented a nontrivial change to
process_refund or extract_invoice and ask it to review its own diff, then separately invoke a fresh claude -p review of the same diff against the same CLAUDE.md. Compare the two sets of findings and see whether the fresh instance catches anything the generating session waved through.
Chapter 12 — Prompt engineering
You have already done prompt engineering, even though you have never called it that. Every time you tightened a CLAUDE.md instruction because Claude kept doing the wrong thing, or added a note to a slash command because the output format kept drifting between runs, you were engineering a prompt. The gap between "check that comments are accurate" and "flag comments only when claimed behavior contradicts actual code behavior" is not a stylistic gap — it is the difference between an instruction the model can apply consistently and one it has to guess at, and you have felt that guess land wrong before, probably in a CLAUDE.md file you rewrote three times before it stuck.
This chapter turns that felt experience into a taxonomy you can reason about under exam pressure, because the exam does not ask you to write eloquent prompts — it asks you to diagnose which failure you are looking at and pick the matching fix. Two of those fixes look similar from the outside and get confused constantly: writing explicit criteria and writing few-shot examples. Both are aimed at the same symptom, a review or extraction step producing findings nobody trusts, and both involve adding words to a prompt, but they treat different diseases. One is for a rule that was never written down. The other is for a rule that was written down and is still applied inconsistently. Telling those apart is, by a comfortable margin, the single most examinable judgment call in this domain, and it anchors task statements 4.1 and 4.2.
The third topic, iterative refinement under task statement 3.5, is a different discipline entirely — how you as a developer converge on a working prompt or implementation through rounds of feedback, rather than what the finished prompt should contain. Keep the two apart mentally: 4.1 and 4.2 are about the shape of the instructions inside a single prompt; 3.5 is about the process you run, turn after turn, to get there. The exam tests them as separate skills, so this chapter treats them as separate sections.
The running example: a code review bot with a trust problem
Picture a review bot built on Claude Code — the "Code Generation with Claude Code" scenario the exam returns to repeatedly — that runs on every pull request and posts findings as comments. It has been live for a month. The security team likes it; the rest of engineering is starting to ignore it, because roughly one comment in three is noise: a stylistic nit dressed up as a "medium" severity finding, or a false alarm about a pattern that is standard in this codebase. Developers who get burned by noise stop reading anything the bot says, including the finding that would have caught a real bug. That is the mechanism the exam wants you to know by name: a high false-positive rate in one review category drags down trust in every other category the same bot produces, because the developer reading the comment has no way to tell which category it came from. Trust is not compartmentalized per finding type — it is a single reservoir, and one leaky category drains all of it.
So you have a precision problem, and the instinct almost every engineer reaches for first is to tell the model to calm down: add a line saying "be conservative" or "only report high-confidence findings." This is worth naming because the exam names it, as a trap. General appeals to caution do not improve precision compared to specific categorical criteria: "be conservative" gives the model no new information about which pattern is the problem. It was already guessing at what counts as worth flagging; asking it to be more conservative about that same undefined guess just makes it guess less often, in both directions, in a ratio you did not choose — because you never told it what the boundary actually is.
4.1 — When the rule itself does not exist
The fix that actually works is to write down the rule. Task statement 4.1 is about the case where the review bot's decision boundary — what counts as worth reporting versus what is fine to let pass — was never defined with any precision. "Check that comments are accurate" asks the model to adjudicate a fuzzy, subjective standard with no stated edge cases, and it will adjudicate it differently on Monday and on Friday. "Flag comments only when claimed behavior contradicts actual code behavior" gives it an operational test it can run against any comment — read the claim, read the code, check for contradiction — and that test produces the same verdict regardless of who reviewed the code or what mood the model is metaphorically in.
The generalizable skill is writing criteria that state which categories to report and which to skip, rather than filtering by confidence. Confidence-based filtering ("only report things you're sure about") sounds like precision engineering, but it inherits the same defect as "be conservative": confidence is the model's own self-assessment of an undefined question, and that self-assessment is not more reliable than the question itself. What actually moves precision is defining the categories — bugs and security issues in scope, minor style preferences and locally-idiomatic patterns out — stated as a rule about the code, not about how sure the model feels.
Python
REVIEW_SYSTEM_PROMPT = """
You are reviewing a pull request diff for get_customer, lookup_order, and
process_refund. Report an issue only if it falls into one of these categories:
- BUG: the code will produce an incorrect result or crash on some input that
the diff's tests do not cover. Do not report a bug for behavior that matches
the existing tests, even if you would have designed it differently.
- SECURITY: the change introduces an injection risk, a missing authorization
check on a path that mutates customer data, or logs a secret or PII field.
- COMMENT_MISMATCH: a comment or docstring makes a factual claim about
behavior that contradicts what the code actually does. Do not report a
comment merely because it is vague, outdated in tone, or could be phrased
better — only contradiction counts.
Do NOT report: formatting, naming conventions, import order, or any pattern
that already appears elsewhere in this file. If a pattern is already in the
codebase, it is a local convention, not a bug, even if you would write it
differently.
"""
TypeScript
const REVIEW_SYSTEM_PROMPT = `
You are reviewing a pull request diff for getCustomer, lookupOrder, and
processRefund. Report an issue only if it falls into one of these categories:
- BUG: the code will produce an incorrect result or crash on some input that
the diff's tests do not cover. Do not report a bug for behavior that matches
the existing tests, even if you would have designed it differently.
- SECURITY: the change introduces an injection risk, a missing authorization
check on a path that mutates customer data, or logs a secret or PII field.
- COMMENT_MISMATCH: a comment or docstring makes a factual claim about
behavior that contradicts what the code actually does. Do not report a
comment merely because it is vague, outdated in tone, or could be phrased
better -- only contradiction counts.
Do NOT report: formatting, naming conventions, import order, or any pattern
that already appears elsewhere in this file. If a pattern is already in the
codebase, it is a local convention, not a bug, even if you would write it
differently.
`;
Severity gets the same treatment: if labels are inconsistent — one reviewer's "medium" is another's "high" — the fix is to define each severity level with a concrete code example anchoring it, so classification stops depending on an unstated internal scale. "High severity: an unauthenticated endpoint that returns another customer's order history, like lookup_order(order_id) with no ownership check against the caller" is a criterion the model can pattern-match against. "High severity: something that seems important" is not.
One more move under 4.1 is worth sitting with, because it sounds like giving up but is actually the disciplined answer: when a category's false-positive rate is destroying trust and you have not yet worked out the right criteria, turn that category off. Temporarily disabling COMMENT_MISMATCH, shipping a bot that only reports BUG and SECURITY, restores trust in the categories that remain — a bot that is right three out of three earns attention, while one that is right two out of three earns skepticism toward everything it says, including the ones it got right. Improve the disabled category's prompt offline, against a sample set, and turn it back on once its precision is defensible. The exam treats this as a legitimate first-class answer, not a cop-out.
4.2 — When the rule exists but bends on hard cases
Now change the failure. Suppose the bug/security/comment-mismatch criteria are in place, well written, specific — and the bot is still inconsistent, but only on ambiguous cases near the boundary. It correctly skips an obviously-fine local pattern and correctly flags an obviously-real SQL injection, but on the pull request where process_refund catches a broad exception and logs it without re-raising — arguably a bug, arguably a deliberate degrade-gracefully pattern — it flags it on some runs and not others. The rule is defined; it is being applied inconsistently on cases where applying it takes judgment rather than lookup. That is a different disease, and it needs a different medicine: few-shot examples.
Few-shot prompting means including, inside the prompt itself, two to four fully worked examples of the task — not a description of what good output looks like, but actual instances of input paired with the reasoning and output you want. It is the most effective technique for consistently formatted, actionable output when prose instructions alone keep producing variation, and it outperforms more prose because a worked example transmits something prose cannot: a demonstration of the reasoning that resolves an ambiguous case, which the model can generalize to structurally similar cases it has never seen. That generalization is the entire point and the thing that separates a good few-shot set from a lookup table — you are not anticipating every ambiguous pattern that will ever appear in a diff, you are showing the model how to weigh competing considerations when it's truly unclear, so it can apply the same weighing to a case it has never seen.
The examples earn their keep specifically by showing the reasoning, not merely the verdict. An input/output pair that says "diff X → no finding" teaches the model what the answer was for that one diff. An input/reasoning/output triple that says "diff X → this broad catch swallows the exception without logging the customer-facing error path, and there's no upstream handler visible in this file, so it's a bug, not a deliberate degrade → BUG, medium" teaches the model the test to run on the next ambiguous diff. That difference is exactly the axis the exam probes: examples without reasoning are weaker at generalizing to novel cases than examples that show why one plausible action was chosen over another.
Python
FEW_SHOT_EXAMPLES = """
Example 1 — ambiguous exception handling
Diff: process_refund catches `PaymentGatewayError` broadly, logs the message
at INFO level, and returns a generic success response to the caller.
Reasoning: swallowing a payment failure and reporting success to the caller
means a failed refund looks successful to the customer-facing code. This is
not a deliberate degrade-gracefully pattern (there's no fallback path here,
just silence), and the customer-facing consequence is a real correctness
issue, not a style choice.
Output: {"location": "process_refund:47", "issue": "Payment failure is
caught and logged but reported to the caller as success", "severity": "high",
"suggested_fix": "Return an error result when the gateway call fails instead
of a generic success payload"}
Example 2 — a broad catch that IS acceptable
Diff: lookup_order catches `Exception` around a metrics-emission call, logs
at DEBUG, and continues; the function's real return value is computed before
the try block and is unaffected either way.
Reasoning: the broad catch only protects a non-critical side effect
(metrics), and its failure cannot alter the function's actual return value.
This matches the existing pattern used around every other metrics call in
this file, so it's a local convention, not a bug.
Output: no finding.
"""
TypeScript
const FEW_SHOT_EXAMPLES = `
Example 1 -- ambiguous exception handling
Diff: processRefund catches PaymentGatewayError broadly, logs the message at
INFO level, and returns a generic success response to the caller.
Reasoning: swallowing a payment failure and reporting success to the caller
means a failed refund looks successful to the customer-facing code. This is
not a deliberate degrade-gracefully pattern (there's no fallback path here,
just silence), and the customer-facing consequence is a real correctness
issue, not a style choice.
Output: {"location": "processRefund:47", "issue": "Payment failure is caught
and logged but reported to the caller as success", "severity": "high",
"suggestedFix": "Return an error result when the gateway call fails instead
of a generic success payload"}
Example 2 -- a broad catch that IS acceptable
Diff: lookupOrder catches a generic error around a metrics-emission call,
logs at DEBUG, and continues; the function's real return value is computed
before the try block and is unaffected either way.
Reasoning: the broad catch only protects a non-critical side effect
(metrics), and its failure cannot alter the function's actual return value.
This matches the existing pattern used around every other metrics call in
this file, so it's a local convention, not a bug.
Output: no finding.
`;
Notice what those two examples do structurally, because the pairing is deliberate and it is a second named use of few-shot: one demonstrates a genuine issue, the other an acceptable pattern that superficially resembles one. Showing both sides of that line, rather than only positive examples of things to flag, teaches the model to distinguish acceptable local patterns from actual problems instead of pattern-matching on surface features like "broad exception catch" regardless of consequence.
Few-shot examples also carry the output format itself. If you want every finding to arrive as location, issue, severity, and suggested fix, in that order, telling the model that shape in prose is weaker than showing it inside worked examples — the model copies structure far more reliably than it follows a structural description, the same way Claude Code hews closer to a CLAUDE.md example block than to an equivalent paragraph of prose.
The same ambiguous-case pattern shows up outside code review, in tool selection. A support agent built around lookup_order and escalate_to_human will handle "where's my package" cleanly — that's an unambiguous lookup_order call — but a message like "this is the third time my order's been late, I want someone to look at my account" is split evenly between the two: there's an order to look up, and there's a customer who plausibly wants a human. A few-shot example that shows the reasoning — the pattern history and the word "someone" both point toward escalation, and looking up the order first would just delay getting a person into the conversation, so escalate_to_human is chosen over the equally-plausible lookup_order — teaches the model the weighing, not just the verdict, so it generalizes to the next request that splits the same way on different words.
The same tool applies past code review, into extraction, which is where 4.2's fourth named use lives: reducing hallucination on documents with varied structure. An extract_invoice(document) tool will happily fabricate a total_amount with a plausible number when the source states the amount only as "call it forty-five hundred, even" in a footnote, unless it has seen an example of exactly that informal phrasing handled correctly. The fix is the same shape as the code-review examples: two or three worked extractions covering document structures you will actually hit — a line-item table with a separate totals block, a total stated only in running prose, a discount implied rather than itemized — each paired with the correct output, including what to do when a field is actually absent (return null, don't invent a number). The document-structure version of this problem, inline citations versus a trailing bibliography, a methodology section stated up front versus embedded case-by-case in the results, gets the same medicine: show the model, don't just tell it, because "tell it" is prose, and prose is exactly the layer that produced the inconsistency in the first place.
Telling 4.1 and 4.2 apart under exam pressure
The question you are actually being asked, every time a scenario describes a review or extraction step behaving badly, is diagnostic: does the boundary exist, or does it exist but wobble on hard cases? If findings are inconsistent and nobody ever wrote down what counts as a bug versus a style nit, that is an undefined rule — a 4.1 answer, and few-shot examples are the wrong tool, because there is no correct behavior yet for them to demonstrate. If the criteria are already stated, reviewers agree the categories are right, and the trouble is specifically on cases everyone would call "judgment calls," that is inconsistent application of a defined rule — a 4.2 answer, and rewriting the criteria in slightly different prose will not move the needle, because the criteria were never the missing ingredient.
| Symptom |
Diagnosis |
Fix |
| No stated rule for what counts as a bug vs. a nit |
Undefined decision boundary |
4.1 — explicit categorical criteria |
| Severity labels vary reviewer to reviewer, no anchor |
Undefined severity scale |
4.1 — severity criteria with concrete examples per level |
| One category has terrible precision, trust is collapsing |
Undefined or poorly-tuned rule for that category |
4.1 — disable the category temporarily, fix its prompt offline |
| Rule is written down, edge cases still go wrong |
Defined boundary, inconsistent application |
4.2 — 2 to 4 few-shot examples with reasoning |
| Output format drifts run to run despite instructions |
Defined intent, inconsistent formatting |
4.2 — examples showing the literal output shape |
| Extraction returns null or fabricates values on unusual documents |
Defined schema, inconsistent handling of varied structure |
4.2 — examples spanning the structural variants |
Do not read "be conservative" as a milder version of explicit criteria. It occupies neither box in that table — it is not a criterion and it is not an example, it is an instruction with no content, and the exam will offer it as a distractor in scenarios that actually call for either fix.
3.5 — Iterative refinement as a developer discipline
Everything above concerns the finished shape of a prompt. Task statement 3.5 concerns something upstream: how you, working with Claude Code turn by turn, actually arrive at a working prompt or implementation, rather than what the winning version contains. It is a distinct skill the exam tests separately, so treat it as its own topic rather than a coda to few-shot examples.
The first technique is one you already reach for when explaining a refactor to a colleague and prose keeps failing: concrete input/output examples. When a transformation is hard to describe in words — "normalize this date format, but handle the three legacy formats we still see in old records" — natural-language description gets interpreted inconsistently across attempts, while two or three concrete pairs of exact input and exact expected output pin it down unambiguously. This is the same mechanism as few-shot prompting from 4.2, applied to communicating a spec during development rather than embedding permanent examples in a production prompt — the audience and lifespan differ, but the reason it works ("show, don't describe") is identical. The clearest named case is edge-case handling in a data migration script: rather than writing "handle null values sensibly," you hand over a concrete case — input row with customer_id: null, order_total: 45.00, expected output with the record routed to a quarantine table rather than inserted with a fabricated ID — and Claude Code converges on the right behavior far faster than it does from an adjective like "sensibly."
The second technique is test-driven iteration, and it inverts the usual order of operations: write the test suite before the implementation, covering expected behavior, edge cases, and performance requirements, then iterate by handing Claude Code the test failures rather than re-describing what you want in prose. A failing test is a precise, falsifiable statement of a gap; a paragraph explaining what you wanted is not, and every round of "no, I meant..." you avoid by pointing at a red test is a round where the prompt does not have to compensate for an ambiguity a test would have caught for free.
The third, the interview pattern, runs opposite to how most engineers start a task: instead of specifying everything up front, you ask Claude Code to interview you first — to ask the questions a competent engineer would ask before touching the code. This is most valuable where you are least equipped to enumerate pitfalls yourself: an unfamiliar domain, where the model's breadth of exposure to prior systems can surface a consideration you would not have thought to specify, like cache invalidation strategy for a caching layer, or which failure modes matter for a job queue you have never operated. You have felt a version of this whenever Claude Code, mid-plan-mode, asked a clarifying question you had not anticipated — the interview pattern is that same behavior, invoked deliberately and up front.
The fourth is a sequencing judgment: when you have found multiple problems, report them one at a time or all at once? The rule is whether they interact. Independent problems — a typo in one function and an unrelated off-by-one in a different, unconnected function — are best fixed sequentially, because bundling adds no information and makes each fix harder to verify. Interacting problems — a caching bug and a related invalidation-ordering bug that only makes sense once you understand the caching bug — are best delivered together in one detailed message, because fixing them one at a time risks a fix for the first invalidating your description of the second, or Claude Code converging on a solution to problem one that makes problem two worse. A migration script's null-handling bug and its transaction-boundary bug, fixed in two separate uncoordinated passes when the right null-handling fix depends on where the boundary ends up, is the mistake the exam wants you to catch.
Context awareness: a newer lever for long-running prompts
Everything above assumes you are the only one tracking how much room is left in the conversation. As of the current API, that stopped being strictly true: on Sonnet 5, Sonnet 4.6, Sonnet 4.5, and Haiku 4.5, Claude automatically receives injected <budget:token_budget> and <system_warning>Token usage: X/Y remaining</system_warning> tags in the system prompt and tool results as a conversation runs, telling it how much context space remains without you having to compute or inject that number yourself. This is context awareness, and it changes what an iterative-refinement or long-running-agent prompt can usefully say — you can now write an instruction like "as you approach your token budget, stop exploring and write up what you have" and expect Claude to know, from the warnings it is already receiving, when that instruction applies.
This is newer material and worth flagging as such precisely because it looks like it makes the case-facts and manifest patterns in Chapter 14 unnecessary — it does not. Context awareness tells the model how much space is left; it says nothing about which facts survive compaction once that space runs out. The two operate at different layers: budget awareness is about pacing a task against a shrinking window, while the case-facts block and manifest are about which specific numbers and conclusions must never be paraphrased away regardless of how much room remains. A model that knows it is at 90% of budget and wraps up gracefully can still lose a dollar amount to summarization on the way there.
One caveat worth carrying into a scenario question: models older than Opus 4.7 do not get this automatically. They instead require an explicit "task budget" passed via a beta flag, spelling out the budget in the prompt yourself rather than relying on injected system warnings — the same underlying idea, but manual rather than automatic.
What the exam tests
Task statements 4.1 and 4.2 are tested primarily as a diagnostic pair, and the single most common trap is offering "be more conservative" or "raise the confidence threshold" as a fix that actually needs either explicit criteria or few-shot examples — treat any general appeal to caution as wrong regardless of the underlying symptom. Expect scenarios requiring you to decide whether a decision boundary is simply missing (4.1: write the criteria, define severity with concrete examples, or disable a collapsing category temporarily to protect trust in the rest) or present but shaky on ambiguous cases (4.2: two to four few-shot examples that show reasoning, distinguishing acceptable patterns from genuine issues, demonstrating the target output format). Extraction questions under 4.2 test whether you recognize varied-document-structure handling and null-field fabrication as few-shot problems once the schema is already correct — if the schema is the issue, that is a different chapter's material. Task 3.5 is tested independently: a scenario needing concrete input/output examples over more prose once prose has already failed twice, a scenario rewarding test-first iteration with test failures as the feedback channel, a scenario where the interview pattern is right because the domain is unfamiliar to the developer, and a sequencing question hinging on whether the reported problems interact.
Exercises
-
Write a review prompt with only vague instructions ("check for bugs and be careful about false positives"). Run it against a diff with one real bug and two deliberately borderline patterns, note what it flags, then rewrite the prompt with explicit categories stating what to report and what to skip, leaving the diff unchanged. Confirm the second version's judgment on the borderline cases moved — that movement is the 4.1 effect.
-
Extend the criteria-only prompt from exercise 1 with two few-shot examples that each show reasoning for a borderline case, one that should be flagged and one that should not. Run the same prompt against a third, novel borderline pattern you have not shown it before, and confirm the model generalizes the reasoning rather than only matching the literal examples — that generalization is the point of 4.2.
-
Pick a truly ambiguous transformation task — normalizing real-world date strings, or mapping informally-worded customer requests onto tool names like lookup_order versus escalate_to_human — and run it through Claude Code three times using only a prose description. Record the inconsistency, then supply two to three concrete input/output examples for the same task and re-run it three more times, and write down whether the variance dropped.
-
Construct a two-problem scenario where the problems actually interact — a rate limiter with both a race condition in its counter increment and an off-by-one in its window boundary, where fixing the boundary changes where the race condition bites. Report both to Claude Code in one detailed message and note the fix. Then, in a fresh session, report them sequentially, one message per problem, and compare. If the sequential version produces a fix for the first problem that has to be partially undone once the second surfaces, you have reproduced the 3.5 sequencing failure the exam tests for.
Chapter 13 — Structured output
Every agent eventually has to hand its output to something that is not a person. A downstream service wants a JSON object with specific fields, an accounting system wants a total that reconciles, a database wants a row shape it can insert without a parsing step. The moment you need that guarantee you are in the territory of structured output, and the instinct most people bring from earlier language models — write a careful prompt, ask for JSON, hope — is exactly the instinct this chapter replaces.
The running example is invoice extraction: a tool called extract_invoice that takes a scanned or copy-pasted invoice and returns vendor, line items, taxes, and a total, in a shape your accounts-payable pipeline can consume directly. It's a good anchor because it fails in two different ways, and the fix for one does nothing for the other. An extraction can come back as malformed JSON — a dangling comma, an unescaped quote in a vendor name with an ampersand in it — or as perfectly well-formed JSON that is simply wrong: three line items summing to $412.50 while the field labeled total says $391.00. The first is a syntax problem, the second a semantic one, and they call for different tools. An engineer who reaches for a stricter schema to fix a semantic error, or a validation pass to fix a syntax error, has misdiagnosed the failure.
The second half of the chapter is about volume. Once extraction works reliably on one invoice, you have a hundred thousand of them in a document store and a nightly job that must process all of them without blocking anyone — not "how do I get the model to answer correctly" but "how do I pay less for the same answers, and what do I do when a fraction fail." The Message Batches API answers that, with constraints that are easy to miss until a synchronous, tool-calling workflow gets shoehorned into it and quietly breaks.
You already know, from earlier chapters, that a tool_use block carries a name and an input object validated against the tool's input_schema. Structured output extraction reuses that exact mechanism for a purpose that has nothing to do with taking an action in the world. The "tool" does not do anything; extract_invoice never touches a database or calls an API. It exists purely so its input_schema becomes the contract the model is constrained to fill in, and so reading tool_use.input gets you a parsed object rather than a string you still have to parse.
This is the most reliable way to get schema-compliant output from Claude, and the reason is structural rather than incidental. Asking for JSON in prose and parsing the response text leaves you validating a string after the fact — if the model drops a closing brace, you find out during json.loads, several steps from where the problem occurred. Tool use moves the constraint earlier: the model fills in a schema given as part of the tool definition, and input arrives already structured. There is no string to parse and therefore no JSON syntax error to catch. This doesn't make Claude infallible — it makes malformed JSON structurally impossible rather than merely less likely.
Python
extract_invoice = {
"name": "extract_invoice",
"description": (
"Extract structured data from an invoice document: vendor identity, "
"line items, tax, and totals. Use this on any invoice, receipt, or "
"billing statement provided as text or an attached document. Do not "
"use this on purchase orders or shipping manifests, which have no "
"total to reconcile."
),
"input_schema": {
"type": "object",
"properties": {
"vendor_name": {"type": "string"},
"invoice_number": {"type": ["string", "null"]},
"invoice_date": {
"type": ["string", "null"],
"description": "ISO 8601 date, e.g. 2026-03-14. Null if unreadable.",
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"unit_price": {"type": "number"},
"line_total": {"type": "number"},
},
"required": ["description", "quantity", "unit_price", "line_total"],
},
},
"tax_amount": {"type": ["number", "null"]},
"stated_total": {
"type": "number",
"description": "The total as printed on the invoice.",
},
"calculated_total": {
"type": "number",
"description": (
"Sum of line_total across line_items, plus tax_amount. "
"Compute this yourself; do not copy stated_total."
),
},
"conflict_detected": {
"type": "boolean",
"description": "True if calculated_total and stated_total differ by more than $0.01.",
},
"category": {
"type": "string",
"enum": ["goods", "services", "subscription", "other"],
},
"category_detail": {
"type": ["string", "null"],
"description": "Required when category is 'other'; free-text description.",
},
},
"required": [
"vendor_name", "line_items", "stated_total",
"calculated_total", "conflict_detected", "category",
],
},
}
TypeScript
const extractInvoice: Anthropic.Tool = {
name: "extract_invoice",
description: [
"Extract structured data from an invoice document: vendor identity, line",
"items, tax, and totals. Use this on any invoice, receipt, or billing",
"statement provided as text or an attached document. Do not use this on",
"purchase orders or shipping manifests, which have no total to reconcile.",
].join("\n"),
input_schema: {
type: "object",
properties: {
vendor_name: { type: "string" },
invoice_number: { type: ["string", "null"] },
invoice_date: {
type: ["string", "null"],
description: "ISO 8601 date, e.g. 2026-03-14. Null if unreadable.",
},
line_items: {
type: "array",
items: {
type: "object",
properties: {
description: { type: "string" },
quantity: { type: "number" },
unit_price: { type: "number" },
line_total: { type: "number" },
},
required: ["description", "quantity", "unit_price", "line_total"],
},
},
tax_amount: { type: ["number", "null"] },
stated_total: {
type: "number",
description: "The total as printed on the invoice.",
},
calculated_total: {
type: "number",
description:
"Sum of line_total across line_items, plus tax_amount. Compute this yourself; do not copy stated_total.",
},
conflict_detected: {
type: "boolean",
description: "True if calculated_total and stated_total differ by more than $0.01.",
},
category: {
type: "string",
enum: ["goods", "services", "subscription", "other"],
},
category_detail: {
type: ["string", "null"],
description: "Required when category is 'other'; free-text description.",
},
},
required: [
"vendor_name", "line_items", "stated_total",
"calculated_total", "conflict_detected", "category",
],
},
};
Reading the result uses no new mechanism, just a new purpose for one you already know.
Python
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=[extract_invoice],
tool_choice={"type": "tool", "name": "extract_invoice"},
messages=[{"role": "user", "content": invoice_text}],
)
invoice_block = next(b for b in response.content if b.type == "tool_use")
invoice_data = invoice_block.input # already a parsed dict, schema-shaped
TypeScript
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
tools: [extractInvoice],
tool_choice: { type: "tool", name: "extract_invoice" },
messages: [{ role: "user", content: invoiceText }],
});
const invoiceBlock = response.content.find(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
);
const invoiceData = invoiceBlock!.input; // already parsed, schema-shaped
Everything above builds structured output out of a tool that never actually runs — extract_invoice exists purely so its input_schema becomes a contract. That's the right call when the chapter was written, but the API now offers two more direct mechanisms worth knowing separately, because the exam can ask you to pick between them rather than assume tool use is the only route.
output_config.format with {"type": "json_schema", "schema": {...}} is a dedicated structured-output mode that has nothing to do with tools. You pass a JSON Schema, Claude uses constrained decoding to guarantee the response matches it, and the JSON lands in a plain text content block — there's no tool_use block to fish out, no fake tool to invent. Where the invoice pipeline reused tool use because forcing a tool call was already the mechanism at hand, a task with no natural "tool" — classify this support ticket into a fixed set of fields, say — is more directly expressed as output_config.format than as a tool nobody calls.
Python
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": invoice_text}],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"vendor_name": {"type": "string"},
"stated_total": {"type": "number"},
},
"required": ["vendor_name", "stated_total"],
"additionalProperties": False,
},
}
},
)
invoice_data = json.loads(response.content[0].text)
TypeScript
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: invoiceText }],
output_config: {
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
vendor_name: { type: "string" },
stated_total: { type: "number" },
},
required: ["vendor_name", "stated_total"],
additionalProperties: false,
},
},
},
});
const invoiceData = JSON.parse(response.content[0].text);
strict: true addresses a narrower gap in the tool-use pattern itself. Forcing a tool with tool_choice guarantees that a call happens, but without strict: true the input to that call is validated the ordinary way — reliable in practice, not structurally guaranteed. Adding "strict": true alongside input_schema on the tool definition turns that guarantee into the same kind of constrained decoding output_config.format uses, applied to the tool's arguments instead of a bare text response. It costs nothing conceptually — same tool, one more field — so a strict extraction tool is what extract_invoice should really carry now.
The two are independent and combine cleanly: output_config.format shapes what Claude says, strict: true shapes how Claude calls a tool, and a single request can use both — a schema-constrained final answer alongside a strictly-validated tool call earlier in the same turn, as in a lookup-then-report agentic workflow.
Not every schema construct survives either mechanism. Both are constrained to a subset of JSON Schema: no numeric bounds (minimum, maximum, multipleOf), no string length bounds, additionalProperties must be false rather than omitted or true, no recursive schemas, and only a limited regex dialect for pattern. The ["string", "null"] nullable-field pattern and the enum-plus-detail escape hatch from the schema design section above both still work — they use only type, enum, and description, none of which fall in the unsupported list. A schema that leans on minLength or a numeric range to do validation work needs that check moved into your own code, because the constrained decoder simply won't enforce it.
One cost worth planning around: the first call using a given schema pays extra latency while its grammar compiles from that schema, and Anthropic caches the compiled grammar for 24 hours from last use. Changing output_config.format — including the schema inside it — invalidates that cache and, separately, invalidates prompt caching for the request. A pipeline that mutates its schema per call (interpolating an enum's allowed values per request, say) pays the compilation cost every time; keeping the schema fixed and pushing per-request variation into the prompt text instead keeps the grammar cache warm.
You saw the three tool_choice values in earlier chapters as a way of shaping agent behavior. In an extraction pipeline they answer a narrower question: what happens if Claude decides a text answer is more natural than a structured one? {"type": "auto"} leaves that door open — Claude may return prose instead of calling any tool, exactly the failure mode you're eliminating when the point of the request is a structured record. For extraction, "auto" is close to always the wrong setting.
The right choice depends on how many extraction schemas run side by side and whether you know which applies in advance. If your pipeline ingests a mixed stream — invoices, purchase orders, shipping manifests — and you don't know which schema fits an unclassified document, tool_choice: "any" is right: Claude must call some tool, but is free to decide which one fits, guaranteeing a structured result while classifying implicitly.
The forced form, {"type": "tool", "name": "extract_invoice"}, is for the opposite situation: the document type is already known, and this extraction must complete before anything else — a call to enrich_vendor_record that needs vendor_name, say, or a flag_for_review step that only makes sense once conflict_detected is known. Force extract_invoice on the first request, then bring in enrichment tools with tool_choice back at "auto" for the turns after.
| Setting |
Guarantees |
Invoice-pipeline use |
{"type": "auto"} |
Nothing — Claude may answer in text |
Wrong default for extraction |
{"type": "any"} |
Some tool is called |
Mixed document stream, schema unknown until Claude looks |
{"type": "tool", "name": "extract_invoice"} |
That specific tool is called |
Document type known, or extraction must precede enrichment |
Schema design decisions that matter more than they look
A schema is a contract, but a badly shaped one creates the very failures it was meant to prevent. The single most consequential decision is which fields are required. It's tempting to mark everything required, since everything is, in principle, wanted information — but a required field on a document that lacks it is an invitation to fabricate. If invoice_number is required and a receipt has no visible one, a model under schema pressure to produce something will occasionally invent a plausible value rather than admit absence. A nullable field — "type": ["string", "null"] above — gives an honest way to report absence, and an honest null is worth more downstream than a confident fabrication, since a null triggers your validation logic while a fabrication sails through silently.
The same instinct extends to categorical fields. A plain enum forces every document into a fixed bucket, and real invoices don't respect your taxonomy. The pattern worth memorizing is an enum with an escape hatch: category takes "goods", "services", "subscription", or "other", paired with a category_detail string required precisely when "other" is chosen — useful for aggregation on the common cases while staying honest on the ones you didn't anticipate. Where an enum represents a judgment call rather than a category — paid, unpaid, disputed — a value like "unclear" gives a legitimate answer for genuine ambiguity instead of a forced guess between two wrong options.
Finally, a strict schema handles shape, not formatting inconsistency. A vendor might write dates as 03/14/2026, 14 Mar 2026, or 2026-03-14, and a schema typing the field as a string accepts all three without normalizing any. Put the normalization rule in the prompt or tool description, not the schema — "return dates as ISO 8601; if ambiguous, infer the most likely reading" does work no pattern constraint can, since the schema confirms a string looks like a date but can't tell the model how to convert 14/03/26 into one.
Syntax versus semantics — the load-bearing distinction
Everything above solves one problem completely: getting output that parses. It does nothing for whether the values inside that output are correct. The natural move after getting reliable schema compliance is to assume the extraction is now trustworthy — and that assumption is the trap.
Go back to the invoice with three line items summing to $412.50 and a total field reading $391.00. Every field is present, correctly typed, schema-valid. Nothing about strict tool use catches this — it was never checking whether the numbers agree with each other, only whether the response has the shape you declared. A model can be perfectly schema-compliant and perfectly wrong; the schema is a claim about structure, and structure says nothing about arithmetic, provenance, or internal consistency.
The exam's own worked example fixes this by making the model do the arithmetic itself, in the open, as a field you can check rather than an internal step you can't see. That's why the schema above has both stated_total — the number printed on the document — and calculated_total, described as "sum of line_total across line_items, plus tax_amount. Compute this yourself; do not copy stated_total." Asking for both forces the model to add up the line items rather than transcribe a headline number, and gives you a field to diff in code: if the two differ by more than a cent, a human should look before the number reaches accounts payable.
That diff is what conflict_detected encodes as a first-class field, a boolean the model itself must set, rather than something your code rederives every time. The pattern generalizes past totals: whenever a source contains internally inconsistent information — two dates for the same event, a name spelled two ways — a conflict_detected boolean, or a comparable field pair, turns "the model might notice this" into "the pipeline will notice this."
The rule for triage, tested directly: malformed JSON — unparseable text, a missing brace, output that never assembles into valid JSON at all — is fixed by schema and tool use, a structural failure this mechanism eliminates. A required field silently omitted is a schema violation tool use will catch and refuse, but why the model omitted it, and whether asking again helps, is a separate question the next section takes up. Well-formed, schema-valid JSON that is simply incorrect — numbers that don't reconcile, a value in the wrong field, a fabrication where a null belonged — is fixed by validation logic, not a stricter schema, because no schema knows that $412.50 and $391.00 should have been the same number.
Validation, retry, and feedback loops
Once you're checking conflict_detected and comparing totals in code, the natural next step is retrying failed extractions — but retry only works on some failures, and knowing which is the actual skill being tested here.
Retry works when the problem is structural or a matter of reading the source more carefully: a schema violation from an omitted field, a miscalculated total from a dropped line item. The needed information was present in the document the whole time; the first pass just didn't extract it correctly, and a nudge toward the specific mistake has a real chance of succeeding.
Retry does not work when the missing piece isn't in the document you gave the model. If an invoice references a purchase order in a system you never provided, no amount of re-prompting produces it — the model has no more access on attempt five than attempt one, and an unguarded retry loop burns requests indefinitely on an unfixable case. Classify why the first attempt failed before retrying: structural or arithmetic failures are retriable; absence-of-information failures need a human or a step that fetches the missing document.
When you do retry, send back more than the original prompt: the document, the failed extraction, and the specific validation error, so the model corrects a named mistake rather than guessing at what displeased you.
Python
def retry_extraction(client, document_text, failed_extraction, validation_error):
retry_prompt = (
f"The previous extraction of this invoice failed validation:\n\n"
f"{validation_error}\n\n"
f"Previous extraction:\n{json.dumps(failed_extraction, indent=2)}\n\n"
f"Original invoice:\n{document_text}\n\n"
f"Re-extract, correcting the specific issue above. Recompute "
f"calculated_total from line_items rather than reusing the prior value."
)
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=[extract_invoice],
tool_choice={"type": "tool", "name": "extract_invoice"},
messages=[{"role": "user", "content": retry_prompt}],
)
return next(b for b in response.content if b.type == "tool_use").input
TypeScript
async function retryExtraction(
client: Anthropic,
documentText: string,
failedExtraction: unknown,
validationError: string,
): Promise<unknown> {
const retryPrompt = [
`The previous extraction of this invoice failed validation:`,
``,
validationError,
``,
`Previous extraction:`,
JSON.stringify(failedExtraction, null, 2),
``,
`Original invoice:`,
documentText,
``,
`Re-extract, correcting the specific issue above. Recompute`,
`calculated_total from line_items rather than reusing the prior value.`,
].join("\n");
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
tools: [extractInvoice],
tool_choice: { type: "tool", name: "extract_invoice" },
messages: [{ role: "user", content: retryPrompt }],
});
const block = response.content.find(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
);
return block!.input;
}
A bare retry — resending the same prompt unchanged — usually reproduces the same output, since nothing about the request changed to point the model at a different answer. The validation error is what does the work.
The same field-design habit that produced conflict_detected generalizes further. If you run an agent that scans code for findings — security issues, style violations — add a detected_pattern field naming the specific construct that triggered each finding. On its own it does nothing for that finding, but once developers start dismissing findings as false positives, grouping dismissals by detected_pattern tells you exactly which construct over-triggers — turning a vague "too many false positives" into a concrete list to tune. Same principle as calculated_total alongside stated_total: make the model's internal reasoning a field you can query, not a step you have to trust blindly.
Designing for batch volume
Everything so far assumes one invoice arrives, is extracted, and the caller waits. A nightly reconciliation job processing forty thousand invoices is a different problem, with constraints from a different part of the API surface: the Message Batches API.
The trade is easy to underweight in practice. A batch request costs half of what the same request costs synchronously — a flat 50% discount, not volume-tiered — in exchange for giving up any latency guarantee. Anthropic processes a batch within a window of up to twenty-four hours, with no SLA promising anything faster; a batch might come back in twenty minutes or use most of the window, and the design has to be correct under the slow case. That combination — half price, unbounded-up-to-24h latency — is the judgment call task statement 4.5 tests: is this a workload where nobody is refreshing a page waiting for the answer.
An overnight technical-debt report, a weekly compliance audit, nightly test-case generation, and the invoice job all fit, because their consumer checks a dashboard the next morning rather than being blocked in real time. A pre-merge check does not fit: a developer watching a pull request cannot be told their merge might unblock in twenty minutes or might take until tomorrow, regardless of the savings. Batch what its consumer can wait for; keep synchronous what a person or process is blocked on right now.
The SLA arithmetic
The exam tests this with a specific calculation, worth working through once by hand. Suppose you've committed to a 30-hour SLA on invoice reconciliation — intake to validated record — and want the batch discount, submitting on a fixed schedule rather than continuously.
A batch can take up to 24 hours, with no tighter guarantee. An invoice landing right after a submission cutoff waits for the next window before it's even included, and only then does the 24-hour clock start — so worst-case latency is the wait before inclusion plus up to 24 hours of processing. For the SLA to hold regardless of arrival time, that sum must stay under 30 hours, leaving 6 hours of budget for the wait before pickup. Submitting every 6 hours puts the worst case at exactly 30 hours — uncomfortably tight, since real processing occasionally runs near the top of the window with no margin left. Submitting every 4 hours totals 28 hours worst case, a 2-hour buffer inside the commitment. That 4-hour cadence, not the 6-hour one that exactly meets the ceiling, is the answer to default to whenever the exam gives a hard SLA and asks for a submission interval: back out the margin, don't spend it all.
What a batch request looks like, and what does not fit inside one
Mechanically, a batch is a list of otherwise-ordinary Messages API requests, each tagged with a custom_id you choose. Submit the list once, poll for completion, and read results back — they can arrive in any order, which is exactly why every request carries a custom_id: never rely on position matching between submission and results.
Python
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request
batch_requests = [
Request(
custom_id=f"invoice-{invoice['id']}",
params=MessageCreateParamsNonStreaming(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=[extract_invoice],
tool_choice={"type": "tool", "name": "extract_invoice"},
messages=[{"role": "user", "content": invoice["text"]}],
),
)
for invoice in todays_invoices
]
batch = client.messages.batches.create(requests=batch_requests)
# Later, after polling batch.processing_status until "ended":
for result in client.messages.batches.results(batch.id):
if result.result.type == "succeeded":
block = next(b for b in result.result.message.content if b.type == "tool_use")
store_invoice(custom_id=result.custom_id, data=block.input)
else:
queue_for_resubmission(custom_id=result.custom_id, failure=result.result)
TypeScript
const batchRequests = todaysInvoices.map((invoice) => ({
custom_id: `invoice-${invoice.id}`,
params: {
model: "claude-sonnet-4-5",
max_tokens: 1024,
tools: [extractInvoice],
tool_choice: { type: "tool" as const, name: "extract_invoice" },
messages: [{ role: "user" as const, content: invoice.text }],
},
}));
const batch = await client.messages.batches.create({ requests: batchRequests });
// Later, after polling batch.processing_status until "ended":
for await (const result of client.messages.batches.results(batch.id)) {
if (result.result.type === "succeeded") {
const block = result.result.message.content.find(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
);
storeInvoice(result.custom_id, block!.input);
} else {
queueForResubmission(result.custom_id, result.result);
}
}
The constraint most worth internalizing, because it quietly breaks a design rather than throwing an error, is that a single batch request does not support multi-turn tool calling. Each entry is one Messages API call: it can return a tool_use block, but there is no mechanism for the batch runner to execute that tool, feed the result back, and let Claude decide what happens next — the request-response cycle behind the agentic loop has nowhere to run inside a batch. So the batch API fits single-shot work — one document in, one extraction or classification out — and not anything where the agent must call a tool, inspect the result, and decide a next step within that same unit of work. Invoice extraction fits perfectly, because extract_invoice is never actually executed as an action — the tool block is just the vehicle for structured output, with nothing waiting on it. An agent that needs to look up a vendor record mid-extraction, decide from that lookup whether to flag the invoice, and extract accordingly does not fit, since that loop requires the multi-turn execution a batch cannot do. That case runs synchronously, or gets decomposed into a batch-friendly first pass followed by a separate step once its outputs are known.
Handling failures without resubmitting everything
At batch volume, some fraction of requests come back errored, canceled, or expired, and the custom_id on every result makes a targeted fix possible: filter for the failed subset and resubmit only those, rather than paying twice for invoices that already succeeded. Blind resubmission of an entire batch is the mistake this design prevents.
Resubmission is also the moment to fix, not just retry. If a cluster of failures shares a cause — oversized scanned invoices exceeding the context limit — chunk those documents before resending, rather than resending the same oversized request unchanged.
The other lever worth using before committing to batch volume is prompt refinement on a small sample. A batch can take most of a day to come back, so discovering at scale that extract_invoice under-specifies date formatting costs you the day and the savings on extractions you now redo. Run fifty or a hundred invoices synchronously first, iterate until first-pass success is high, and only then submit the full volume as a batch.
What the exam tests
Task statement 4.3 is tested mostly through recognition: given a downstream system that needs guaranteed-parseable output, the correct mechanism is tool_use with a JSON schema, eliminating syntax errors as a category rather than reducing their frequency. Expect a companion question distinguishing "auto", "any", and a forced named tool by which fits when the document type is unknown, already known, or one extraction must precede enrichment. Task statement 4.4's central judgment call is the syntax-versus-semantics split argued above: a malformed response is a schema problem, a well-formed but internally inconsistent one is a validation problem, and the exam's worked example is the calculated_total-versus-stated_total pattern, which you should be able to reproduce from memory. A second recurring 4.4 shape asks you to classify whether a failure is retriable — format and arithmetic errors are, information absent from the source is not — and to route the latter to a human. Task statement 4.5 is tested with concrete arithmetic: a hard SLA and a 24-hour batch window, asking for a submission cadence solved by subtracting the window from the SLA and choosing an interval comfortably inside the remainder, not exactly at its boundary. The exam also tests whether you recognize that a scenario requiring the agent to call a tool, see its result, and decide what happens next cannot run inside a single batch request, and that resubmission after partial failure should be scoped to the failed custom_ids, not the whole batch.
Exercises
- Build
extract_invoice as specified above, run it against five invoices with intentionally inconsistent totals, and confirm conflict_detected flags every one. Then corrupt the JSON your test harness sends back — as if it were a raw-text extraction rather than tool use — and observe that tool_use makes that failure mode disappear.
- Write a retry harness that resends a failed extraction alongside the original document and the specific validation error. Test it against a genuine arithmetic slip (fixable) and an invoice missing a field because the document doesn't contain that information (not fixable). Confirm the retry succeeds on the first and cannot succeed on the second, no matter how many attempts.
- Work through the SLA arithmetic with a 20-hour SLA against the same 24-hour batch window. Determine whether any fixed submission cadence can satisfy it, and explain why the answer changes once the SLA is shorter than the batch window's maximum.
- Submit a small batch of ten invoices, including one deliberately malformed document that triggers a request-level failure. Poll for completion, iterate over results by
custom_id, and write resubmission logic that isolates and reprocesses only the failed entry.
Chapter 14 — Context engineering
You have felt this chapter's entire subject matter without necessarily having a name for it. A Claude Code session runs for two hours, you have read forty files and run a dozen greps, and somewhere past the ninety-minute mark the answers start softening. Claude tells you a change "typically follows the pattern used elsewhere in the codebase" instead of naming the class it found earlier. You run /compact because the context indicator is climbing toward red, and afterward the session feels lighter but also a little forgetful — a decision you made twenty minutes ago has to be repeated. Both experiences are the same underlying fact wearing different clothes: the messages array is finite, everything the agent knows lives inside it, and neither growth nor compression is free.
Chapter 2 established that history is the agent's memory and that /compact is a rewrite of that array. This chapter is about what you do with that fact deliberately, rather than letting it happen to you. It splits into two problems that look similar but are not. The first is keeping critical facts alive inside a single long-running conversation — a support case that runs two hours and touches three issues, where a dollar amount or a promised delivery date must survive to the end exactly as stated at the start. The second is coordinating context across an exploration task big enough that no single conversation can hold it — a codebase investigation spanning phases and subagents, where the risk is not one number getting fuzzy but the whole investigation losing track of what it already found. Task statement 5.1 covers the first. Task statement 5.4 covers the second. Treat them as one and you will misapply the fix — a case-facts block does not help a codebase crawl, and a manifest does not help a refund amount survive summarization.
5.1 — Preserving facts inside one long conversation
Start with the failure mode you already know experientially: progressive summarization. Every time context fills up, something has to give, and the natural instinct — Claude's, and an engineer's, if you write your own compaction — is to replace verbose turns with a shorter paraphrase. Paraphrase is the wrong operation to run on certain content, because it is very good at preserving the gist of a passage and very bad at preserving the specific numbers inside it. "Customer disputed a charge and we discussed a refund" is a faithful summary of a turn that said "customer disputed a $284.17 charge on order SO-88213, was promised a refund by the 15th, and this is their second contact about it." The gist survived; the amount, the order id, and the date did not. This is not a bug in any particular summarizer; it is what summarization is for. Compression trades specifics for brevity, and specifics are precisely what a support case, a compliance record, or a financial reconciliation cannot afford to trade away.
A second, independent failure compounds it: the lost-in-the-middle effect. Long-context models are reliably good at attending to the beginning and end of a long input and measurably worse at the middle — a structural property of attention over long sequences, not a defect you patch by upgrading something. A fact stated once mid-conversation and never repeated is at meaningfully higher risk of being ignored later than a fact stated at the start or restated near the end. In a support session this shows up as an agent that correctly captured the customer's original complaint, then several tool calls and a side-issue later, answers as though that complaint never happened.
The third failure is the one the exam frames concretely: tool results accumulate in context and cost tokens in proportion to their size, not their relevance. lookup_order in a realistic order system does not return five fields. It returns forty or more — SKUs, warehouse routing codes, tax jurisdiction flags, fulfillment timestamps, loyalty point deltas, three address blocks — because the underlying record is the one fulfillment and billing systems actually use, and nobody trimmed it for conversational consumption. If the agent is handling a return, five of those forty fields are relevant: order id, item, amount, ship date, and status. The other thirty-five sit in context anyway, because nothing removes a tool result once appended. Three such lookups and the case-relevant signal is buried under a hundred-plus fields of warehouse metadata the model still has to process every turn.
Underneath all three sits a fact the exam wants you to hold as a constraint, not a workaround: you must pass the complete conversation history on every request, because the API is stateless and there is no other channel through which the model learns what has already happened. That rules out the tempting shortcut of dropping earlier turns to save tokens — do that and the model does not "still generally understand," it structurally cannot know what it does not have.
Worth knowing as a small, concrete fact rather than a workaround: on Claude 4.5+ models, running out of room mid-generation surfaces as stop_reason: "model_context_window_exceeded" rather than a hard validation error. Older models simply reject the request when input plus max_tokens exceeds the window; 4.5+ models can start generating and then report this stop reason when they run out of space before finishing, which a loop needs to handle as its own case rather than lumping in with an ordinary max_tokens truncation — the two look similar in effect but mean different things about why the response is incomplete.
The case-facts block
The fix is architectural, not clever. Given that summarization corrupts numbers and dates, keep them in a form that is never summarized: extract the transactional facts of the case — amounts, dates, order numbers, statuses, anything the customer explicitly stated as an expectation — into a small, structured block you assemble yourself and include in full in every request, outside whatever the compaction process is allowed to touch. This is the same principle as CLAUDE.md sitting in the system field rather than in messages — content that shapes every turn without being subject to the churn the conversation itself goes through. The case-facts block is your equivalent, built per-conversation instead of per-project.
Python
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class CaseFacts:
"""Durable, non-summarizable facts for one support case."""
customer_id: Optional[str] = None
tier: Optional[str] = None
issues: list[dict] = field(default_factory=list)
# each issue: {order_id, amount_cents, status, promised_date, stated_by_customer}
def render(self) -> str:
lines = [f"CASE FACTS (verified, do not paraphrase):",
f"customer_id={self.customer_id} tier={self.tier}"]
for i, issue in enumerate(self.issues, start=1):
lines.append(
f" issue {i}: order={issue['order_id']} "
f"amount=${issue['amount_cents']/100:.2f} "
f"status={issue['status']} "
f"promised_by={issue.get('promised_date', 'n/a')}"
)
return "\n".join(lines)
def build_request(case_facts: CaseFacts, messages: list, user_turn: str) -> dict:
# The facts block rides in `system`, alongside the persistent instructions,
# so /compact and any summarization pass over `messages` never touch it.
return {
"model": "claude-sonnet-4-5",
"system": f"{SUPPORT_SYSTEM_PROMPT}\n\n{case_facts.render()}",
"messages": messages + [{"role": "user", "content": user_turn}],
}
TypeScript
interface Issue {
orderId: string;
amountCents: number;
status: string;
promisedDate?: string;
}
interface CaseFacts {
customerId?: string;
tier?: string;
issues: Issue[];
}
function renderCaseFacts(facts: CaseFacts): string {
const lines = [
"CASE FACTS (verified, do not paraphrase):",
`customer_id=${facts.customerId} tier=${facts.tier}`,
];
facts.issues.forEach((issue, i) => {
lines.push(
` issue ${i + 1}: order=${issue.orderId} ` +
`amount=$${(issue.amountCents / 100).toFixed(2)} ` +
`status=${issue.status} promised_by=${issue.promisedDate ?? "n/a"}`,
);
});
return lines.join("\n");
}
function buildRequest(facts: CaseFacts, messages: Anthropic.MessageParam[], userTurn: string) {
return {
model: "claude-sonnet-4-5",
system: `${SUPPORT_SYSTEM_PROMPT}\n\n${renderCaseFacts(facts)}`,
messages: [...messages, { role: "user" as const, content: userTurn }],
};
}
Two design choices here are load-bearing rather than stylistic. First, the case-facts block lives outside messages and is rebuilt fresh on every request from your own application state, not derived from the conversation by asking the model to summarize itself — the whole point is that it is never subject to the same lossy compression that /compact or any other summarization step applies to the rest of the history. Second, it is a separate context layer for multi-issue sessions specifically because a customer who opens with a billing dispute and adds a shipping complaint twenty minutes later needs both issues tracked with their own order id, amount, and status, not folded into one paragraph where the second issue's amount quietly borrows the first issue's details. Extracting structured issue data into its own layer, rather than letting each issue live only as prose in the transcript, is what lets the agent answer "what's the status of the shipping issue" without wading back through the billing discussion to find it.
The case-facts block protects facts once extracted. It does nothing about the forty-plus-field problem, because that happens before extraction — the moment lookup_order returns, its full payload lands in a tool_result block and stays in context for the rest of the conversation. The fix has to happen at the same moment: trim the tool's output before it is appended, not after.
This means the tool boundary is a design decision, not just a data-fetching one. Where lookup_order in your backend returns the full internal record, the function your agent actually calls should project that record down to what a return-eligibility conversation needs — order id, item, amount, ship date, status — and drop warehouse routing, tax codes, and loyalty deltas entirely. Chapter 3's argument that tool descriptions shape what the model calls extends here to shaping what the model has to read once it has called: a five-field tool is not a stylistic courtesy, it prevents thirty-five irrelevant fields from occupying context for every subsequent turn of a possibly-long case.
Python
RETURN_RELEVANT_FIELDS = {"order_id", "item", "amount_cents", "ship_date", "status"}
def lookup_order(order_id: str) -> dict:
full_record = fulfillment_backend.get_order(order_id) # 40+ fields
return {k: v for k, v in full_record.items() if k in RETURN_RELEVANT_FIELDS}
TypeScript
const RETURN_RELEVANT_FIELDS = new Set([
"orderId", "item", "amountCents", "shipDate", "status",
]);
async function lookupOrder(orderId: string): Promise<Record<string, unknown>> {
const fullRecord = await fulfillmentBackend.getOrder(orderId); // 40+ fields
return Object.fromEntries(
Object.entries(fullRecord).filter(([key]) => RETURN_RELEVANT_FIELDS.has(key)),
);
}
Trimming at the source beats trimming later for the same reason prevention beats cleanup everywhere else: once irrelevant fields have already been appended into several tool_result blocks, no later step short of a destructive rewrite removes them — and a destructive rewrite is exactly the summarization operation that damages numbers and dates. Prune before it enters, and there is nothing to prune later.
Trimming at the source requires owning the tool, which is not always true — a third-party MCP server's tool result is not yours to project down before it lands in context. For that case, the API now offers a server-side complement: the context editing API (beta header context-management-2025-06-27), configured as context_management: {"edits": [{"type": "clear_tool_uses_20250919", ...}]}. It clears old tool results out of context according to rules you configure — a trigger for when clearing kicks in, keep for how many recent results to leave untouched, clear_at_least for a minimum amount to reclaim, exclude_tools to protect specific tools from ever being cleared, and clear_tool_inputs to also strip the original call arguments. A companion edit type, clear_thinking_20251015, does the same for accumulated thinking blocks. This is a fallback, not a first choice: trimming at the source is still the better fix whenever you control the tool, because it prevents the bloat from ever entering context rather than reclaiming it after the fact — but for a tool result you don't control, context editing is the lever you actually have.
Position matters even after you have the facts right
The lost-in-the-middle effect means even a well-curated set of findings can get shortchanged if it is buried mid-input. When you assemble a prompt bundling several tool results, a research summary, and a question — the shape you get when synthesizing findings from multiple lookups before asking Claude to decide on a resolution — put the key findings summary first, not buried at position four of seven. Follow it with supporting detail under explicit section headers, so that even if attention to the middle is weaker, the headers give the model an anchor to navigate back to rather than requiring it to have retained everything in sequence.
KEY FINDINGS
- Order SO-88213: shipped 19 days ago, within 30-day return window.
- Customer has one prior return on this account (SO-77410, approved).
- Policy: standard return, no manager approval required.
## Order Detail
...
## Account History
...
## Applicable Policy Section
...
This restructures information you already have; it costs nothing except discipline in how you assemble a prompt, and it matters most when it looks least necessary — when results are individually short and you are tempted to just concatenate them in call order.
Chapter 5 covers subagent isolation as an architectural feature: a subagent's internal exploration never touches the coordinator's context, only its final report does. That isolation benefits token budget but becomes a liability for accuracy if the report is verbose prose with the reasoning chain still attached and no metadata about where any of it came from. A downstream agent synthesizing several such reports has no way to tell which claim came from which source, when the data was current, or how confident the subagent actually was — and has to spend context re-deriving structure from prose instead of consuming structure that was already there.
The fix is a contract on the subagent's output shape, not a hope that it writes clearly. Require dates, source locations, and methodological context in every structured result a subagent returns, and require key facts, citations, and relevance scores in place of verbose content and reasoning chains whenever the consumer downstream has a limited context budget — which, in a multi-agent pipeline, is the normal case.
Python
@dataclass
class SubagentFinding:
claim: str
source: str # file path, ticket id, or document name
as_of_date: str # when this fact was true / last verified
relevance: float # 0-1, how directly this bears on the question asked
# deliberately no "reasoning" field — the coordinator needs the conclusion,
# not the chain that produced it
TypeScript
interface SubagentFinding {
claim: string;
source: string; // file path, ticket id, or document name
asOfDate: string; // when this fact was true / last verified
relevance: number; // 0-1, how directly this bears on the question asked
// deliberately no `reasoning` field — the coordinator needs the conclusion,
// not the chain that produced it
}
The reasoning chain that got a subagent to a conclusion was necessary for the subagent to reach it and is close to worthless for the coordinator to read — it costs tokens in proportion to how thorough the exploration was, exactly backward from what a limited budget can afford. Insist on the structured shape at the interface, and a five-paragraph investigation compresses to one line the coordinator can place next to four other subagents' findings and reconcile.
5.4 — Context across a large codebase exploration
Everything above concerns one conversation getting long. A large codebase exploration is a different shape of problem: not one conversation stretching thin, but a task big enough that a single agent's context window was never going to hold it, so the work has to be split across phases and subagents from the start. The failure mode is correspondingly different, and you have almost certainly seen it: not a number going vague, but the agent's answers turning generic. Ask it to explain how retries are implemented an hour into a deep exploration and instead of naming RetryableOrderProcessor at services/orders/retry.py:142, which it found and read forty tool calls ago, it says something like "this typically follows a standard retry pattern with exponential backoff." That sentence is the named symptom — the model is no longer citing the specific class it discovered, it is reconstructing a plausible generic answer because the specific one has degraded out of effective reach. The fact may still technically be present in context; in a long, noisy, tool-call-heavy session, the signal-to-noise ratio around it drops until the model falls back to what it knows about codebases in general.
Subagent delegation for verbose exploration
The first countermeasure is one you already understand as an architectural pattern: keep exploration and coordination in separate contexts. A question like "find every file that touches the refund flow" or "trace what calls process_refund and what it calls in turn" involves reading a large number of files, most of which are dead ends, and every one of those reads is a tool result that would otherwise sit in the main conversation forever. Spawn a subagent to answer exactly that question. Its context fills with the search noise — the false leads, the files it opened and discarded — and none of that noise ever reaches the coordinator. What comes back is a short, structured list of the files and call sites that matter, ideally in the finding-metadata shape from the 5.1 discussion above, because the same argument about structured facts over reasoning chains applies here.
The coordinator's job through all of this is to stay at the altitude Chapter 5 assigns it: decide what needs investigating, dispatch a subagent per question, and reconcile what comes back, never doing the file-by-file digging itself. A coordinator that reads files directly "just this once" while waiting on a subagent is accumulating exactly the verbose exploration output the delegation was meant to keep out of its own context.
Scratchpad files
Delegation solves the coordinator's context. It does not solve degradation inside a single agent's own long-running exploration, and a subagent doing a deep trace can itself run long enough to degrade. The technique here is a scratchpad file: a plain file on disk, written to as the agent works, recording key findings in a form the agent re-reads rather than re-derives. Instead of relying on its own attention to a fact stated fifty tool calls back, the agent writes "refund cancellation path goes through RefundCanceller.revert at services/orders/cancel.py:88, called from the webhook handler at services/webhooks/stripe.py:210" to a scratchpad the moment it finds it, and when a later question needs that fact, it reads the scratchpad rather than trusting its own memory of the conversation so far.
# scratchpad: refund-flow-trace.md
## Confirmed call chain
- entry: `services/webhooks/stripe.py:210` (webhook handler)
- calls: `RefundCanceller.revert` at `services/orders/cancel.py:88`
- calls: `LedgerAdjustment.apply` at `services/ledger/adjust.py:41`
## Open questions
- Does `LedgerAdjustment.apply` retry on ledger-service timeout? Not yet checked.
## Files ruled out
- `services/orders/refund.py` — handles issuance, not cancellation. Not relevant here.
This is not a log of everything the agent did; a transcript of every tool call already exists in history and re-reading it would not help. It is a deliberately curated, append-as-you-go record of conclusions, written so the exact class name and file path survive being asked about forty tool calls later, instead of being reconstructed as "the retry pattern typically used here." The scratchpad is the countermeasure to lost-in-the-middle applied to an entire exploration session rather than one prompt: instead of hoping the model attends correctly to a fact buried deep in its own history, you give it an external, re-readable, always-current place to look.
The pattern above is hand-rolled: you decide the file, you write the read/write calls, you manage the lifecycle. Anthropic now ships a supported version of the same idea as the memory tool ({"type": "memory_20250818", "name": "memory"}), a client-executed tool that gives Claude a persistent /memories directory surviving across sessions, with view, create, str_replace, insert, delete, and rename operations Claude calls directly rather than you scripting around a plain file. Anthropic's own documentation describes what it calls a multisession software development pattern built on it — a progress log plus a feature checklist, read back in on resume — which maps onto this chapter's scratchpad-and-manifest combination closely enough that the two are worth thinking of as the same architecture, one built by hand and one provided. Knowing why the scratchpad pattern works is still the point: the memory tool only makes sense to reach for once you understand what it's automating, and the DIY version remains the right call whenever you need a bespoke format or storage location the standard tool doesn't give you.
Phase summaries before the next round of subagents
A multi-phase exploration — say, first mapping every code path that touches refunds, then auditing each path for a specific bug pattern — should not let the second phase's subagents start from nothing and reconstruct the first phase's findings by re-exploring. Before spawning phase two's subagents, the coordinator should summarize what phase one concluded and inject that summary directly into each phase-two subagent's initial prompt. This is the deliberate, structural version of what a scratchpad does opportunistically: a subagent starts already knowing the shape of the terrain, and the tokens phase one spent mapping it are not spent twice.
Manifests for crash recovery
Long exploration sessions get interrupted — the process dies, the session is closed, or context fills up mid-investigation and needs a fresh start. Recovering gracefully is a design decision, not an accident of what survives. Design each agent — coordinator or subagent — to export its state to a known location as it works: what it has concluded, what remains open, references to the scratchpad files it has been writing. On resume, the coordinator's first act is to load a manifest — an index of what state exists and where — and inject the relevant pieces into each freshly-spawned agent's initial prompt, rather than starting the whole exploration over.
{
"exploration_id": "refund-flow-audit-2026-09-03",
"phase": 2,
"phase_1_summary": "scratchpad/refund-flow-trace.md",
"subagents_completed": ["trace-webhook-path", "trace-ledger-path"],
"subagents_pending": ["audit-retry-behavior", "audit-idempotency"],
"open_questions": [
"Does LedgerAdjustment.apply retry on ledger-service timeout?"
]
}
The manifest is what makes a crash cost minutes instead of hours. Without it, resuming an interrupted exploration means re-running phase one to reconstruct facts the coordinator already had.
/compact inside an exploration, used deliberately
You already know /compact as the thing you run when the context indicator gets uncomfortable. Inside a long exploration session it is a legitimate tool for exactly the situation this chapter has been describing: the window fills with accumulated noise from dozens of file reads and greps, most of them dead ends, and compacting reclaims room to keep going. The judgment call the exam is interested in is not whether to use it — you will, on any exploration long enough to matter — but when, relative to the other techniques here. Compacting is safest right after a phase summary has been written and a scratchpad is current, because the durable findings already live somewhere that survives compaction, and what gets compressed away is exactly the exploratory noise that was never going to be needed again. Compact before writing the summary, and you risk compressing away the specific fact the summary was supposed to capture in the first place.
/compact in Claude Code is the client-side, CLI-level version of this operation. The API has since grown a server-side equivalent — compaction as a configurable context_management edit (beta compact-2026-01-12, context_management: {"edits": [{"type": "compact_20260112", "instructions": "..."}]}) — and the detail worth knowing is the instructions field, which replaces the default summarization prompt entirely. Rather than accepting whatever generic compression Anthropic's default summarizer applies, you can write "preserve all dollar amounts, order numbers, and dates verbatim" directly into the compaction call itself. This does not replace the case-facts block or make the earlier discussion of why summarization corrupts numbers moot — it is a second, complementary fix operating at the compaction layer instead of the prompt-assembly layer, and it is only as good as the instructions you give it, which still requires knowing exactly what's at risk of being paraphrased away. pause_after_compaction is also worth knowing: it stops execution right after compacting so you can review what survived before the agent continues, rather than discovering after the fact that a needed fact didn't make it through.
What the exam tests
Task statement 5.1 is tested through support-shaped stems where a specific fact — a dollar amount, a promised date, an order number — goes vague after several turns, and the correct diagnosis is progressive summarization compressing history that should have had a persistent case-facts block instead; expect distractors proposing "summarize more carefully" or "ask the model to double-check the number," which miss that the fix is structural, not a prompting adjustment. A second shape names the forty-field tool-result problem directly and expects trimming at the source, before the result enters context, rather than after. A third turns on where the case-facts block lives: the guide's wording is that it is "included in each prompt, outside summarized history," so any answer that leaves the facts inside the history being summarized has missed the point, whatever else it gets right. Task statement 5.4 is tested through a described symptom — an agent referencing "typical patterns" instead of a specific class it found earlier — which the exam wants recognized as context degradation, not a reasoning failure fixable with a better prompt. Expect items asking you to choose spawning a subagent for an isolated question over letting the coordinator explore directly, and items testing whether a manifest belongs to crash recovery specifically, distinct from a scratchpad file, which belongs to counteracting degradation within an ongoing session. /compact appears as a legitimate answer in exploration-session items, but the judgment being probed is sequencing — compact after findings are externalized, not before.
Exercises
- In a long-running Claude Code session, ask a factual question about something discovered more than thirty tool calls ago — a specific function name, a file path, a config value. A hedged or generic answer reproduces this chapter's degradation symptom; note the exact phrasing, likely including a word like "typically."
- Design a case-facts block for a support conversation involving two disputed charges and one shipping complaint for the same customer. Decide which fields belong in the persistent block versus ordinary conversational history, and write out the block after all three issues are raised.
- Pick a tool that returns a large record — a database row, an API response — and write the trimmed projection an agent actually needs for one task. Count fields in the full record versus the trimmed one, and estimate the token difference across a twenty-turn conversation.
- Design the manifest schema for a three-phase codebase audit of your choosing (map dependencies, find dead code, assess test coverage). Specify what each phase writes to disk, what the coordinator reads on resume, and what gets injected into a phase-three subagent's prompt if the process crashed midway through phase two.
Chapter 15 — Escalation, review, and provenance
Every system you have designed across the last fourteen chapters eventually produces an answer, and this chapter is about the moment right before that: who checks the answer, when the system should refuse to give one on its own, and what happens to the trail of evidence behind it. These are three different problems with one shared temptation — let the model check its own work, let it decide for itself when it is out of its depth, let a summary carry the gist and drop the citations — and the exam wants you to know precisely why each shortcut fails.
The three problems also share a villain: the model's opinion of itself. A model that just generated code is a poor judge of its own bugs. A model uncertain about a case is a poor judge of whether that case needs a human. A raw, unexamined confidence score tells you almost nothing about whether the model is right. This chapter puts these together on purpose, because the exam tests the seams between them, and a plausible-sounding wrong answer usually hides in a seam.
By the end you should be able to design a review architecture that catches what self-review misses, write escalation criteria that hold up against a demanding customer and an ambiguous policy, build a human-review pipeline that measures its own blind spots rather than trusting its aggregate accuracy, and keep a multi-source research report honest about where its claims came from. The final section resolves what looks, from the sections before it, like a contradiction about confidence — the one to remember if you remember nothing else.
Review architecture: why a second instance beats a second look
You already know what a self-review instruction looks like from watching Claude Code work: "review the diff you just wrote before finishing." It sometimes catches something, but it is far less effective than it feels, and the reason is structural rather than a matter of the model trying harder. A model reviewing code it just generated in the same session retains the reasoning context from generation — the assumptions it made, the design choices it committed to. That retained context is precisely what makes self-review weak: the model is not reading the code fresh, it is re-reading its own explanation of it, and stays biased toward finding that explanation persuasive because it is still shaping everything downstream. It readily catches a typo; it rarely questions a decision it just made, because that means questioning the reasoning chain it is still holding onto.
Anchor this against something you already toggle routinely: extended thinking on a hard problem. More reasoning room before answering helps generation, but it does not fix the self-review weakness, because the bias is not "insufficient reasoning effort," it is "reasoning happened, and now it's anchoring the read." A model that thought longer before writing the code is still reluctant to tear that code apart minutes later in the same session, for the same reason a human engineer is a worse reviewer of their own pull request than a colleague who never saw it get written. Independent review instances, spun up without the generator's reasoning context, catch subtle issues that self-review and extended thinking both miss, because a fresh read is not anchored to the generator's justification for its own choices.
The practical consequence: instead of asking the same conversation to check itself, dispatch a second, independent Claude instance whose context contains only the artifact — the diff, the extraction, the plan — and none of the reasoning that produced it. That instance has no investment in the first one's choices.
Python
async def generate_and_review(task_description: str) -> dict:
generation = await claude.messages.create(
model="claude-sonnet-4-5",
messages=[{"role": "user", "content": task_description}],
)
code = extract_code(generation)
# A fresh instance: no system prompt overlap with generation, no shared
# conversation history, nothing but the artifact and a review brief.
review = await claude.messages.create(
model="claude-sonnet-4-5",
messages=[{
"role": "user",
"content": (
"Review the following code for correctness bugs, edge cases, "
"and unstated assumptions. You have no knowledge of why any "
"decision was made; judge only what is written.\n\n" + code
),
}],
)
return {"code": code, "review": extract_text(review)}
TypeScript
async function generateAndReview(taskDescription: string) {
const generation = await claude.messages.create({
model: "claude-sonnet-4-5",
messages: [{ role: "user", content: taskDescription }],
});
const code = extractCode(generation);
const review = await claude.messages.create({
model: "claude-sonnet-4-5",
messages: [{
role: "user",
content:
"Review the following code for correctness bugs, edge cases, and " +
"unstated assumptions. You have no knowledge of why any decision " +
"was made; judge only what is written.\n\n" + code,
}],
});
return { code, review: extractText(review) };
}
Two further moves address a different failure that shows up even in independent review once the artifact gets large: attention dilution. Ask one pass to review a ten-file change at once and attention spreads across all ten simultaneously, so local bugs get less scrutiny than they would alone, and cross-file issues tangle with single-file issues into contradictory findings. The fix is multi-pass review: a local pass per file focused on that file's internal correctness, plus a separate integration pass whose job is cross-file data flow — does a function's return value match what the downstream caller assumes, does a schema change get honored everywhere it is consumed. Narrow questions get better answers than one diffuse one.
The second move is having the review pass self-report a confidence score alongside every finding, as a routing signal rather than a substitute for judgment. A high-confidence finding — "this null check is missing and will throw on the documented empty-list case" — routes toward being surfaced prominently to the human reviewer first. A low-confidence one — "this might be a race condition depending on call scheduling" — routes toward a follow-up pass or lower-priority review rather than being accepted or dismissed outright. Note what this self-report is not doing: it is not, by itself, deciding that the high-confidence finding is correct and blocking merges on the model's say-so. A raw self-report is only a prioritization signal for a human's attention until it has been checked against outcomes; the confidence section below returns to exactly this point and explains what has to be true before a confidence score is allowed to carry more weight than that.
Escalation and ambiguity resolution: what actually earns a handoff
A customer-service agent that escalates too often is expensive and useless; one that escalates too rarely is dangerous. The exam's answer is a short, closed list, and the discipline it wants is refusing to add anything to it: the customer explicitly asks for a human, the applicable policy has a genuine gap the agent cannot resolve on its own authority, or the agent has made a real attempt and cannot make further progress. Everything else — the case felt hard, the customer seemed upset, the agent wasn't sure — is off the list, and a prompt that escalates on those grounds misfires both ways: escalating easy cases that merely looked complicated, and grinding forward on hard cases where the model was confidently wrong.
The first trigger looks like the least interesting one, but it is worth being precise about. When a customer explicitly says "let me talk to a person," escalate immediately, without first investigating — not "let me just check one thing first." Running an investigation anyway, however well-intentioned, treats their stated preference as an obstacle rather than an instruction. This differs from a customer who is merely frustrated but has not asked for a human: frustration alone is not a trigger, and the right move is to acknowledge it while offering to resolve the issue if it is within the agent's capability, escalating only if the customer reiterates that they want a person. The distinction is between what the customer asked for and what the agent inferred from tone, and only the former counts.
The second trigger, a policy gap, requires the most judgment to recognize, because it does not announce itself the way an explicit request does. Consider a refund policy that specifies exactly how to handle a price drop on the company's own site, but says nothing about a price match against a competitor's listing. An agent that treats silence as license — "the policy doesn't forbid it" — has invented a policy that was never approved; one that treats silence as a flat no has denied a request the business might want honored. Both are wrong for the same reason: this is a decision the agent lacks authority to make either way, and the silence itself is the trigger to escalate, not a gap to reason through.
The third trigger, inability to make meaningful progress, is the closest thing to a genuine complexity signal, because it is measured by outcome rather than feeling. An agent that tried the available tools and a reasonable set of alternate approaches and still cannot resolve the request has evidence, in exhausted options, that escalation is warranted — unlike an agent that merely predicts a request will be hard before trying anything, which is a forecast, not a finding.
Two named failure modes appear here because they are what question-writers reach for. Sentiment-based escalation — routing to a human whenever the customer's language reads as upset — solves a different problem: sentiment does not correlate with case complexity, since an angry customer can have a trivially resolvable issue and a calm one a case the agent has no business handling alone. Self-reported confidence is more seductive, sounding like exactly the humility you would want from a cautious system, but it fails for a sharper reason: a model wrong about a hard case is often wrong about being wrong, because the same limitations that made the case hard also make it a poor judge of its own performance. Asked "are you confident?" on the case it is most likely to get wrong, it will frequently say yes, because the pattern that fooled its reasoning fools its self-assessment too.
The fix is explicit escalation criteria written into the system prompt, reinforced with few-shot examples demonstrating the boundary in both directions — a case that looks complicated but resolves cleanly, paired with one that looks simple but hits a genuine policy gap.
Python
ESCALATION_SYSTEM_PROMPT = """
Escalate to a human agent only when one of the following is true:
1. The customer explicitly asks to speak with a human agent.
2. The applicable policy is silent or ambiguous on the customer's specific
request (not merely complex to apply).
3. You have attempted a resolution using the tools available and cannot
make further progress.
Do not escalate based on how difficult the case seems, how frustrated the
customer sounds, or how confident you feel. Frustration without an explicit
request for a human should be met with acknowledgment and an offer to
resolve; escalate only if the customer repeats the request for a person.
Example — escalate:
Customer: "I want a price match against a competitor's listing."
Policy covers only same-site price drops. -> Escalate: policy gap.
Example — do not escalate:
Customer (frustrated): "This is the third time I've had to explain this,
just fix it!"
Issue: a standard, in-policy refund. -> Acknowledge frustration, resolve
the refund, do not escalate.
"""
TypeScript
const ESCALATION_SYSTEM_PROMPT = `
Escalate to a human agent only when one of the following is true:
1. The customer explicitly asks to speak with a human agent.
2. The applicable policy is silent or ambiguous on the customer's specific
request (not merely complex to apply).
3. You have attempted a resolution using the tools available and cannot
make further progress.
Do not escalate based on how difficult the case seems, how frustrated the
customer sounds, or how confident you feel. Frustration without an explicit
request for a human should be met with acknowledgment and an offer to
resolve; escalate only if the customer repeats the request for a person.
Example -- escalate:
Customer: "I want a price match against a competitor's listing."
Policy covers only same-site price drops. -> Escalate: policy gap.
Example -- do not escalate:
Customer (frustrated): "This is the third time I've had to explain this,
just fix it!"
Issue: a standard, in-policy refund. -> Acknowledge frustration, resolve
the refund, do not escalate.
`;
One more ambiguity is a close relative rather than a fourth trigger: what to do when get_customer returns more than one match. The temptation is a heuristic — most recently active account, closest name match — and the exam is unambiguous that this is wrong, since a heuristic risks silently acting on the wrong customer's data, real harm in process_refund territory. The correct behavior is to ask for an additional identifier — an order number, a billing zip code — and let it disambiguate deterministically, rather than letting the model guess which account "probably" matches.
Human review workflows and confidence calibration
The extraction domain — pulling structured fields out of invoices, contracts, or forms with something like extract_invoice — runs into a seductive number: overall accuracy. A system reporting 97% sounds close to done, and the instinct is to reduce human review in proportion to how close to 100% it gets. The exam treats this as a trap, because an aggregate figure is an average across everything the system processes, and averages hide unevenness. A 97% overall figure is fully consistent with 99.5% on the invoice type that dominates volume and 60% on a rarer document type or a field the model reliably mishandles — a foreign-currency total, a hand-filled date, a field unique to one vendor's template. Automate on the aggregate and you remove oversight from exactly the segment that needed it most.
The fix is to distrust the aggregate and validate accuracy broken out by document type and by field, confirming performance is consistent across every segment before reducing review on any of them.
Two further mechanisms to build, not one-time checks to run, round this out. The first is stratified random sampling of extractions the model reported as high confidence — the ones that would otherwise sail through unseen. Counterintuitive, since these are the cases the system claims to have handled well, but that is the point: ongoing sampling of the "safe" bucket is how you measure the true error rate on the population you are not otherwise checking, and how you catch a novel error pattern before it accumulates.
The second mechanism is field-level confidence scoring, calibrated against a labeled validation set: emit a confidence value per field, then check those values against ground truth on data with known-correct labels. Calibration turns a raw number into something usable — fields reported at 0.9 confidence should be correct 90% of the time, not 65%, or the score is miscalibrated and routing on it misallocates review effort. Once calibrated, route low-confidence fields and any document with contradictory source material to human review first, since reviewer time is limited and calibrated confidence spends it where it is most likely to catch a real error.
Python
def route_for_review(extraction: dict, calibration: dict) -> str:
"""calibration maps confidence buckets to measured accuracy from a
labeled validation set -- e.g. {0.9: 0.97, 0.8: 0.88, 0.7: 0.61}."""
field_conf = extraction["fieldConfidence"] # per-field raw score
lowest = min(field_conf.values())
measured_accuracy = calibration_lookup(calibration, lowest)
if extraction.get("sourceContradictory"):
return "human_review" # contradictory source, regardless of score
if measured_accuracy < 0.90:
return "human_review" # calibrated accuracy below threshold
if is_in_stratified_sample(extraction["documentId"]):
return "human_review" # sampled even though it looks safe
return "auto_accept"
TypeScript
function routeForReview(
extraction: Extraction,
calibration: Map<number, number>,
): "human_review" | "auto_accept" {
const fieldConf = extraction.fieldConfidence;
const lowest = Math.min(...Object.values(fieldConf));
const measuredAccuracy = calibrationLookup(calibration, lowest);
if (extraction.sourceContradictory) return "human_review";
if (measuredAccuracy < 0.90) return "human_review";
if (isInStratifiedSample(extraction.documentId)) return "human_review";
return "auto_accept";
}
Notice what makes this loop trustworthy where the escalation trigger in the previous section was explicitly untrustworthy: the confidence number here has been checked against reality before it is allowed to make a decision. Hold that thought — it is the entire subject of the next section.
Provenance in multi-source synthesis
The last task statement moves into research-style workflows: several subagents gather material from different sources, and a coordinator or synthesis step combines their findings into one report. The failure mode here is quieter than a wrong answer — a right answer that has lost the ability to say where it came from.
Source attribution gets lost during summarization for an ordinary, almost innocent reason: a summarization step's job is to compress, and compression discards whatever the compressing model judges inessential. A citation attached to a sentence looks, to a step whose only mandate is "shorten this," like detail safe to drop, because the claim survives compression even when its source does not. Multiply that across two or three passes and the document ends up full of confident, unattributed claims indistinguishable from claims the system verified itself.
The fix is structural, not a matter of asking more nicely for citations. Require every subagent to output a claim-source mapping — the claim, paired with the source URL or document name and the supporting excerpt — and require every downstream step, including synthesis, to preserve and merge those mappings rather than compress them away. A prompt that says "summarize these findings" invites the attribution loss above; one that says "merge these findings, and every claim must carry the source mapping it arrived with" makes dropping a citation a visible, checkable defect.
Python
class ClaimSource(TypedDict):
claim: str
sourceUrl: str
excerpt: str
publicationDate: str | None # required when available -- see below
class SubagentFinding(TypedDict):
topic: str
claims: list[ClaimSource]
def synthesize(findings: list[SubagentFinding]) -> dict:
# Merge, don't summarize away, the claim-source pairs.
all_claims = [c for f in findings for c in f["claims"]]
conflicts = detect_conflicting_values(all_claims)
contested_claims = {c["claim"] for c in conflicts}
return {
"wellEstablished": [c for c in all_claims if c["claim"] not in contested_claims],
"contested": conflicts, # annotated, not resolved by picking one
"allSources": all_claims,
}
TypeScript
interface ClaimSource {
claim: string;
sourceUrl: string;
excerpt: string;
publicationDate?: string;
}
interface SubagentFinding {
topic: string;
claims: ClaimSource[];
}
function synthesize(findings: SubagentFinding[]) {
const allClaims = findings.flatMap((f) => f.claims);
const conflicts = detectConflictingValues(allClaims);
const contestedClaims = new Set(conflicts.map((c) => c.claim));
return {
wellEstablished: allClaims.filter((c) => !contestedClaims.has(c.claim)),
contested: conflicts,
allSources: allClaims,
};
}
The claim-source pattern above is necessary because, in a multi-agent pipeline, a subagent's findings arrive as a subagent's own prior conclusions — text it wrote, with no original document attached for Claude to point back into. That is not the only shape provenance takes, though, and it is worth distinguishing from the case where Claude is reading a source document itself. When a document content block is present in the request and citations: {"enabled": true} is set on it, the API returns citation blocks automatically — the cited text, which document it came from, and its location within that document — without you building or maintaining the mapping by hand. Anthropic's own evaluation reports this beating prompt-based citation approaches by as much as 15% on recall, since the model is grounding claims against text it can see rather than being asked to remember and report a source. This does not replace the ClaimSource pattern above; it applies to a narrower case. Use built-in citations when Claude is reading the document directly. Keep the claim-source mapping for subagent-to-subagent handoffs, where the thing being passed along is a prior finding, not a document Claude can cite into.
Two sources disagreeing on a number — one report says a market grew 12%, another says 9% — is not a bug to fix by picking the more reputable-sounding source and discarding the other; both may be credible, measuring slightly different things or the same thing at different times. The right behavior is to annotate the conflict with both values and sources, letting the reader see the disagreement rather than an artificially confident single number. This is the same discipline chapter 4 asked of a subagent hitting a coverage gap: a contested finding, clearly labeled, is useful; a confident finding that quietly resolved a disagreement by fiat is a hazard, because nothing signals a judgment call was buried inside it.
Document analysis upstream of synthesis should follow the same principle rather than resolving the conflict itself: when it finds two documents stating different figures for the same nominal fact, complete the analysis with both values annotated as conflicting, and hand that up to the coordinator, which holds the full picture and is better placed to reconcile it or surface it as an open question.
Closely related is the temporal problem, worth separating from the conflict problem because it produces the same symptom for a different reason: figures that look contradictory are sometimes simply measurements taken at different times, and dropping the dates converts a normal, explainable trend into an apparent inconsistency. Requiring every claim to carry a publication or data-collection date, as in the publicationDate field above, is what lets synthesis tell "these sources disagree" apart from "the world changed between publications."
Finally, provenance is also about not flattening material into one uniform shape. Financial figures read best as a table, a news development as prose, a technical finding with discrete components as a structured list. Forcing all three into paragraph form throws away information about the material's own structure. Preserving the source's original characterization and methodological context — survey, regulatory filing, estimate — belongs in the same category: part of what the claim is, not decoration to trim for length.
The confidence contradiction, resolved
Read straight through, the sections above look like they disagree about confidence, and the contradiction is worth naming exactly so the resolution lands. Self-reported confidence was named, explicitly, as an unreliable proxy for case complexity and not one of the three legitimate escalation triggers. A moment later, field-level confidence scores were the load-bearing mechanism for deciding what a reviewer sees first, and the review-architecture section presented a verification pass self-reporting confidence as good design, not a trap. Confidence is condemned in one breath and relied upon in the next. Carry only the first half into the exam room and you will confidently pick the wrong answer on a question built to test this.
The resolution is calibration. An uncalibrated self-report is the model's own guess about itself, and a guess made by the same system whose competence is in question proves nothing about that competence. That is why "the model says it's not confident" is not on the list of three valid triggers: the case where the model is most likely to be silently wrong is exactly the case where its self-assessment is also most likely to be wrong, for the same underlying reason, and stacking one unreliable signal on another does not produce a reliable one.
A calibrated confidence score is a different object entirely, even though it is computed the same way. What makes it different is that the number has been checked against a labeled validation set and shown to track the true error rate: extractions scored at 0.9 really are correct roughly 90% of the time, measured against ground truth the model never saw. At that point the score has stopped being the model's private opinion of itself and become a measurement, verified the way you would verify any instrument before trusting its readings — and a measurement is a legitimate routing input in a way a guess never is.
So the rule is not "trust confidence" or "distrust confidence." It is: never let a model's raw, unchecked self-report make an autonomous decision on its own — not whether to escalate a case, not whether code it just wrote is correct — but once a score has been calibrated against ground truth and shown to predict actual error rate, it graduates from an opinion into a measurement, and measurements are exactly what you should route decisions on.
What the exam tests
This chapter covers task statements 4.6, 5.2, 5.5 and 5.6. On 4.6, expect direct questions on why independent review instances outperform self-review — retained reasoning context biasing the generator toward its own choices, not a matter of trying less hard — and why extended thinking does not substitute for a second reviewer. Expect the three escalation triggers tested as a closed set, with sentiment and self-reported confidence as plausible wrong answers on 5.2, and the explicit-request-versus-frustration distinction tested as its own judgment call. Expect a question built around a policy that is silent rather than restrictive, testing whether you escalate the gap, and a multiple-match lookup case testing whether you ask for another identifier instead of guessing. On 5.5, expect the aggregate-accuracy trap tested directly, alongside stratified sampling and calibrated field-level routing. On 5.6, expect questions on claim-source mappings surviving synthesis, annotated rather than arbitrarily resolved conflicts, required publication dates, and matching output format to content type. Above all, expect a question probing whether self-reported confidence is invalid as an autonomous trigger but valid as a calibrated routing input — the reconciliation this chapter exists to teach.
Exercises
-
Take code you generated with Claude Code in one session, and in a separate fresh session with no shared history, ask a new instance to review it for correctness with no context beyond the code. Compare against asking the original session to review its own output, and identify one finding the fresh instance caught that the original missed.
-
Draft an escalation system prompt for process_refund and escalate_to_human, with few-shot examples for all three triggers plus negative examples for sentiment- and confidence-based escalation. Test it against an angry customer with a simple in-policy request, and a calm customer asking about a scenario the policy never addresses, and confirm behavior diverges correctly.
-
Build a small extract_invoice pipeline emitting per-field confidence, construct a labeled validation set of twenty documents, and compute measured accuracy per confidence bucket. Determine whether the raw scores are calibrated, and if not, what routing threshold you would use instead.
-
Design a three-subagent research task where sources are likely to disagree on a figure. Require claim-source mappings with publication dates, and write a synthesis step producing explicit wellEstablished and contested sections. Rerun it with a "summarize the findings" prompt instead, and compare which citations and conflict annotations survive.