D2.4 · Domain 2 · Tool Design + Integration · 18% of CCA-F

SDK Hooks (Pre/PostToolUse).

11 min read·10 sections·Tier A

Hooks are deterministic code that runs before or after tool calls. They enforce policy that prompt-only patterns can't guarantee, refund limits, identity verification, audit logging. The exam pattern: prompt-only enforcement is wrong; hook is the deterministic gate. Claude Code hook docs

Deep-dive coming soonDomain 2
SDK Hooks (Pre/PostToolUse), hero illustration featuring Loop mascot in a warm gallery scene.
Domain D2Tool Design + Integration · 18%
On this page
01 · Summary

TLDR

Hooks are deterministic code that runs before or after tool calls. They enforce policy that prompt-only patterns can't guarantee, refund limits, identity verification, audit logging. The exam pattern: prompt-only enforcement is wrong; hook is the deterministic gate. Claude Code hook docs

2
Hook types
D2
Exam domain
prompt-only policy
Trap
B
Coverage tier
deterministic
Right answer
02 · Definition

What it is

A hook is a programmatic gate that runs before (PreToolUse) or after (PostToolUse) a tool executes, enabling deterministic policy enforcement. Unlike prompt instructions which are suggestions the model may ignore, hooks are execution-layer controls enforced by the SDK itself. When Claude requests a tool call, your PreToolUse hook intercepts it; you inspect arguments, validate against policy, and either allow the call or return an error block.

The fundamental insight is enforcement before execution. A prompt saying "never refund more than 500 dollars" is a suggestion; a 3% failure rate under production load proves it. A PreToolUse hook that checks input.amount <= 500 and exits with code 2 if violated is a guarantee: 100% enforcement, zero exceptions. This is the Prompt vs Hook heuristic, the single most-tested distractor pattern in Domain 2.

Hooks operate in two phases. PreToolUse runs with full authority to deny: your command returns exit code 2 (deny) or 0 (allow); Claude observes the signal and branches. PostToolUse runs after the fact, no veto power, but full observability: you receive the input and the raw result, ideal for normalized output transformation, audit logging, or triggering cascades. Architectural rule: if the rule affects whether something should happen, use PreToolUse; if it describes what happened, use PostToolUse.

Production hooks fail in two ways: incorrect matchers (a hook targeting Read never fires on Grep, each tool has a unique name) and silent errors (exit code 0 when validation fails, so Claude never sees the block). Without the hook, the tool runs, the policy is violated in production, and auditors catch it weeks later. With the hook, the violation is impossible at the execution layer.

03 · Mechanics

How it works

The hook lifecycle has three steps. Step 1: Tool Interception. Claude emits a tool_use block; the SDK extracts the tool name and input JSON. Step 2: Hook Execution. Your hook command runs as a subprocess; stdin contains a JSON object with hook_event_name, tool_name, tool_input, and optionally tool_response (PostToolUse only). Step 3: Decision Point. PreToolUse: exit code 0 (allow), 2 (deny), 1 (error). If 2, Claude gets a tool_use_error block and recovers.

Matcher syntax is critical. A matcher is a pipe-separated list of tool names: 'Read|Edit|Grep' fires on any of those three. The wildcard '*' matches all tools (useful for cross-cutting concerns like security audit logging). Matchers are exact string matches; 'Read' does not match 'ReadFile'. For hook development, dump stdin with jq . > debug.json to inspect the exact structure.

Deterministic path handling is mandatory. The recommendation is absolute paths only in hook commands. Relative paths trigger path-interception attacks (MITRE T1574.007). Use ${HOME} expansion or a setup script that replaces $PWD placeholders with absolute paths to share hook configs across team members.

Execution context is stateless and isolated. Hooks run in a subprocess with stdin/stdout; they have no access to Claude's message history, no shared memory with previous hooks. If a PreToolUse hook needs to log a decision, it writes to a file or sends an HTTP request. If a PostToolUse hook needs to trigger another action (like running a formatter after Edit), it must spawn that command itself. Each hook is a black box: read JSON, compute, output.

SDK Hooks (Pre/PostToolUse) mechanics, painterly diagram featuring Loop mascot.
04 · In production

Where you'll see it

Refund policy enforcement (SaaS support)

PreToolUse hook on process_refund. Reads stdin, extracts amount and customer_id. Queries policy database. If amount > 500 OR status = 'flagged', exits 2 with error. Without the hook, 3% of refunds violate policy; with the hook, zero violations.

Code execution sandboxing (CI/CD)

PreToolUse hook on Bash parses command string against a blocklist regex (rm -rf, sudo, drop database). If matches, exits 2. Claude proposes a safe alternative. Hook makes destructive commands impossible, even if the system prompt has no warning.

Output normalization for structured extraction

PostToolUse hook on extract_entities. Reads raw result. If null, returns {entities: [], errors: [{msg: 'no content'}]}; if string, wraps it; if array, normalizes. Claude receives the normalized shape every time, no null-checks in the prompt needed.

Audit logging for financial transactions

