Skip to lesson

AnthropicClaude Certified Architect – Foundations2026-101.5 Apply Agent SDK hooks for tool call interception and data normalization

Reject prohibited calls before tools execute

Completed by the knowledge check

Learning objective

Use PreToolUse interception to deny policy-violating operations before their side effects occur.

Core idea

For a support agent that can issue refunds, the important boundary is the moment a proposed tool call becomes an executed operation. A prompt can describe the refund ceiling, but the execution path must reject a call that exceeds it. A PreToolUse hook places application code at that boundary in the Claude Agent SDK.

Return an explicit denial before the refund tool runs. A warning delivered after payment processing cannot prevent the payment. The same distinction applies to deleting a file, changing a customer subscription, or sending a message: policy belongs before the side effect it governs.

See 1.4.1 for prerequisite ordering and 1.4.2 for the evidence a sensitive operation requires. This lesson focuses on the hook interface that carries the blocking decision, rather than repeating those business checks.

How it works

Register the check against the actual tool name

Both the TypeScript and Python Agent SDKs support PreToolUse. Register the callback under the hooks option. TypeScript uses an array of matcher objects; Python uses HookMatcher objects inside ClaudeAgentOptions. A matcher filters tool names, not argument values. Inspect the amount inside the callback, rather than attempting to encode a monetary predicate in the matcher.

MCP tool names have the form mcp__<server>__<tool>, with the server segment taken from your mcpServers configuration in TypeScript or mcp_servers in Python. In an illustrative support application, a server named payments exposes a tool named refund, so the hook targets mcp__payments__refund. This is an application-defined tool, not an Anthropic refund API.

The callback input includes hook_event_name, tool_name, and tool_input. The SDK also passes a tool-use identifier for correlating the attempted call with later events. Check the event and tool identity before interpreting the input. The TypeScript input declares tool_input as unknown: a type assertion alone does not validate a monetary value.

Return the decision in the correct envelope

For PreToolUse, put permissionDecision inside hookSpecificOutput and set hookEventName to "PreToolUse". Set permissionDecision: "deny" to stop this tool call. Put the explanation in permissionDecisionReason, which Claude receives when the call is denied.

For a policy breach, the returned object can be this short:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Refund exceeds $500. Use the human-review workflow."
  }
}

The reason directs the agent toward another workflow; it does not itself submit a human-review request. If a review must actually be created, execute that separately through the application’s approved escalation path and verify its outcome. See 1.4.4 for what the escalation summary must preserve.

Returning {} makes no explicit permission decision and leaves the request in the normal permission flow. Use that result for a call that passes business policy when you still want access rules to determine whether execution is permitted. Returning "allow" is stronger: it approves at the hook layer, although deny and ask rules still apply. Do not turn a business check into broad access approval accidentally.

Make the predicate independent of the model’s explanation

Suppose the application permits USD refunds of at most $500 per operation. Its tool contract uses amount_minor as an integer count of cents and currency as a string. The hook rejects a request above 50,000 cents. It also rejects inputs that cannot establish a valid amount and currency, rather than accepting a phrase such as “manager approved” from the proposed arguments as authorization.

These fields and this threshold are application policy, not SDK defaults. The hook must use the same representation as the payment service. For the stated policy, 50,000 cents passes the ceiling check and 50,001 fails it. Whether a passing request satisfies eligibility, identity, and other conditions remains a separate check; see 1.4.2.

A denial should preserve the requested operation as denied. Silently rewriting $700 to $500 changes the customer’s transaction. The SDK offers updatedInput for legitimate input modification, but a policy breach is not automatically permission to substitute another amount. When you do use updatedInput, return the complete input object, including unchanged fields, because the replacement covers the entire input.

Wait for the enforcement result

An async callback function can await evidence and return a decision. That is different from returning an asynchronous hook output that lets execution continue immediately. Background hook outputs cannot block or modify the operation; reserve them for observational work such as logging.

