1.5 Agent SDK Hooks
1.5.1 Code That Runs at Exactly the Right Moment
Lesson 1.4 told you that for high-stakes requirements you need code, not just prompts. This lesson shows you the specific machinery Claude gives you to run that code at precisely the right moment: hooks. If enforcement was the 'why,' hooks are the 'how.'
A hook is a piece of your own code that Claude runs automatically at a defined point in the agent's lifecycle — for instance, 'right before any tool runs' or 'right after a tool returns.' You don't call the hook yourself; you register it, and Claude fires it at that moment, every time, without fail. Think of hooks like the sensors and interlocks on a factory line: a light barrier that halts the press before a hand can reach in, or an inspection station that stamps every part as it passes. They're not part of the product being made — they're guarantees wired into the line itself.
That 'every time, without fail' is the whole point. Because a hook is your code firing at a fixed point, it behaves deterministically — exactly what Lesson 1.4 said high-stakes work demands. The exam focuses on two moments in particular (just before a tool runs, and just after), one decision rule for choosing between them, and one classic mistake about timing. Let's build all three up carefully.
A hook fires at a fixed lifecycle point. The two the exam cares about straddle a tool call: PreToolUse (before — can block) and PostToolUse (after — can reshape the result).
The one idea to hold onto
A hook is your code that Claude runs automatically at a fixed point in the lifecycle. Because it fires every time, it's deterministic — the mechanism Lesson 1.4 called for. The two key moments are just before a tool runs and just after.
1.5.2 Timing Is Everything: Before vs After
The single most important thing about these two hooks is WHEN they fire, because that determines what they can and cannot do. Everything else follows from timing.
A PreToolUse hook fires BEFORE the tool runs. At that instant the action hasn't happened yet — so this is your one chance to stop it. You can block it outright, or redirect it somewhere else. A PostToolUse hook fires AFTER the tool has already run. The action is done; you can't un-ring that bell. What you CAN do is reshape the result before the model sees it. The mental test is simple: if you want to PREVENT something, you must act before it happens (PreToolUse); if you want to TIDY UP what came back, you act after (PostToolUse).
Here's where the classic exam mistake lives. Suppose you need to block refunds over $500. A tempting answer is 'use a PostToolUse hook to check the amount.' But PostToolUse fires AFTER process_refund has already executed — the $500 is already gone. By the time your hook looks at the amount, it's too late. Blocking MUST happen in PreToolUse. This timing trap is tested directly, so let the factory analogy anchor it: the safety interlock has to stop the press BEFORE it comes down, not file a report afterwards.
Timing decides capability. A PostToolUse hook runs after the tool already executed, so it can never block — it can only reshape what came back. Blocking belongs in PreToolUse.
1.5.2 — Key Concept
Use PreToolUse to BLOCK or redirect an action (it fires before execution) and PostToolUse to RESHAPE a result (it fires after). A PostToolUse hook can never block anything, because the tool has already run by the time it fires.
1.5.3 PostToolUse: Making Messy Data Tidy
Let's see each hook's signature use case, starting with PostToolUse. Its job is normalization — turning the messy, inconsistent data that different tools return into one clean shape before the model has to reason about it.
Picture an agent wired to three different backend tools. One returns timestamps as Unix epoch numbers (1719792000), another as ISO-8601 strings (2024-07-01), a third as numeric status codes (2 instead of 'shipped'). If all that heterogeneity reaches the model, it has to waste effort decoding three formats and may slip up. A PostToolUse hook intercepts each tool's output and rewrites it into a single consistent shape — every timestamp as ISO-8601, every status as a readable word — so the model sees clean, uniform data.
Crucially, this is a DETERMINISTIC transformation, which is exactly why it belongs in a hook and NOT in the prompt. You could write 'always convert timestamps to ISO-8601' in the system prompt, but that's a sign again — probabilistic, occasionally ignored, and it burns tokens every turn. A hook does the conversion in code, every single time, invisibly. Format-wrangling is a job for a hook, not a request to the model.
1.5.3 — Key Concept
PostToolUse hooks normalize heterogeneous tool results (Unix→ISO-8601, status codes→words, currency formatting) before the model sees them. Because it's a deterministic transform, it belongs in a hook — never in the prompt, which is probabilistic and wastes tokens.
1.5.4 PreToolUse: Enforcing the Rules
PreToolUse is the enforcement hook — it's where Lesson 1.4's 'lock' actually gets built for tool calls. Because it fires before execution, it can inspect what the model is about to do and decide: allow it, block it, or redirect it.
This is how you implement compliance rules deterministically: a refund over $500 → block and route to human escalation; a transfer_funds call → block unless an AML check has passed; a discount over 20% → redirect into a manager-approval queue. In each case the model proposes the action, the PreToolUse hook intercepts it before it can run, and your code has the final word — the same model-proposes/code-disposes principle from Lesson 1.1, now realised as a concrete hook.
# PreToolUse hook: block refunds over $500, redirect to escalation
AMOUNT=$(jq -r '.tool_input.amount' < /dev/stdin)
if (( $(echo "$AMOUNT > 500" | bc -l) )); then
jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",
permissionDecision:"deny",
permissionDecisionReason:"Refund over $500 requires human escalation"}}'
else
exit 0 # no decision; let the normal permission flow proceed
fiHow does a hook communicate its decision back to Claude? Two ways, and you pick ONE, not both. The simplest is exit codes: exit 0 means 'fine, carry on'; exit 2 means 'BLOCK this.' There's a subtle gotcha the exam likes: exit code 1 does NOT block — only exit 2 does. (Exit 1 is treated as an ordinary, non-blocking error.) The alternative is to exit 0 and print a small JSON object like {"decision":"block","reason":"…"} for richer control. Use exit codes alone, or JSON alone — never mix them.
1.5.4 — Key Concept
PreToolUse hooks enforce rules by blocking or redirecting actions before they run. A hook signals its decision via exit codes (exit 2 blocks; exit 0 allows; exit 1 does NOT block) OR via JSON on exit 0 — choose one mechanism, not both.
1.5.5 The Bigger Picture: a Lifecycle Full of Hooks
PreToolUse and PostToolUse are the two the exam drills, but they're part of a much larger family. Claude can fire hooks at many lifecycle moments — and knowing the families exist marks you as someone who understands the system, not just two events.
| Hook family | Example events | Can it block? |
|---|---|---|
| Around tool calls | PreToolUse, PostToolUse, PostToolBatch | Pre: yes · Post: no |
| Around subagents | SubagentStart, SubagentStop | SubagentStop: yes |
| Around the turn | UserPromptSubmit, Stop | yes |
| Around the session | SessionStart, SessionEnd | no |
| Around compaction | PreCompact, PostCompact | Pre: yes |
The hook taxonomy spans tools, subagents, turns, sessions, and compaction. PreToolUse blocks before a tool; PostToolUse is for reshaping/logging after.
Two details connect back to earlier lessons. First, hooks can be scoped to a subagent — defined in that subagent's own configuration so they only fire while it's active, then clean up when it finishes. Second, a neat consequence: a 'Stop' hook (which fires when an agent finishes its turn) automatically becomes a 'SubagentStop' hook when it's attached to a subagent — because that's the event that actually fires when a subagent completes. You don't have to special-case it; Claude does the right thing.
1.5.5 — Key Concept
Beyond Pre/PostToolUse, hooks span subagent, per-turn, session, and compaction events. Subagent-scoped hooks fire only while that subagent is active, and a Stop hook attached to a subagent auto-converts to SubagentStop.
1.5.6 The Exam Traps
Every 1.5 trap is a confusion about timing or layer — using the wrong hook direction, expecting a prompt to do a hook's deterministic job, or fumbling the exit codes. Anchor on 'before blocks, after reshapes' and they dissolve.
- •Blocking in the wrong place. ✗ Using a PostToolUse hook to block an action. ✓ It already ran — block in PreToolUse.
- •Prompting where a hook is needed. ✗ Relying on a system-prompt instruction for a 100%-compliance requirement. ✓ Use a hook — prompts are probabilistic.
- •Transforming in the model instead of in code. ✗ Asking the model to convert every timestamp to ISO-8601. ✓ Do it deterministically in a PostToolUse hook.
- •Wrong exit code. ✗ Returning exit 1 expecting it to block. ✓ Only exit 2 blocks; exit 1 is a non-blocking error.
Three quick questions route you to the right mechanism: prevent → PreToolUse; tidy up → PostToolUse; guarantee → a hook rather than a prompt.
1.5.6 — Exam Trap
Reject any answer that blocks in PostToolUse (too late), relies on a prompt for a 100% guarantee (probabilistic), normalizes data in the model instead of a PostToolUse hook, or uses exit 1 to block (only exit 2 blocks). Block in PreToolUse; normalize in PostToolUse; choose hooks when the guarantee must be absolute.
1.5.7 Put It Together: Wire Up Both Hooks
You now understand what a hook is, why timing decides its power, PostToolUse for normalization, PreToolUse for enforcement, the exit-code mechanics, and the wider lifecycle. As always, the surest way to cement it is to build both — and to feel the timing trap by deliberately putting the block in the wrong place.
1.5.7 — Build Exercise (45 min)
Add hooks to a finance/support agent. (1) Write a PostToolUse hook that normalizes three tools returning timestamps in different formats (Unix, ISO-8601, numeric status codes) into one consistent shape. (2) Write a PreToolUse hook that blocks process_refund above $500 and redirects to escalation, using permissionDecision:"deny" (or exit 2). (3) Prove the direction matters: move the blocking logic into PostToolUse and watch the refund go through before your hook ever fires. (4) Confirm the exit-code rule — exit 1 lets the action proceed, exit 2 blocks it.
Hooks are the precision instrument of enforcement. With them, you've now seen the full toolkit Domain 1 gives you for shaping agent behaviour: the loop you control (1.1), teams you coordinate (1.2–1.3), and guarantees you enforce (1.4–1.5). The remaining two lessons turn to how you STRUCTURE the work itself — decomposition (1.6) and managing state across sessions (1.7).
Where this shows up on the exam
1.5 questions are usually 'where should this logic go?' If you ask yourself whether the goal is to PREVENT (PreToolUse), to RESHAPE (PostToolUse), or to GUARANTEE (a hook, not a prompt), and you remember exit 2 — not exit 1 — blocks, you'll land the right answer.
Key Takeaways
- ✓A hook is your code that Claude runs automatically at a fixed lifecycle point; because it fires every time, it's deterministic — the enforcement mechanism Lesson 1.4 called for.
- ✓Timing decides power: PreToolUse fires BEFORE a tool (can block or redirect); PostToolUse fires AFTER (can reshape the result but never block — the action already ran).
- ✓PostToolUse normalizes heterogeneous tool results (Unix→ISO-8601, status codes→words) before the model sees them; this deterministic transform belongs in a hook, not the probabilistic prompt.
- ✓PreToolUse enforces compliance by blocking/redirecting before execution (refund over $500 → escalate; transfer_funds gated on AML) — model proposes, the hook (your code) disposes.
- ✓A hook signals via exit codes (exit 2 blocks; exit 0 allows; exit 1 does NOT block) OR JSON on exit 0 — pick one mechanism, not both.
- ✓The hook taxonomy spans tool, subagent, per-turn, session, and compaction families; subagent-scoped hooks fire only while active, and a Stop hook on a subagent auto-converts to SubagentStop.
- ✓Classic traps: blocking in PostToolUse (too late), using a prompt for a 100% guarantee, normalizing in the model instead of a hook, and expecting exit 1 to block.
Check Your Understanding
Test what you learned in this lesson.
Q1.You need to GUARANTEE that refunds over $500 are never processed and instead go to human escalation. Where should this logic live?
Q2.Three MCP tools return timestamps as Unix epochs, ISO-8601 strings, and numeric status codes. You want the model to see one consistent format. What's the right mechanism?
Q3.A hook script exits with code 1 expecting to block a dangerous tool call, but the call proceeds anyway. Why?
Q4.Which statement about PostToolUse hooks is correct?
Practice This Lesson