CertSafari
    CLAUDE-CERTIFIED-ARCHITECT-FOUNDATIONS-CCAR-F · Lessons

    Domain 1 · Lesson 5/30

    PreToolUse Hooks: Blocking Policy-Violating Tool Calls in the Agent SDK

    Apply Agent SDK hooks for tool call interception and data normalization

    15 min read
    3.86% of exam
    5 sources
    Published 28 Sep 2026
    Docs as of 27 Sep 2026

    What you will be able to do

    • Explain why a hook gives a guaranteed business-rule gate and a prompt instruction does not
    • Write and register a PreToolUse callback that denies a tool call and gives a reason
    • Design a block that sends the agent to another workflow, such as human escalation, when an action like a refund over $500 breaks policy
    • Scope a hook to the right tools with a matcher, and know what the matcher can and cannot express
    • Choose a hook over prompt-based enforcement when a business rule requires guaranteed compliance, and a prompt when probabilistic compliance is acceptable
    • Write a PostToolUse hook that intercepts tool results and normalizes heterogeneous data formats (Unix timestamps, ISO 8601 strings, numeric status codes) from different MCP tools before the model processes them

    Key concept

    Deterministic tool-call interception — A hook is your own code that the SDK runs at a fixed point in the agent loop, such as just before a tool executes. Its decision applies whatever the model intended, so a business rule enforced there holds on every call. A rule written in a prompt holds only as often as the model chooses to follow it.

    1.Guaranteed compliance versus probable compliance

    Suppose a support agent may issue refunds, and the business rule is that no refund above $500 ever goes out without a human. You could enforce this in two places. One is the instructions: a system prompt or CLAUDE.md line such as "never refund more than $500." The other is code that inspects each refund call before it runs.

    The SDK's feature guide treats these as different tools for different jobs. Project conventions go in CLAUDE.md. Deterministic logic on tool calls (audit, block, transform) goes in hooks. An instruction shapes what the model is likely to do, but the model still writes every tool call itself. On most turns it follows the instruction, but nothing stops the one turn where a long conversation, a persuasive customer or an ambiguous amount leads it to call the tool anyway. A hook doesn't depend on the model's judgement. It sees the actual call and returns a decision.

    That is the distinction between deterministic guarantees and probabilistic compliance. Prompt-based enforcement is probabilistic: it raises the odds of the right behaviour but cannot make it certain. A hook is deterministic: the same input always produces the same decision, and the model cannot talk its way past it. So the rule for choosing is simple. When a business rule requires guaranteed compliance, put it in a hook. When an occasional miss is tolerable, such as a tone preference or a formatting habit, a prompt instruction is enough and cheaper to maintain.

    Where the SDK feature guide places each kind of guidance
    MechanismFeature-guide purposeWhat it can guarantee
    CLAUDE.md (loaded via settingSources)Set project conventions your agent always followsNothing at call time: the model reads the convention and decides each tool call itself
    Hooks (hooks parameter or settings-file hooks)Run deterministic logic on tool calls (audit, block, transform)Your code sees every matched call and can stop it before it executes

    Hooks intercept in both directions, and the guarantee argument applies to both. PreToolUse hook patterns intercept outgoing tool calls, so they enforce compliance rules such as blocking a refund above a threshold. PostToolUse hook patterns intercept tool results after the tool has run, so they can transform the data before the model processes it. The hooks page lists transforming inputs and outputs to sanitize data among the things hooks are for. A prompt that says "treat a Unix timestamp as a date" is a probabilistic fix for messy tool output; a PostToolUse hook that rewrites the timestamp is a deterministic one. The rest of this lesson covers the outgoing side first, then the result side.

    Sources12

    2.Blocking a call with a PreToolUse hook

    PreToolUse is the hook event for outgoing tool calls. In the hooks reference it fires before a tool call executes, and it can block that call. The SDK's event table describes its trigger as a tool call request that the hook can block or modify. Listed uses include blocking dangerous operations before they execute and requiring human approval for sensitive actions.

    The callback receives the call's details in its input: tool_name, tool_input (the arguments the model chose) and tool_use_id. It returns a decision. The official example below blocks writes to .env files. A refund-limit rule has the same shape: read the amount from tool_input, compare it to the threshold, and deny when it is too high.

    A PreToolUse callback that reads tool_input and returns a deny decision with a reasonpython
    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 decision sits under hookSpecificOutput. It echoes the event name, gives permissionDecision: "deny", and adds a human-readable permissionDecisionReason. A deny means the call does not run. The empty return is subtler than the comment suggests. In the hooks reference, a hook that gives no decision leaves the normal permission flow in charge. An empty object means "this hook has no objection." It does not approve the call over any other permission check.

    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?

    Sources32

    3.Denying a call and sending the agent to escalation

    Blocking is only half the job. The exam guide's example is a refund above $500 that must be blocked and sent to another workflow, such as human escalation. If the agent sees only a blocked call, it may try again or give up and leave the customer stranded.

    The SDK gives you two practical ways to redirect. First, give the agent a sanctioned path, such as a separate escalation tool or a human-approval step. The hooks docs name human approval for sensitive actions as a core hook use. Second, make permissionDecisionReason explain what to do instead, not just "denied." A reason that names the approved alternative tells the agent there is a next step. A generic reason tells it nothing about why the call failed or what else to try. (These doc excerpts show the reason field but do not describe exactly how it is shown to the model. Check the full hooks reference before relying on specific wording.)

    Keep the threshold check in the hook, not in the reason text or the prompt. The hook remains the gate: however the model reacts to the reason, a refund over the limit cannot execute. Better wording only reduces wasted retries. The guarantee comes from the deny.

    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?

    Sources2

    4.Registering the hook and scoping it with a matcher

    A callback does nothing until you register it. Registration pairs an event name with one or more matcher groups. Each group holds a matcher pattern and the callbacks to run when the pattern matches. The Python reference describes the matcher as a tool name or pattern to match, with examples like Bash and Write|Edit. The same HookMatcher also takes an optional timeout in seconds.

    Registering the callback for PreToolUse, filtered to Write and Edit callspython
        options = ClaudeAgentOptions(
            hooks={
                # Register the hook for PreToolUse events
                # The matcher filters to only Write and Edit tool calls
                "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
            }
        )

    For a compliance rule, choose the matcher carefully. Too broad, and the callback runs on unrelated tools. Too narrow, and a refund tool you add later slips past the rule. In the TypeScript SDK, the hook input also carries an optional mcp_server provenance field, so a callback can check which MCP server a tool came from before applying a server-specific rule. These excerpts do not document the naming scheme MCP tools use in matcher patterns. Look it up in the full hooks reference rather than guessing.

    One more property matters for guarantees: programmatic hooks also fire inside subagents. The input carries agent_id and agent_type so you can tell which agent made the call. Routing a refund through a subagent does not get around the rule.

    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?

    Sources41

    5.Normalizing tool results with a PostToolUse hook

    PreToolUse guards what goes out. PostToolUse handles what comes back. The hooks reference says PostToolUse fires after a tool call succeeds, and the SDK's event table gives its trigger as the tool execution result. Among the listed hook uses is transforming inputs and outputs to sanitize data. That is the pattern for data normalization: the tool has already run, its raw result is in the hook's hands, and your code can reshape that result before the model processes it.

    Why you would want this: an agent wired to several MCP servers receives heterogeneous data formats. A billing tool returns created_at as a Unix timestamp such as 1717000000. A CRM tool returns the same kind of field as an ISO 8601 string. A shipping tool reports status as numeric status codes, where 2 means shipped, while an inventory tool uses words. You could explain all of this in the system prompt and hope the model converts correctly every time. That is probabilistic compliance again, and a misread code becomes a wrong answer to a customer. A PostToolUse hook that converts every timestamp to ISO 8601 and maps every numeric status code to its label is deterministic. The model only ever sees one format, so it never has to guess.

    The input a Python PostToolUse callback receives; tool_response holds the raw result to normalizepython
    class PostToolUseHookInput(BaseHookInput):
        hook_event_name: Literal["PostToolUse"]
        tool_name: str
        tool_input: dict[str, Any]
        tool_response: Any
        tool_use_id: str
        agent_id: NotRequired[str]
        agent_type: NotRequired[str]
    PostToolUse input fields and their role in a normalizer
    FieldDocumented meaningUse in a normalizer
    tool_nameName of the tool that was executedPick the converter for that tool's format
    tool_inputInput parameters that were usedRecover context the result omits, such as the requested currency
    tool_responseResponse from the tool executionThe raw data to rewrite: timestamps, dates, status codes
    mcp_serverMcpServerProvenance (TypeScript SDK, optional)Branch on which MCP server produced the result
    agent_idSubagent identifier, present when the hook fires inside a subagentConfirms results inside subagents are normalized too

    Registration mirrors the PreToolUse example: the same HookMatcher shape, keyed under PostToolUse, with a matcher that names the MCP tools whose output needs cleaning. Inside the callback, read tool_name to choose the converter, then rewrite tool_response into the canonical shape. In the TypeScript SDK the input also carries mcp_server and duration_ms, so a hook can branch on the server rather than on individual tool names.

    One honest caveat. These excerpts document the input a PostToolUse callback receives and state that hooks transform outputs, but they do not show the exact output field that replaces the tool result. Look up that field name in the full hooks reference before you ship a normalizer. The design is unchanged either way: normalize in the hook, not in the prompt.

    Do not confuse the two events. A PostToolUse hook runs after the tool call has succeeded, so it cannot undo a refund or a file write. Money rules belong in PreToolUse. Data-shape rules belong in PostToolUse.

    Sources3245

    Exam traps

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

    1. 1.A firmly worded system prompt or CLAUDE.md rule ("NEVER refund over $500") is enough when compliance must be guaranteed.Why is that wrong?

      Instructions set conventions the model usually follows, but the model still makes each call. Only a hook runs deterministic logic on the call itself and can block it.

      Covered in Guaranteed compliance versus probable compliance

    2. 2.Returning an empty object from a PreToolUse hook explicitly approves the call and skips any other permission checks.Why is that wrong?

      Giving no decision means the hook does not object. The normal permission flow still decides whether the call runs.

      Covered in Blocking a call with a PreToolUse hook

    3. 3.A PostToolUse hook is the right place to block a policy-violating action, because it can inspect the full result before deciding.Why is that wrong?

      PostToolUse fires after the tool call has succeeded, so the side effect has already happened. It can transform or log the result, but only PreToolUse fires before execution and can block the call.

      Covered in Normalizing tool results with a PostToolUse hook

    Sources

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

    1. 1.
      “Set project conventions your agent always follows”
      ↩︎ Guaranteed compliance versus probable compliance
      “Run deterministic logic on tool calls (audit, block, transform)”
      ↩︎ Guaranteed compliance versus probable compliance
      “These also fire inside subagents.”
      ↩︎ Registering the hook and scoping it with a matcher
      “Run deterministic logic on tool calls (audit, block, transform)”
      ↩︎ Key concept
      “Run deterministic logic on tool calls (audit, block, transform)”
      ↩︎ Exam trap 1
    2. 2.
      “Transform inputs and outputs to sanitize data, inject credentials, or redirect file paths”
      ↩︎ Guaranteed compliance versus probable compliance
      “Tool call request (can block or modify)”
      ↩︎ Blocking a call with a PreToolUse hook
      “Block dangerous operations before they execute, like destructive shell commands or unauthorized file access”
      ↩︎ Blocking a call with a PreToolUse hook
      “Require human approval for sensitive actions like database writes or API calls”
      ↩︎ Denying a call and sending the agent to escalation
      “Transform inputs and outputs to sanitize data, inject credentials, or redirect file paths”
      ↩︎ Normalizing tool results with a PostToolUse hook
    3. 3.
      “Before a tool call executes. Can block it”
      ↩︎ Blocking a call with a PreToolUse hook
      “no decision; normal permission flow applies”
      ↩︎ Blocking a call with a PreToolUse hook
      “no decision; normal permission flow applies”
      ↩︎ Exam trap 2
      “After a tool call succeeds”
      ↩︎ Exam trap 3
    4. 5.
      “the tool's response is checked before it's sent back to the model”
      ↩︎ Normalizing tool results with a PostToolUse hook