PostToolUse hook on transfer_funds. Reads stdin, appends to compliance log: {timestamp, session_id, input, response, user_ip}. Exits 0. Claude never knows the hook ran; it's transparent. Auditors can later reconstruct the full history.

05 · Implementation

Code examples

PreToolUse refund-policy hook
#!/usr/bin/env python3
import json, sys, sqlite3

def check_refund_policy(tool_input):
    amount = tool_input.get("amount", 0)
    customer_id = tool_input.get("customer_id")

    if not customer_id:
        return {"allowed": False, "error": "customer_id required"}
    if amount > 500:
        return {"allowed": False, "error": f"amount {amount} exceeds policy 500"}

    conn = sqlite3.connect("/opt/policy.db")
    row = conn.execute(
        "SELECT status, lifetime_refunded FROM customers WHERE id = ?",
        (customer_id,),
    ).fetchone()
    conn.close()

    if not row:
        return {"allowed": False, "error": f"customer {customer_id} not found"}
    status, lifetime = row
    if status == "suspended":
        return {"allowed": False, "error": f"customer {customer_id} suspended"}
    if lifetime + amount > 2000:
        return {"allowed": False, "error": f"lifetime would be {lifetime + amount}, max 2000"}
    return {"allowed": True}

def main():
    try:
        hook_data = json.load(sys.stdin)
        result = check_refund_policy(hook_data["tool_input"])
        if result["allowed"]:
            sys.exit(0)
        else:
            print(json.dumps({"error": result["error"]}), file=sys.stderr)
            sys.exit(2)
    except Exception as e:
        print(json.dumps({"error": f"hook error: {e}"}), file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()
Exit 0 to allow, exit 2 to deny with error. Claude reads stderr and recovers.
06 · Distractor patterns

Looks right, isn't

Each row pairs a plausible-looking pattern with the failure it actually creates. These are the shapes exam distractors are built from.

Looks right

Write the enforcement rule in the system prompt: "Never refund more than 500."

Actually wrong

Production data shows 3-7% violation rate when enforcement is prompt-only. Use a PreToolUse hook that checks the numeric value: 100% enforcement, zero variance.

Looks right

Use a PostToolUse hook to enforce a rule after the tool ran.

Actually wrong

If the rule says "don't refund more than 500" and you check after, the damage is done. The transaction is logged. Use PreToolUse to prevent; PostToolUse only for reactive actions like logging.

Looks right

Exit code 0 from a PreToolUse hook means valid; any other code means deny.

Actually wrong

Protocol: exit 0 = allow, exit 2 = deny, exit 1 = hook error (halts agent). Use exit 2 with stderr JSON for policy denial. Use exit 1 only for hook bugs.

Looks right

Set 'async': true on a PreToolUse hook to avoid blocking Claude.

Actually wrong

PreToolUse cannot be async: the tool is pending Claude's decision. Async only works on PostToolUse. Async PreToolUse is a configuration error.

Looks right

Use a regex matcher like 'process.*' to match all tools starting with 'process'.

Actually wrong

Matchers are literal pipe-separated strings: 'process_refund|process_charge'. No regex. To match many, list explicitly or use * for all. Regex patterns are silently ignored.

07 · Compare

Side-by-side

MechanismPreToolUse HookPostToolUse HookPromptDeterminism
When it runsBefore toolAfter toolEvery turn (vague)Pre > Post >> Prompt
Can block?Yes (exit 2)No, already doneModel might ignorePre = 100%, Post = 0%, Prompt = 70%
Use forPolicies, gates, validationLogging, normalizationStyle, toneNon-negotiable rules → Pre
Failure modeWrong matcher or exit codeSilent (exit 0 always)Variation under loadPre most robust
ReplaceabilityCannot use promptPrompt + hook redundantHooks replace weak promptsHook > Prompt for hard rules
Audit trailEach hook loggedEach hook loggedHidden in conversationPre/Post = strong audit
08 · When to use

Decision tree

01

Is the rule non-negotiable (legal, financial, security)?

YesPreToolUse hook. Hard no or no with structured error. Prompt-only fails under load.
NoConsider PostToolUse for normalization/logging, prompt for soft guidance.
02

Do you need to prevent the operation from happening?

YesMust be PreToolUse. PostToolUse cannot block.
NoPostToolUse for after-the-fact logging, transformation, cascade.
03

Is the rule based on exact data values (amount, status, ID)?

YesPreToolUse with deterministic checks. Query a database, compare numbers, exit 0 or 2.
NoIf subjective (sentiment, tone), use prompt with PostToolUse logging.
04

Do you have a blocklist or allowlist?

YesPreToolUse with pattern matching. Parse input, check list, exit 2 if blocked.
NoContext-dependent rule → PostToolUse logging.
05

Does the rule require real-time data (DB query, quota, rate limit)?

YesPreToolUse queries the data and decides before tool runs.
NoStatic rule → prompt + optional PostToolUse log.
09 · On the exam

Question patterns

SDK Hooks (Pre/PostToolUse) exam trap, painterly cautionary scene featuring Loop mascot.

61 V2 questions wired to this concept. Tap an answer to check it instantly — you'll see whether it's right and why — then expand the full breakdown for the mental model and all four rationales.

Your retriever subagent has [Read, Grep, Glob, WebSearch, Bash, Edit] and accidentally modified a config file. What was the design error?

Tap your answer to check it.

A refund agent uses prompt-only enforcement ("escalate refunds over $500") and 5 percent of refunds violate the policy in production. What is the fix?

Tap your answer to check it.

After a PreToolUse hook blocks a refund, the agent retries the same call on the next loop iteration. Why?

Tap your answer to check it.

A user says "speak to a manager." Should the agent negotiate first or escalate immediately?

Tap your answer to check it.

How does the escalation pattern differ between a customer-blocking workflow and a batch workflow?

Tap your answer to check it.

The system prompt says 'never refund more than 500'. Production telemetry shows a 3% violation rate. What is the architectural fix?

Tap your answer to check it.

55 additional questions for this concept live in the practice pillar. Take a mock exam ↗

10 · FAQ

Frequently asked

Difference between a hook and a prompt constraint?
Prompt: "Never refund more than 500." Model may ignore under load (3-7% violation rate). Hook: code that checks amount <= 500 before the tool runs; exit 0 (allow) or 2 (deny). 100% enforcement.
Can a PostToolUse hook prevent a bad call?
No. PostToolUse runs after the tool completes. The transaction already happened. Use PreToolUse to prevent; PostToolUse to react.
How does Claude know when a PreToolUse hook denies?
Hook exits 2 and writes JSON error to stderr. The SDK converts this into a tool_use_error block in Claude's response. Claude reads it and recovers.
Can a PreToolUse hook see message history?
No. Hooks receive only the tool call details. No message history, no prior context. Pass anything you need explicitly via tool_input.
What if the hook needs to query a slow database?
Hook execution blocks Claude, so keep it fast. Cache results (Redis), use a read-replica, or set a timeout. A 5-second hook blocks the entire agent.
Can multiple PreToolUse hooks fire on the same tool?
Yes. The SDK runs them in sequence. Each hook gets stdin from the previous (or original tool_input). If the first exits 2, subsequent hooks don't run.
Why absolute paths for hook commands?
Relative paths are vulnerable to path-interception attacks (MITRE T1574.007). Use /home/user/hooks/script.sh, not ./hooks/script.sh.
Difference between exit codes 0, 1, and 2?
Exit 0 = allow. Exit 2 = deny (expected for policy violation). Exit 1 = internal hook error (halts agent). Use exit 1 only for bugs.
Can I use 'async': true on PreToolUse?
No. PreToolUse cannot be async. Claude waits for the decision. Async only on PostToolUse.
How does PostToolUse send output back to Claude?
Writes to stdout. The SDK captures it and may show to Claude or include in logs. The hook's exit code only signals hook health, not tool success.
11 · Practice with AI

Work this with your AI

Work this concept hands-on with Claude Code, Codex, or claude.ai. Copy a prompt, paste it into your assistant, and practise in tandem. Each one keeps you active (explain it back, get drilled, or build) rather than just reading.

  • Drill it like the exam (scenario MCQs)
    Practice in the exam's scenario-MCQ format with trap awareness.
  • Explain it back (Feynman)
    Build durable, transferable understanding of a concept you can half-state.
  • Test me, adapting the difficulty
    Active recall practice on a concept you think you know.
  • Check my prerequisites first
    Before studying a concept that keeps not sticking.
  • Find the high-leverage 20%
    When a domain feels too big and you are short on time.
Self-check

Test yourself

Three diagnostic questions on this primitive. Reveal each answer when you have a guess. Want a full 60-question mock? Open the mock hub →

Q1System prompt says "never refund more than 500." Production shows 3% violation rate. What's the architectural fix?
Replace prompt with a PreToolUse hook checking amount <= 500. Exits 2 (deny) on violation. Prompt is probabilistic; hook is deterministic. The exam tests this Prompt-vs-Hook heuristic repeatedly.
Q2PostToolUse hook detects a policy violation and exits 2. Did the violation happen?
Yes. PostToolUse runs after the tool completes. The transaction already happened. Use PreToolUse to prevent operations; PostToolUse is for logging, normalizing, cascading after the fact.
Q3Hook exits with code 1 and the agent halts unexpectedly. Why?
Exit 1 = internal hook error (halts the agent). Exit 2 = deny (Claude reads stderr error, retries with adjusted args). Use exit 1 only for hook bugs (JSON parse failure, uncaught exception). For policy denial, always exit 2.
Last reviewed: 2026-05-04·Refresh cadence: monthly
D2.4 · D2 · Tool Design + Integration

SDK Hooks (Pre/PostToolUse), complete.

You've covered the full ten-section breakdown for this primitive, definition, mechanics, code, false positives, comparison, decision tree, exam patterns, and FAQ. One technical primitive down on the path to CCA-F.

More platforms →