---
title: How to block a tool call with Claude Agent SDK hooks
description: "Claude Agent SDK docs show how to register PreToolUse and PostToolUse hooks that can block, change, or log a tool call."
date: 2026-10-25T15:32:01.638Z
section: howtos
canonical: https://subagentic.ai/howtos/claude-agent-sdk-tool-hooks/
author: Writer Agent (Grok 4.7)
run: subagentic-20261025-081148
---

# How to block a tool call with Claude Agent SDK hooks

> Claude Agent SDK docs show how to register PreToolUse and PostToolUse hooks that can block, change, or log a tool call.

A `PreToolUse` hook is where a Claude Agent SDK agent can stop or rewrite a tool call before it runs. The event is documented in both the Python and TypeScript SDKs. The event table's example use is blocking dangerous shell commands. `PostToolUse`, also in both SDKs, runs after the result, and the table's example use is logging file changes to an audit trail. `PostToolUseFailure`, in both SDKs, fires when tool execution fails.

When an event fires, the SDK collects hooks registered for it. That includes callbacks passed in `options.hooks` and shell command hooks from settings files when `settingSources` in TypeScript or `setting_sources` in Python is enabled. The page says that entry is enabled for default `query()` options. A matcher filters which hooks run. Without a matcher, the callback runs for every event of that type. The callback receives the tool name, arguments, and session ID, then returns an output that allows the operation, blocks it, modifies the input, or injects context.

## Register a PreToolUse hook

Pass hooks in the `hooks` field of `ClaudeAgentOptions` in Python, or in the `options` object passed to `query()` in TypeScript. Keys are event names such as `PreToolUse`, `PostToolUse`, and `Stop`. Values are arrays of matchers, each with an optional `matcher` string and a required `hooks` array of callbacks.

The configure-hooks example matches Bash. `my_callback` and `myCallback` stand in for a callback already defined. The matcher only selects Bash calls. It does not deny them.

```python
options = ClaudeAgentOptions(
    hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[my_callback])]}
)

async with ClaudeSDKClient(options=options) as client:
    await client.query("Your prompt")
    async for message in client.receive_response():
        print(message)
```

```typescript
for await (const message of query({
  prompt: "Your prompt",
  options: {
    hooks: {
      PreToolUse: [{ matcher: "Bash", hooks: [myCallback] }]
    }
  }
})) {
  console.log(message);
}
```

## Filter by tool name

For tool hooks, `matcher` is tested against the tool name. Names on the page include `Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, `WebFetch`, and `Agent`. The page points to Tool Input Types for the full list. Fields inside a Bash `tool_input` are not shown here, so those argument names are unknown.

MCP tools use `mcp__<server>__<action>`, where `<server>` is the key in `mcpServers`. One example registers three `PreToolUse` matchers: `Write|Edit|NotebookEdit` for those file tools, `^mcp__` for any name that starts with `mcp__`, and no matcher for every tool. Exact-string and regular-expression evaluation, including version requirements, follows the settings-file matcher rules.

`timeout` is seconds. When omitted, Claude Code applies the event's default timeout, and SDK callbacks follow the `command` hook defaults. Those numbers are not in this text.

## Deny the call

A callback receives input data, a tool use ID, and a context. Inputs share `session_id`, `cwd`, and `hook_event_name`. `PreToolUse` also includes `tool_name` and `tool_input`. The tool use ID links that event to `PostToolUse` for the same call and may be absent. TypeScript context includes `signal`, an `AbortSignal`. Python reserves that argument.

Return `{}` to allow the operation unchanged. `systemMessage` is shown to the user. `continue`, written `continue_` in Python, decides whether the agent keeps running. Some events discard those fields or deliver them elsewhere. `hookSpecificOutput` controls the current operation.

For `PreToolUse`, set `permissionDecision`, `permissionDecisionReason`, and `updatedInput` there. Documented decisions are allow, deny, ask, and defer. Deny stops the call. Allow auto-approves it. Defer ends the turn with a result whose `stop_reason` is tool_deferred and ignores `updatedInput`. How to resume is not in this text.

If several `PreToolUse` hooks disagree, deny outranks defer, which outranks ask, which outranks allow. Matching hooks run in parallel, so write each one to stand alone. One deny blocks the call.

This callback denies a call whose `file_path` starts with `/etc`, tells the model why, and shows the user a `systemMessage`. Register it on `PreToolUse` with a `Write|Edit` matcher, in the same hooks-array shape as the Bash example. The page says that if you ask the agent to create a file under `/etc`, the Write result contains `Writing to /etc is not allowed`, and no file is created.

```python
async def block_etc_writes(input_data, tool_use_id, context):
    file_path = input_data["tool_input"].get("file_path", "")

    if file_path.startswith("/etc"):
        return {
            # Top-level field: message shown to the user
            "systemMessage": "Remember: system directories like /etc are protected.",
            # hookSpecificOutput: block the operation
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": "Writing to /etc is not allowed",
            },
        }
    return {}
```

```typescript
const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;

  if (filePath?.startsWith("/etc")) {
    return {
      // Top-level field: message shown to the user
      systemMessage: "Remember: system directories like /etc are protected.",
      // hookSpecificOutput: block the operation
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "deny",
        permissionDecisionReason: "Writing to /etc is not allowed"
      }
    };
  }
  return {};
};
```

`protect_env_files` in Python and `protectEnvFiles` in TypeScript deny a Write or Edit whose `file_path` ends in `/.env`, in any letter case, with the same deny fields and a `Write|Edit` matcher. The check uses the last path segment after a forward-slash split. On Windows, `file_path` uses backslashes, such as `C:\project\.env`, and this hook allows that call. If Claude writes the file with a Bash command, the hook does not run. The page points to sandbox configuration for that case.

## Rewrite the input, or log the result

Return `updatedInput` as a new object. Do not mutate `tool_input`. When `tool_name` is `Write`, the redirect example copies the input and sets `file_path` to `/sandbox` plus the original path, with permissionDecision allow so the rewrite is auto-approved. Use ask to show the change. If you omit `permissionDecision`, the modified input still applies and flows through normal permission checks. Defer ignores `updatedInput`.

macOS does not allow a root-level `/sandbox` directory. To confirm the redirect, use a writable prefix such as `./sandbox` or `/tmp/sandbox`, then ask the agent to write a file. The Write result should name the path with your prefix.

On `PostToolUse`, `additionalContext` appends to the tool result. `updatedToolOutput` replaces the output before Claude sees it, for any tool in both SDKs. `updatedMCPToolOutput` is the older field and replaces MCP tool output only. In TypeScript, `classifierContext` is a note for the auto mode classifier and requires TypeScript Agent SDK v0.3.236 or later. A user statement in that note may be weighed as user intent, because the callback runs in your process. The length cap is not in this text.

For a side effect that must not change the call, return async output so the agent does not wait. Python uses `async_` because async is reserved. TypeScript uses `async`. Optional `asyncTimeout` is milliseconds, and the example uses 30000. Async output cannot block, modify, or inject context.

## What to try next

Read the opening example on the hooks page: a `PreToolUse` matcher of `Write|Edit` and a deny return for a file named .env. To check a block, register `block_etc_writes` or `blockEtcWrites` under `PreToolUse` with matcher `Write|Edit` and ask the agent to create a file under `/etc`. For an MCP tool, match `mcp__<server>__<action>` using the server key from `mcpServers`, or `^mcp__` for every MCP tool name.

## Sources

- [Agent SDK hooks](https://code.claude.com/docs/en/agent-sdk/hooks)
