What you need to know
A hook is a callback the agent runtime calls at a fixed point in the loop. Two hook events matter most for this task:
| Event | Fires | Typical use |
|---|---|---|
| PreToolUse | After Claude requests a tool, before it runs | Block policy-violating calls, redirect to another workflow, rewrite inputs |
| PostToolUse | After the tool runs, before Claude sees the result | Normalise formats, trim fields, add context |
Hooks attach to tools through a matcher on the tool name. MCP tools are named mcp__<server>__<tool>, so a hook for Cedar Ridge's refund tool matches mcp__support__process_refund.
Blocking and redirecting: PreToolUse
A PreToolUse hook inspects the call and returns a decision. To block, it returns deny with a reason. The reason goes back to Claude, which can then take the alternative path, such as escalating.
async def refund_limit(input_data, tool_use_id, context):
amount = input_data["tool_input"].get("amount", 0)
if amount > 500:
return {"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason":
"Refunds over $500 need a human. Call escalate_to_human with a summary."}}
return {} # allow
options = ClaudeAgentOptions(hooks={"PreToolUse": [
HookMatcher(matcher="mcp__support__process_refund", hooks=[refund_limit])]})
Normalising data: PostToolUse
Different tools return the same kind of data in different shapes: Unix epoch seconds from one, ISO 8601 strings from another, numeric status codes from a third. Left alone, Claude has to reconcile them in every turn and sometimes gets it wrong. A PostToolUse hook converts everything to one format before the model reasons about it. Current SDKs let the hook replace the result (the updatedToolOutput field) or append a note (additionalContext).
STATUS = {0: "succeeded", 2: "pending", 5: "rejected"}
def normalise(result: dict) -> dict:
out = dict(result)
if "created" in out and isinstance(out["created"], int):
out["created"] = datetime.fromtimestamp(out["created"], tz=timezone.utc).isoformat()
if "status" in out and isinstance(out["status"], int):
out["status"] = STATUS.get(out["status"], "unknown")
return out
The same normalisation could live inside each tool. A hook is the right place when you do not own the tools, or when you want one rule applied to all of them.
Deterministic versus probabilistic
This is the idea the exam tests hardest. A rule written in the system prompt is followed with high probability. A rule enforced by a hook is followed every time, because the call never reaches the tool. When a business rule requires guaranteed compliance (refund limits, blocked file paths, approval before changes), choose the hook.
In the exam
Look for a rule that "usually" holds but has failed a measured number of times, or for tools returning inconsistent formats. Hooks are the answer when the stem says the rule must always hold. Watch the direction: blocking a call is PreToolUse; changing what Claude sees from a result is PostToolUse.
Check your understanding
Three MCP tools return timestamps as epoch integers, ISO strings and locale strings. Claude sometimes compares them wrongly. Where should you fix this?
Show answer
Answer: C. PostToolUse hooks run after the tool and before the model sees the result, so Claude only ever reasons over one format.
Prompt instructions keep refunds under $500 in 99.5% of cases. Finance requires 100%. What do you add?
Show answer
Answer: D. Only code that intercepts the call can guarantee the rule. Descriptions, examples and temperature improve the odds but leave a failure rate.
A PreToolUse hook denies a call. What should its reason tell Claude?
Show answer
Answer: A. The reason is returned to Claude. A clear, actionable reason lets Claude take the alternative path. "Unavailable, retry" invites pointless retries of a call that will always be blocked.
Apply it: Cedar Ridge's hook register
Cedar Ridge's platform engineer wants every hook in one register before code review. Build it.
- List the hooks. For each one give the event, the matcher, what it checks or changes, and what Claude sees.
Done when: the register covers the refund limit, identity first, and the three data quirks in the case. - Write the normalisation rules for timestamps and refund statuses.
- Mark each rule as hook or prompt, and justify the split.
- Name one thing you would not put in a hook, and say why.
Show model answer
1. Hook register
| Hook | Event | Matcher | Does | Claude sees |
|---|---|---|---|---|
| Identity first | PreToolUse | mcp__support__lookup_order and mcp__support__process_refund | Denies if no verified customer in this session | "Verify the customer with get_customer first" |
| Refund limit | PreToolUse | mcp__support__process_refund | Denies amounts over $500 | "Over $500: escalate with a handoff summary" |
| Normalise | PostToolUse | ^mcp__support__ | Epoch and ISO timestamps to ISO 8601 UTC; numeric status to a label | Consistent results |
| Trim orders | PostToolUse | mcp__support__lookup_order | Keeps the 6 return-relevant fields of about 40 | A short result |
2. Rules. Timestamps: integer epoch seconds become ISO 8601 in UTC; ISO strings are re-emitted in UTC. Statuses: 0 to "succeeded", 2 to "pending", 5 to "rejected", anything else to "unknown" with the raw code kept.
3. Hook or prompt. Hooks: identity first and the refund limit, because finance requires 100% and the case calls them rules. Normalisation is also a hook, because it is mechanical and must apply to every result. Prompt: tone, apology wording, when to offer a replacement versus a refund, and an explanation of the two blocking rules so Claude plans around them.
4. Not a hook. Deciding whether a hardship story justifies an exception. That is a judgement call, so it belongs to escalation criteria and a human, not to deterministic code.