All matching hooks run in parallel. One hook must not assume another has already validated the request or populated shared state. A denial takes precedence over the other hooks’ permission decisions, so an independent approval cannot override an explicit rejection.

Configure callback timeout with the matcher’s timeout field, measured in seconds. When omitted, the default is 600 seconds for PreToolUse and PostToolUse: a hung policy lookup can stall a refund call for ten minutes. On PreToolUse timeout, Claude Code cancels the callback, discards its output, and leaves the tool unexecuted while the turn continues with a timeout result. Before Claude Code v2.1.210, the timeout was reported as a user rejection, which made unattended sessions stop and wait for input. PostToolUse differs: its timeout retains the tool result.

Key distinctions and trade-offs

Deny, ask, and defer: "deny" rejects the operation; "ask" requests confirmation. "defer" ends the turn with stop_reason: "tool_deferred" and preserves the pending call. The caller resumes the session, and the same call fires PreToolUse again. This supports pausing for input through the caller’s interface, rather than rejecting the operation.

Claude Code honors "defer" only in non-interactive (-p) mode, which the Agent SDK uses. When Claude makes several tool calls in one turn, it ignores "defer" with a warning and continues through normal permissions. Decision precedence is deny > defer > ask > allow.

This refund policy requires denial and a separate review workflow. Deferral would preserve the same refund call for possible approval on resume; it does not make an over-limit automatic refund permissible. Reapply policy when a deferred call resumes, and do not treat a generic permission confirmation as business approval.

Call blocking versus session stopping: Rejecting one call lets the agent pursue a permitted alternative. The top-level continue output field (continue_ in Python) determines whether the agent keeps running; it does not replace the explicit tool permission decision.

Named-tool coverage versus broad coverage: A matcher of only letters, digits, _, -, |, , and spaces is compared as exact names, so mcp__payments__refund fires for that tool alone. Any other character makes it an unanchored JavaScript regular expression: mcp__payments__.* covers every tool on the server. An exact matcher keeps payment logic off unrelated tools, but requires you to account for every alternate tool that can perform the same payment. Removing an alternate route or enforcing the same predicate there is part of making the boundary complete.

Architecture / decision scenario

A customer requests a $700 refund. The support agent proposes mcp__payments__refund with USD and 70,000 cents. The registered PreToolUse callback evaluates the amount before the payment service executes and returns "deny" with a human-review instruction.

The tool invocation count remains zero and no refund record appears. The agent can gather evidence and request review through the approved escalation workflow. If it retries the refund with a different justification but the same amount, the hook rejects it again. If an unrelated hook returns "allow", the explicit denial still wins.

A $500 request clears this ceiling predicate but continues through access permissions and the other business prerequisites. If the policy lookup does not complete within the configured timeout, the refund remains unexecuted; the agent receives a timeout outcome instead of an approval. The decision is therefore tied to execution evidence, not to whether the final response promises compliance.

Common exam traps

  • Enforcing the ceiling with prompt instructions alone.
  • Returning a denial field at the top level instead of inside hookSpecificOutput.
  • Matching an amount with matcher, which filters tool names.
  • Treating a denial reason as proof that human review was submitted.
  • Rewriting a forbidden amount into a permitted payment without authorization.
  • Using a background hook output for a check that must finish before execution.
  • Writing the ceiling into the tool’s description, which Claude reads as guidance like any prompt.

Where the exam guide differs

Note:

Task Statement 1.5 asks for hooks that block policy-violating actions “and redirect to alternative workflows (e.g., human escalation).” PreToolUse has no redirect decision. A "deny" stops the call, and permissionDecisionReason tells Claude why, so it can choose the escalation tool on its next turn; updatedInput only rewrites the arguments of the same tool. In an exam item, the redirect is the denial whose reason names the alternative workflow, plus any escalation your own code performs.

What to remember

Use PreToolUse to evaluate the proposed operation before its side effects. Return hookSpecificOutput.permissionDecision: "deny" for a breach and a useful permissionDecisionReason. A passing business check can leave normal permissions intact; a rejected call must remain unexecuted.

Knowledge check

References