What you will be able to do
- Explain why prompt instructions alone cannot guarantee tool-call ordering, and when deterministic enforcement is required
- Design a PreToolUse/PostToolUse pair that blocks process_refund until get_customer has succeeded in the same session
- Choose a matcher that gates a specific set of MCP tools without catching unrelated ones
- Write a deny reason that lets Claude recover by completing the missing prerequisite
- Decompose a multi-concern customer request into distinct items, investigate each in parallel with subagents that receive shared context through their prompts, and synthesize one resolution
Key concept
Programmatic prerequisite gate — A hook that runs between Claude's tool request and the tool's execution. It checks whether earlier workflow steps have completed and denies the call if they have not, so the ordering holds no matter what the model decided.
1.Two places to put an ordering rule
Take a refund workflow: verify the customer, check the order, then issue the refund. The order matters, and you can enforce it in one of two places. One is the prompt, for example a system prompt that says "always call get_customer before process_refund." The other is code that sits between Claude and the tool.
To see why the choice matters, look at who decides what happens in the agent loop. On each turn Claude "evaluates the current state and determines how to proceed", which includes picking the next tool to call. A prompt instruction feeds into that choice. It makes the right order very likely, but the model still makes the call, so a prompt can influence the decision without guaranteeing it. The sources don't give a failure rate for prompt-only ordering. What the exam expects you to know is that the rate is not zero, and for some operations any non-zero rate is unacceptable.
The loop also has a step the model doesn't control. After Claude requests a tool, the SDK runs it, and "You can use hooks to intercept, modify, or block tool calls before they run." Hooks are code you register. They fire on the tool call itself, whatever reasoning led to it. Claude Code's feature guide lists hooks as the tool to "Run deterministic logic on tool calls (audit, block, transform)", and its power-user tips put it this way: hooks let you deterministically run logic at points in the agent lifecycle.
That gives you a decision rule. Use prompt guidance for preferences where an occasional slip costs little, such as tone, which lookup to try first, or how to word a summary. Use programmatic enforcement where compliance has to be deterministic. Identity verification before money moves is the standard example. The SDK hook docs list this kind of use directly: block dangerous operations before they execute, and "Require human approval for sensitive actions like database writes or API calls".
Checkpoint 1 of 6· Check yourself
A fintech support agent must never call process_refund until the customer's identity has been verified. Which design actually guarantees this ordering?
Prompt wording and examples only influence the model's choice. A PreToolUse hook runs before the tool executes and can block it. A Stop hook fires after Claude has finished responding, which is too late because the refund has already gone through.
“PreToolUse | Before a tool call executes. Can block it”Source: code.claude.com
2.Building the gate: record on success, enforce before execution
A prerequisite gate has two parts, and each uses a different hook event.
Record. Register a PostToolUse callback on get_customer. PostToolUse fires "After a tool call succeeds". If the lookup fails, PostToolUseFailure fires instead, so a failed lookup can never unlock the refund. The callback records that verification is done. Key that record by session, because "All hook inputs share session_id, cwd, and hook_event_name."
Enforce. Register a PreToolUse callback on process_refund. It checks the record for the current session_id. If there's no record, it returns a deny decision. If there is one, it returns an empty object and the call goes through the normal flow.
The docs' example below blocks writes to .env files rather than refunds. The deny shape is identical, though: put a different condition in the if statement and you have a refund gate.
async def protect_env_files(input_data, tool_use_id, context):
# Extract the file path from the tool's input arguments
file_path = input_data["tool_input"].get("file_path", "")
file_name = file_path.split("/")[-1]
# Block the operation if targeting a .env file
if file_name == ".env":
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "Cannot modify .env files",
}
}
# Return empty object to allow the operation
return {}Watch out for one tempting shortcut. The callback also receives a tool_use_id, but that ID only "correlates PreToolUse and PostToolUse events for the same tool call." It ties the before and after of one call together. It does not link get_customer to a later process_refund, so it can't be your verification key.
Next, decide which tools the gate covers. Tools that move money often come from an MCP server, and "MCP tools use the pattern mcp__<server>__<action>". The matcher string decides how a pattern is read:
| Matcher value | Evaluated as | Example from the docs |
|---|---|---|
| "*", "", or omitted | Match all | Fires on every occurrence of the event |
| Only letters, digits, _, -, spaces, , and | | Exact string, or a list of exact strings | Edit|Write matches either tool exactly |
| Contains any other character | JavaScript regular expression, unanchored | mcp__memory__.* matches every tool from the memory server |
So a single registration can cover a handful of money-moving tools: either an exact list joined with |, or a regular expression scoped to just those action names. Avoid a server-wide wildcard if the server also exposes read-only tools you don't want to gate. Above all, never leave the matcher empty on a gate. The troubleshooting guide warns: "Verify matcher patterns aren’t too broad: an empty matcher matches all tools".
Checkpoint 2 of 6· Exam question
A refund workflow requires that process_refund always be called with the exact customer_id captured by an earlier verified get_customer call, never a value the model retypes from the conversation. A PreToolUse hook already blocks the call when no verified ID exists in session state. What should the hook do once a verified ID is present, to prevent the model from substituting a different ID string?
Correct answer: B — Return permissionDecision "allow" together with updatedInput that overwrites the tool's customer_id argument with the verified ID stored in session state
- A. Incorrect. An empty object allows the call with whatever customer_id the model supplied, which is exactly the untrusted value the workflow needs to prevent from reaching the tool.
- B. Correct. Combining "allow" with updatedInput lets the hook substitute the trusted, verified value for whatever the model passed, closing the gap between what was verified and what actually reaches the tool.
- C. Incorrect. Escalating every already-verified refund to manual retyping adds friction without fixing the substitution risk, and does not correct the argument itself.
- D. Incorrect. Defer ends the query for later resumption; it does not correct the argument and is unnecessary once the ID is already verified in session state.
Checkpoint 3 of 6· Fill the gap
Which hook event key registers this callback so it can block a tool call before it runs?
options = ClaudeAgentOptions(
hooks={" ? ": [HookMatcher(matcher="Bash", hooks=[my_callback])]}
)Only PreToolUse fires before execution with the power to deny. PostToolUse fires after the tool has already succeeded, and Stop fires when Claude finishes responding.
Source: code.claude.com3.What happens when the gate fires
Each time Claude requests a tool, the same pipeline runs: an event fires, the SDK collects the registered hooks, matchers filter which ones run, the callbacks execute, and each callback returns a decision. A deny is one of those decisions, but the workflow doesn't have to end there.
For a command hook, permissionDecision: "deny" sends the reason back to Claude as the tool error. Claude reads it and can change course. The docs describe this for prompt-based hooks too. With continueOnBlock: true, they "return the reason to Claude as the tool error so it can adjust and continue, equivalent to a command hook’s permissionDecision: "deny"". Without that flag, a prompt-based PreToolUse deny ends the turn by default.
That makes permissionDecisionReason part of how the workflow works, not just a log line. A reason like "Customer identity not verified: call get_customer first" tells Claude exactly which prerequisite is missing. Claude calls get_customer, your PostToolUse callback records success, and the retried process_refund passes. The gate enforces the order and its message steers the agent toward it. If a tool gets blocked when you didn't expect it, the SDK troubleshooting steps start with checking every PreToolUse hook for permissionDecision: 'deny' returns.
Checkpoint 4 of 6· Put it in order
Put the stages of hook resolution in the order the SDK runs them when Claude requests a tool
- 1.The SDK collects registered hooks
- 2.Callback functions execute
- 3.An event fires
- 4.Matchers filter which hooks run
- 5.Your callback returns a decision
The SDK hooks guide gives this sequence. Matching happens before any callback code runs, which is why a too-narrow or too-broad matcher decides whether your gate runs at all.
“Matchers filter which hooks run”Source: code.claude.com
Checkpoint 5 of 6· Check yourself
Your command hook denies process_refund with permissionDecisionReason "Customer identity not verified: call get_customer first". What does Claude experience?
A command hook's deny returns the reason to Claude as a tool error and the turn continues, so a clear reason lets Claude complete the missing step and retry.
“return the reason to Claude as the tool error so it can adjust and continue, equivalent to a command hook’s permissionDecision: "deny"”Source: code.claude.com
4.Multi-concern requests: decompose, investigate in parallel, synthesize
Customers often raise several problems in one message: a refund for a damaged item, a delayed second order, and an address change. Handling them as one blob invites missed items. The pattern is to decompose the request into distinct items, investigate each one separately, then merge the findings into a single resolution. Anthropic's multi-agent guidance describes the same shape: the core implementation "decomposes a question into independent facets, runs subagents concurrently, then synthesizes the results."
The lead agent does the decomposition and the synthesis. In between, each concern goes to its own subagent. The subagents docs give the payoff for the middle step: "multiple subagents can run concurrently, so independent subtasks finish in the time of the slowest one". Use this only when the items really are independent. If one concern depends on another's result, run them in sequence.
Shared context needs care. A subagent does not see the lead's conversation: "each subagent runs in its own conversation, which starts fresh unless the subagent is a fork." The features guide states the practical consequence: what a subagent gets is "Whatever context the lead agent passes in the prompt". So when you fan out, put the shared facts into each subagent's prompt, such as the verified customer ID, the order numbers, and the specific concern that subagent owns. Without that, each subagent has to rediscover them or will guess.
Finally, only each subagent's final message returns to the lead. The lead reconciles those summaries into one reply that addresses every item, and it states clearly any item that needs escalation. Hooks still apply along the way: hooks in settings.json "fire in the main agent and any subagents it spawns", so a prerequisite gate is not bypassed by delegating.
Checkpoint 6 of 6· Check yourself
A lead agent splits a customer message into three independent concerns and spawns one subagent per concern. The customer ID was verified earlier in the lead's conversation. How does each subagent learn it?
Subagents start in a fresh conversation, so the prompt the lead passes is the channel for shared context. Intermediate tool calls stay inside each subagent.
“Whatever context the lead agent passes in the prompt”Source: code.claude.com
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.A forceful enough system prompt, with capitals, repetition and examples, makes identity verification before refunds effectively guaranteed.Why is that wrong?
A prompt only shapes the model's choice of the next tool. Deterministic compliance needs code that intercepts the tool call itself, which is what hooks do.
Covered in Two places to put an ordering rule
2.The tool_use_id passed to hooks can link the get_customer call to the later process_refund call, so it works as the verification key.Why is that wrong?
tool_use_id only pairs the PreToolUse and PostToolUse events of one tool call. Track verification across calls by session_id, which every hook input carries.
Covered in Building the gate: record on success, enforce before execution
3.Subagents spawned for each part of a multi-concern request automatically share the lead agent's conversation, so the lead need not repeat the customer details.Why is that wrong?
Each subagent starts fresh. The lead must pass the shared facts in each subagent's prompt.
Covered in Multi-concern requests: decompose, investigate in parallel, synthesize
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.https://code.claude.com/docs/en/agent-sdk/agent-loopOfficial docs
“You can use hooks to intercept, modify, or block tool calls before they run.”
↩︎ Two places to put an ordering rule“Claude evaluates the current state and determines how to proceed.”
↩︎ Two places to put an ordering rule“You can use hooks to intercept, modify, or block tool calls before they run.”
↩︎ Key concept - 2.
“Hooks let you deterministically run logic at points in the agent lifecycle.”
↩︎ Two places to put an ordering rule - 3.https://code.claude.com/docs/en/agent-sdk/hooksOfficial docs
“Require human approval for sensitive actions like database writes or API calls”
↩︎ Two places to put an ordering rule“All hook inputs share session_id, cwd, and hook_event_name.”
↩︎ Building the gate: record on success, enforce before execution“MCP tools use the pattern mcp__<server>__<action>”
↩︎ Building the gate: record on success, enforce before execution“Verify matcher patterns aren’t too broad: an empty matcher matches all tools”
↩︎ Building the gate: record on success, enforce before execution“Check all PreToolUse hooks for permissionDecision: 'deny' returns”
↩︎ What happens when the gate fires“correlates PreToolUse and PostToolUse events for the same tool call.”
↩︎ Exam trap 2“Matchers filter which hooks run”
↩︎ Checkpoint - 4.https://code.claude.com/docs/en/hooksOfficial docs
“PostToolUse | After a tool call succeeds”
↩︎ Building the gate: record on success, enforce before execution“JavaScript regular expression, unanchored”
↩︎ Building the gate: record on success, enforce before execution“return the reason to Claude as the tool error so it can adjust and continue, equivalent to a command hook’s permissionDecision: "deny"”
↩︎ What happens when the gate fires“PreToolUse | Before a tool call executes. Can block it”
↩︎ Checkpoint - 5.
“decomposes a question into independent facets, runs subagents concurrently, then synthesizes the results.”
↩︎ Multi-concern requests: decompose, investigate in parallel, synthesize - 6.https://code.claude.com/docs/en/agent-sdk/subagentsOfficial docs
“multiple subagents can run concurrently, so independent subtasks finish in the time of the slowest one”
↩︎ Multi-concern requests: decompose, investigate in parallel, synthesize“each subagent runs in its own conversation, which starts fresh unless the subagent is a fork”
↩︎ Multi-concern requests: decompose, investigate in parallel, synthesize“each subagent runs in its own conversation, which starts fresh unless the subagent is a fork”
↩︎ Exam trap 3 - 7.https://code.claude.com/docs/en/features-overviewOfficial docs
“Whatever context the lead agent passes in the prompt”
↩︎ Multi-concern requests: decompose, investigate in parallel, synthesize - 8.
“These fire in the main agent and any subagents it spawns.”
↩︎ Multi-concern requests: decompose, investigate in parallel, synthesize“Run deterministic logic on tool calls (audit, block, transform)”
↩︎ Exam trap 1