CertSafari
    Anthropic Claude Certified Architect – Foundations (CCAR-F)· Lessons

    Domain 1 · Lesson 5/30

    Agent SDK Hooks: Intercept Tool Calls and Normalize Results

    Apply Agent SDK hooks for tool call interception and data normalization

    16 min read
    3.86% of exam
    4 sources
    Published 4 Oct 2026
    Docs as of 30 Sep 2026

    What you will be able to do

    • Place PreToolUse and PostToolUse hooks in the agent loop and explain what each one can change
    • Explain why a business rule that must always hold belongs in a hook rather than a prompt instruction
    • Write a PreToolUse gate that denies a policy-violating call, such as a refund over $500, and points Claude to an alternative workflow
    • Use PostToolUse with updatedToolOutput to replace inconsistent tool results before Claude reads them
    • Write matchers that target one MCP server's tools

    Key concept

    Tool-call interception hook — A hook is a callback in your own application that the SDK runs at a fixed point in every tool call. PreToolUse runs before the tool executes and can allow, deny, or rewrite the call. PostToolUse runs after the tool returns and can change what Claude sees. Because your code makes the decision, a rule in a hook is enforced whether or not the model would have followed it.

    1.Where hooks sit in the agent loop

    An SDK agent works in a loop. Claude reads the prompt and history and decides whether to call tools. The SDK runs those tools, and the results go back to Claude: "Each set of tool results feeds back to Claude for the next decision." The docs put hooks inside the tool-execution step. That makes them the one place where your code sits between Claude's request and the real side effect, and again between the tool's raw output and Claude's next decision.

    This lesson covers two events. PreToolUse fires when Claude requests a tool call; the SDK describes it as "Tool call request (can block or modify)". PostToolUse fires on the "Tool execution result". Neither one is optional or chosen by Claude. The hooks reference says they fire "on every tool call inside the agentic loop: PreToolUse and PostToolUse, except EndConversation calls, which skip both". Each callback receives a typed input. For tool events this includes tool_name and tool_input, plus the shared session_id, cwd, and hook_event_name. The SDK docs list what hooks are for, including blocking dangerous operations and the use that matters most here: "Transform inputs and outputs to sanitize data, inject credentials, or redirect file paths".

    Each time an event fires, the SDK handles it in the same order. It collects the hooks registered for that event, uses matchers to decide which ones apply, runs those callbacks, and then acts on the decision your callback returns.

    Checkpoint 1 of 9· Put it in order

    Put the SDK's hook resolution steps in order.

    1. 1.Callback functions execute
    2. 2.An event fires
    3. 3.Your callback returns a decision
    4. 4.Matchers filter which hooks run
    5. 5.The SDK collects registered hooks

    Sources123

    2.Deterministic hooks versus prompt instructions

    You can state a business rule in two ways. One is to write it in the system prompt: "never issue a refund above $500." The other is to enforce it in a hook. They work differently. A prompt instruction is input to the model's decision. In the agent loop, "Claude evaluates the current state and determines how to proceed", and an instruction is one thing Claude weighs while doing that. Most of the time it will follow the instruction. Nothing in the loop guarantees it will.

    A hook works differently on three counts. It fires on every matching tool call, so it is never skipped. It is ordinary code: the SDK docs note that "your callback runs in your application’s own process". And its decision is enforced, not suggested: "permissionDecision: 'deny' stops the tool call." A $500 threshold in a PreToolUse callback is a comparison your code runs. Model judgment plays no part in it.

    So the rule for choosing is this. If a business rule needs guaranteed compliance (refund caps, protected files, regulated actions), enforce it in a hook. Use the prompt for guidance where an occasional miss is acceptable. You can also use both: the prompt helps Claude make the right call the first time, and the hook stops the call if Claude doesn't. Note that the provided sources never use the words "deterministic" or "probabilistic". That framing is the exam guide's. The sources do supply the mechanics behind it: hooks fire on every call, run as your code, and enforce their decisions.

    Checkpoint 2 of 9· Check yourself

    A support agent must never execute a refund above $500. The team has added "Never refund more than $500" to the system prompt, and audits still find rare violations. What change gives a guarantee?

    Sources23

    3.PreToolUse: blocking policy-violating calls

    The SDK's reference example shows the pattern. The callback reads tool_input, checks it against a rule, and returns a hookSpecificOutput with a permissionDecision. If the call is fine, it returns an empty object. A refund gate has the same structure: read the amount from tool_input, compare it to 500, and deny if it is higher.

    A PreToolUse callback that denies a policy-violating call and allows everything else (Python SDK)python
    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 {}

    The empty object needs care. It means "no objection". It does not mean "approved". The hooks guide says the same about a command hook that exits 0: "For a PreToolUse hook this doesn’t approve the tool call: the normal permission flow still applies." Each value of permissionDecision has a different effect, so choose by what the rule needs. If the requirement is that the call must never run and no one should be prompted, the answer is deny, not ask.

    PreToolUse decisions and what each one does to the tool call
    Return valueEffect on the call
    permissionDecision: "allow"Skips the interactive permission prompt; deny and ask rules still apply
    permissionDecision: "deny"Cancels the tool call and sends permissionDecisionReason to Claude
    permissionDecision: "ask"Shows the permission prompt to the user as normal
    permissionDecision: "defer"Ends the query so you can resume it later
    {} (empty object)No decision; the normal permission flow applies

    Checkpoint 3 of 9· Match them up

    Match each permissionDecision value to its runtime effect.

    Tap a term, then the definition that fits it.

    Checkpoint 4 of 9· Exam question

    An architect registers three independent PreToolUse hooks for the same charge_card tool: one checks fraud signals, one checks the daily spending cap, and one checks account status. During a live call, the fraud-signal hook returns permissionDecision "deny" while the other two both return "allow". What happens to the tool call?

    Sources34

    4.Redirecting a blocked call to an alternative workflow

    A deny stops the call, but the agent still has a goal it is trying to reach. The denial tells the model what happened: "permissionDecisionReason tells the model why, so it avoids retrying." That makes the reason string more than an error message. It is your way of steering the agent toward the right path. A generic reason such as "Refund denied" gives Claude nothing to act on, and Claude may simply retry the same call. A useful reason says why the call was blocked and what to do instead. For example: "Refunds above $500 require human approval. Use the escalation tool with the order details." The hook remains the gate. The reason only shapes what Claude tries next.

    Your callback can also return the top-level systemMessage field. That one goes to the user, not the model: "systemMessage shows the user what happened." If escalation should pause the whole run rather than continue in the loop, use "defer". In the SDK's words, "If you return "defer", the query ends so you can resume it later."

    Blocking isn't the only way to intercept a call. PreToolUse can also return updatedInput together with allow, so the call goes ahead with corrected arguments. The SDK example below sends every Write into a sandbox path.

    Checkpoint 5 of 9· Fill the gap

    Which field rewrites the tool's arguments before the call executes?

    async def redirect_to_sandbox(input_data, tool_use_id, context):
        if input_data["hook_event_name"] != "PreToolUse":
            return {}
    
        if input_data["tool_name"] == "Write":
            original_path = input_data["tool_input"].get("file_path", "")
            return {
                "hookSpecificOutput": {
                    "hookEventName": input_data["hook_event_name"],
                    "permissionDecision": "allow",
                    " ? ": {
                        **input_data["tool_input"],
                        "file_path": f"/sandbox{original_path}",
                    },
                }
            }
        return {}

    Checkpoint 6 of 9· Exam question

    A PreToolUse hook needs to block refunds above $500 only when they target a specific merchant category, based on a field named category inside the refund tool's arguments. The hook is registered with matcher="refund_customer". Where should the category check be implemented?

    Sources3

    5.PostToolUse: normalizing tool results before Claude reads them

    The other direction matters too. An agent connected to several MCP servers often receives the same kind of data in different formats. One tool returns a Unix timestamp, another returns ISO 8601, and a third reports status as a numeric code where another uses a label. If those results reach Claude unchanged, Claude has to reconcile them on every turn, and it may not do it the same way each time. A PostToolUse hook can convert each result to one canonical format before Claude sees it.

    That is the main point of this section. The PostToolUse input includes the raw tool_response alongside tool_name and tool_input, so your callback can parse whatever the tool returned. To change what Claude reads, return updatedToolOutput: "To replace the tool’s output before Claude sees it, set updatedToolOutput, which works for any tool in both SDKs." A normalizer therefore parses tool_response, converts every timestamp to ISO 8601 and every numeric status code to its canonical label, and returns the result as updatedToolOutput. Claude only ever sees one format. The sources don't include a ready-made normalization callback. The steps above apply the documented fields to the exam guide's scenario.

    PostToolUse output fields: which ones change what Claude sees
    FieldWhat it does
    updatedToolOutputReplaces the tool's output before Claude sees it; works for any tool in both SDKs
    additionalContextAppends information to the tool result; the original output stays
    updatedMCPToolOutputOlder field that replaces MCP tool output only; deprecated

    If the normalizer needs to know what the original request was, use the tool use ID passed to every callback. It "correlates PreToolUse and PostToolUse events for the same tool call".

    Checkpoint 7 of 9· Check yourself

    Three MCP tools return dates as Unix seconds, ISO 8601 strings, and a custom format. You want Claude to only ever see ISO 8601. What should the PostToolUse hook return?

    Sources31

    6.Scoping hooks to the right tools with matchers

    Both kinds of hook need to fire on the right tools: the refund gate on the refund tool, the normalizer on the tools that return mixed formats. You choose those tools with the matcher in each HookMatcher. For tool events, the matcher is checked against the tool name. MCP tool names follow a fixed pattern: "MCP tools use the pattern mcp__<server>__<action>, where <server> is the key you use in the mcpServers configuration."

    Registering a PreToolUse callback with a matcher (Python SDK)python
    options = ClaudeAgentOptions(
        hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[my_callback])]}
    )

    How a matcher string is interpreted depends on which characters it contains. Letters, digits, _, -, spaces, commas, and | give exact matching. Any other character makes the matcher an unanchored JavaScript regular expression. A pattern like mcp__memory__.* therefore covers every tool on one server and nothing else.

    How matcher values are evaluated
    Matcher valueEvaluated asMatches
    "*", "", or omittedMatch allEvery occurrence of the event
    BashExact stringOnly the Bash tool
    Edit|WriteList of exact stringsEither Edit or Write
    mcp__memory__.*JavaScript regular expression, unanchoredEvery tool from the memory server

    Checkpoint 8 of 9· Check yourself

    Which matcher value is evaluated as a regular expression rather than as an exact tool name?

    Checkpoint 9 of 9· Exam question

    A team wants to send every tool call's arguments to an external audit service without slowing down the agent's response time, and this audit step has no bearing on whether the call is allowed to proceed. Which hook output pattern fits this requirement?

    Sources31

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 1.Returning an empty object (or exiting 0) from a PreToolUse hook approves the tool call.Why is that wrong?

      An empty result means no objection. The normal permission flow still decides, so only an explicit decision changes the outcome.

      Covered in PreToolUse: blocking policy-violating calls

    2. 2.The deny decision is all that matters, so the permissionDecisionReason can be generic.Why is that wrong?

      The reason is what the model reads after a deny. A vague reason invites Claude to retry, while a specific one points it to the alternative workflow.

      Covered in Redirecting a blocked call to an alternative workflow

    3. 3.A PostToolUse hook that returns decision "block" stops Claude from seeing a malformed tool result.Why is that wrong?

      After execution, "block" only adds a reason next to the result. Replacing the output takes updatedToolOutput.

      Covered in PostToolUse: normalizing tool results before Claude reads them

    4. 4.A firmly worded system-prompt rule enforces a hard business limit as reliably as a hook does.Why is that wrong?

      A prompt rule is something Claude weighs when it decides what to do. A PreToolUse deny is enforced by your code and stops the call regardless.

      Covered in Deterministic hooks versus prompt instructions

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 1.
      “on every tool call inside the agentic loop: PreToolUse and PostToolUse, except EndConversation calls, which skip both”
      ↩︎ Where hooks sit in the agent loop
      “adds the reason next to the tool result. Claude still sees the original output; to replace it, use updatedToolOutput”
      ↩︎ PostToolUse: normalizing tool results before Claude reads them
      “mcp__memory__.* matches every tool from the memory server”
      ↩︎ Scoping hooks to the right tools with matchers
      “JavaScript regular expression, unanchored”
      ↩︎ Scoping hooks to the right tools with matchers
      “adds the reason next to the tool result. Claude still sees the original output; to replace it, use updatedToolOutput”
      ↩︎ Exam trap 3
      “^Notebook matches any tool whose name starts with Notebook”
      ↩︎ Checkpoint
    2. 2.
      “Each set of tool results feeds back to Claude for the next decision.”
      ↩︎ Where hooks sit in the agent loop
      “Claude evaluates the current state and determines how to proceed.”
      ↩︎ Deterministic hooks versus prompt instructions
      “You can use hooks to intercept, modify, or block tool calls before they run.”
      ↩︎ Key concept
      “Claude evaluates the current state and determines how to proceed.”
      ↩︎ Exam trap 4
    3. 3.
      “Transform inputs and outputs to sanitize data, inject credentials, or redirect file paths”
      ↩︎ Where hooks sit in the agent loop
      “Tool call request (can block or modify)”
      ↩︎ Where hooks sit in the agent loop
      “Because your callback runs in your application’s own process”
      ↩︎ Deterministic hooks versus prompt instructions
      “permissionDecision: 'deny' stops the tool call.”
      ↩︎ Deterministic hooks versus prompt instructions
      “Return empty object to allow the operation”
      ↩︎ PreToolUse: blocking policy-violating calls
      “permissionDecisionReason tells the model why, so it avoids retrying.”
      ↩︎ Redirecting a blocked call to an alternative workflow
      “systemMessage shows the user what happened.”
      ↩︎ Redirecting a blocked call to an alternative workflow
      “If you return "defer", the query ends so you can resume it later.”
      ↩︎ Redirecting a blocked call to an alternative workflow
      “To replace the tool’s output before Claude sees it, set updatedToolOutput, which works for any tool in both SDKs.”
      ↩︎ PostToolUse: normalizing tool results before Claude reads them
      “For PostToolUse hooks, you can set additionalContext to append information to the tool result.”
      ↩︎ PostToolUse: normalizing tool results before Claude reads them
      “correlates PreToolUse and PostToolUse events for the same tool call.”
      ↩︎ PostToolUse: normalizing tool results before Claude reads them
      “MCP tools use the pattern mcp__<server>__<action>, where <server> is the key you use in the mcpServers configuration.”
      ↩︎ Scoping hooks to the right tools with matchers
      “permissionDecisionReason tells the model why, so it avoids retrying.”
      ↩︎ Exam trap 2
      “The SDK collects registered hooks”
      ↩︎ Checkpoint
      “The older updatedMCPToolOutput field replaces MCP tool output only and is deprecated.”
      ↩︎ Checkpoint
    4. 4.
      “show the permission prompt to the user as normal”
      ↩︎ PreToolUse: blocking policy-violating calls
      “For a PreToolUse hook this doesn’t approve the tool call: the normal permission flow still applies.”
      ↩︎ PreToolUse: blocking policy-violating calls
      “For a PreToolUse hook this doesn’t approve the tool call: the normal permission flow still applies.”
      ↩︎ Exam trap 1
      “cancel the tool call and send the reason to Claude”
      ↩︎ Checkpoint

    Spotted a mistake, or was something unclear? Tell us.