Skip to main content
Hooks are callback functions that run your code in response to agent events, like a tool being called, a session starting, or execution stopping. With hooks, you can:
  • Block dangerous operations before they execute, like destructive shell commands or unauthorized file access
  • Log and audit every tool call for compliance, debugging, or analytics
  • Transform inputs and outputs to sanitize data, inject credentials, or redirect file paths
  • Require human approval for sensitive actions like database writes or API calls
  • Track session lifecycle to manage state, clean up resources, or send notifications

How hooks work

1

An event fires

Something happens during agent execution and the SDK fires an event: a tool is about to be called (PreToolUse), a tool returned a result (PostToolUse), a subagent started or stopped, the agent is idle, or execution finished. See the full list of events.
2

The SDK collects registered hooks

The SDK checks for hooks registered for that event type. This includes callback hooks you pass in options.hooks and shell command hooks from settings files when the corresponding settingSources or setting_sources entry is enabled, which it is for default query() options.
3

Matchers filter which hooks run

If a hook has a matcher pattern (like "Write|Edit"), the SDK tests it against the event’s target (for example, the tool name). Hooks without a matcher run for every event of that type.
4

Callback functions execute

Each matching hook’s callback function receives input about what’s happening: the tool name, its arguments, the session ID, and other event-specific details.
5

Your callback returns a decision

After performing any operations (logging, API calls, validation), your callback returns an output object that tells the agent what to do: allow the operation, block it, modify the input, or inject context into the conversation.
The following example puts these steps together. It registers a PreToolUse hook (step 1) with a "Write|Edit" matcher (step 3) so the callback only fires for file-writing tools. When triggered, the callback receives the tool’s input (step 4), checks if the file path targets a .env file, and returns permissionDecision: "deny" to block the operation (step 5):
When you run either script, Claude attempts to create the .env file, the hook denies the tool call, and Claude’s final response explains that it can’t create .env files.

Available hooks

The SDK provides hooks for different stages of agent execution. Some hooks are available in both SDKs, while others are TypeScript-only.

Configure hooks

To configure a hook, pass it in the hooks field of your agent options (ClaudeAgentOptions in Python, the options object in TypeScript). This snippet assumes you have already defined a hook callback, like protect_env_files in Python or protectEnvFiles in TypeScript from the example above:
The hooks option is a dictionary in Python or an object in TypeScript, where:

Matchers

Use matchers to filter when your callbacks fire. The matcher field matches against a different value depending on the hook event type. For example, tool-based hooks match against the tool name, while Notification hooks match against the notification type. SDK matchers follow the same rules as matchers in settings files. That section documents the exact-string and regular-expression evaluation paths, their version requirements, and the matcher values for each event type. Use the matcher pattern to target specific tools whenever possible. A matcher with 'Bash' only runs for Bash commands, while omitting the pattern runs your callbacks for every occurrence of the event. Omit it on purpose to log every tool call your session makes.

Callback functions

Inputs

Every hook callback receives three arguments:
  • Input data: a typed object containing event details. Each hook type has its own input shape. For example, PreToolUseHookInput includes tool_name and tool_input, while NotificationHookInput includes message. See the full type definitions in the TypeScript and Python SDK references.
    • All hook inputs share session_id, cwd, and hook_event_name.
    • agent_id and agent_type are populated when the hook fires inside a subagent. In TypeScript, these are on the base hook input and available to all hook types. In Python, they are optional fields on PreToolUse, PostToolUse, PostToolUseFailure, and PermissionRequest, and required fields on SubagentStart and SubagentStop.
  • Tool use ID (str | None / string | undefined): correlates PreToolUse and PostToolUse events for the same tool call.
  • Context: in TypeScript, contains a signal property (AbortSignal) for cancellation. In Python, this argument is reserved for future use.

Outputs

Your callback returns an object with two categories of fields:
  • Top-level fields are accepted on every event: systemMessage shows a message to the user, and continue (continue_ in Python) determines whether the agent keeps running after this hook. Some events discard them or deliver them elsewhere. Each event’s section on the hooks page says where they land.
  • hookSpecificOutput controls the current operation. The fields you set inside depend on the hook event type:
    • For PreToolUse hooks, this is where you set permissionDecision ("allow", "deny", "ask", or "defer"), permissionDecisionReason, and updatedInput. If you return "defer", the query ends so you can resume it later.
    • For PostToolUse hooks, you can set additionalContext to append information to the tool result. To replace the tool’s output before Claude sees it, set updatedToolOutput, which works for any tool in both SDKs. The older updatedMCPToolOutput field replaces MCP tool output only and is deprecated.
    • In the TypeScript SDK, a PostToolUse callback can also return classifierContext, a short note about the tool call’s result for the auto mode permission classifier. Because your callback runs in your application’s own process, the classifier may weigh a user statement you relay in the note as user intent. The field requires TypeScript Agent SDK v0.3.236 or later. Annotate a result for the auto mode classifier covers the length cap, the synchronous-only rule, and what not to put in the note.
Return {} to allow the operation without changes. SDK callback hooks use the same JSON output format as Claude Code shell command hooks, which documents every field and event-specific option. For the SDK type definitions, see the TypeScript and Python SDK references.
When multiple hooks or permission rules apply, deny takes priority over defer, which takes priority over ask, which takes priority over allow. If any hook returns deny, the operation is blocked regardless of other hooks.

Asynchronous output

By default, the agent waits for your hook to return before proceeding. If your hook performs a side effect, such as logging or sending a webhook, and doesn’t need to influence the agent’s behavior, you can return an async output instead. This tells the agent to continue immediately without waiting for the hook to finish. In this snippet, send_to_logging_service in Python and sendToLoggingService in TypeScript stand in for any logging function you define:
Async outputs can’t block, modify, or inject context into the operation since the agent has already moved on. Use them only for side effects like logging, metrics, or notifications.

Examples

Several examples in this section show only the callback function. To run one, register the callback under the matching event in the hooks field of your options, as shown in Configure hooks.

Modify tool input

This example intercepts Write tool calls and rewrites the file_path argument to prepend /sandbox, redirecting all file writes to a sandboxed directory. The callback returns updatedInput with the modified path and permissionDecision: 'allow' to auto-approve the rewritten operation:
Pair updatedInput with permissionDecision: 'allow' to auto-approve the modified input, or permissionDecision: 'ask' to show it to the user. If you omit permissionDecision, the modified input still applies and flows through the normal permission evaluation. With 'defer', updatedInput is ignored. Always return a new object rather than mutating the original tool_input.
To confirm the redirect, set the prefix to a path you can write to, such as ./sandbox or /tmp/sandbox (macOS doesn’t allow creating a root-level /sandbox directory), then ask the agent to write a file: the Write tool’s result in the message stream names the path with your sandbox prefix rather than the one Claude requested.

Add context and block a tool

This example blocks writes to the /etc directory and explains why to both the model and the user:
  • permissionDecision: 'deny' stops the tool call.
  • permissionDecisionReason tells the model why, so it avoids retrying.
  • systemMessage shows the user what happened.

Auto-approve specific tools

By default, the agent may prompt for permission before using certain tools. This example auto-approves read-only filesystem tools (Read, Glob, Grep) by returning permissionDecision: 'allow', letting them run without user confirmation while leaving all other tools subject to normal permission checks:

Register multiple hooks

When an event fires, all matching hooks run in parallel. For permission decisions, the most restrictive result applies: a single deny blocks the tool call regardless of what the other hooks return. Because completion order is non-deterministic, write each hook to act independently rather than relying on another hook having run first. The example below registers three independent checks for every tool call:

Filter with multi-tool matchers

Use multi-tool matchers to share one callback across related tools. This example registers three matchers with different scopes:
  • A pipe-separated exact list (Write|Edit|NotebookEdit) triggers file_security_hook only for file modification tools.
  • A regex (^mcp__) triggers mcp_audit_hook for any MCP tool whose name starts with mcp__.
  • An omitted matcher triggers global_logger for every tool call regardless of name.

Track subagent activity

Use SubagentStop hooks to monitor when subagents finish their work. See the full input type in the TypeScript and Python SDK references. This example logs a summary each time a subagent completes:

Make HTTP requests from hooks

Hooks can perform asynchronous operations like HTTP requests. Catch errors inside your hook instead of letting them propagate, since an unhandled exception can interrupt the agent. This example sends a webhook after each tool completes, logging which tool ran and when. The hook catches errors so a failed webhook doesn’t interrupt the agent:
To confirm the hook fires, point the webhook URL at an endpoint you can watch and send a prompt that uses a tool: the hook sends a POST with the tool name and timestamp after each tool completes.

Forward notifications to Slack

Use Notification hooks to receive system notifications from the agent and forward them to external services. In SDK sessions, Claude Code runs this hook for the following notification types:
  • permission_prompt once a permission request has waited about six seconds on your canUseTool callback. Requires TypeScript Agent SDK v0.3.233 or later, or Python Agent SDK v0.2.139 or later
  • elicitation_complete and elicitation_response for user-prompt elicitation flows
Claude Code emits the other types, such as idle_prompt, auth_success, and elicitation_dialog, from interactive UI that SDK sessions don’t run. Each notification includes a message field with a human-readable description and optionally a title. This example forwards every notification to a Slack channel. It requires a Slack incoming webhook URL, which you create by adding an app to your Slack workspace and enabling incoming webhooks:
When a Notification event fires, the hook posts the notification’s message, prefixed with Agent status:, to the channel your webhook targets.

Fix common issues

Hook not firing

  • Verify the hook event name is correct and case-sensitive (PreToolUse, not preToolUse)
  • Check that your matcher pattern matches the tool name exactly
  • Ensure the hook is under the correct event type in options.hooks
  • For non-tool hooks that support matchers, like Notification and SubagentStop, matchers match against different fields, and Stop ignores matchers entirely (see matcher patterns)
  • Hooks may not fire when the agent hits the max_turns limit because the session ends before hooks can execute

Matcher not filtering as expected

Matchers only match tool names, not file paths or other arguments. To filter by file path, check tool_input.file_path inside your hook:

Hook timeout

Claude Code runs each callback with a timeout, which you set in seconds with the timeout field on its HookMatcher. When you don’t set one, Claude Code uses the event’s default: 600 seconds for most events, 30 seconds for UserPromptSubmit, and 10 seconds for MessageDisplay. Claude Code runs SessionEnd callbacks during shutdown under the shorter SessionEnd timeout budget, 1.5 seconds by default. When a callback exceeds its timeout, Claude Code cancels it and treats it as a failed hook: it discards the callback’s output and the session continues rather than hanging. What happens next depends on the event:
  • PreToolUse: Claude Code doesn’t run the tool call, Claude receives a tool result stating the hook didn’t respond before its timeout, and the turn continues. If another PreToolUse hook returned an explicit deny, Claude receives that denial instead of the timeout error. Before v2.1.210, Claude Code reported the timeout to Claude as a user rejection, which made unattended sessions stop and wait for input.
  • PostToolUse and PostToolUseFailure: Claude Code keeps the tool result and the turn continues.
  • UserPromptSubmit and UserPromptExpansion: Claude Code blocks the prompt with a message naming the hook and the timeout, and the session continues. Because a callback on these events can act as a policy gate, Claude Code never lets a timed-out prompt through unscreened. Before v2.1.208, Claude Code ended the query with error_during_execution when a callback on these events timed out.
  • Stop and SubagentStop: Claude Code shows a warning and the agent stops normally.
  • Other events, such as Notification and PreCompact: Claude Code logs the failure and continues.
If you interrupt the query while a callback is pending, Claude Code cancels the pending tool call. Before v2.1.208, the tool call could still proceed if you interrupted during a pending PreToolUse callback. If your callback needs more time, set a higher timeout on its HookMatcher. In TypeScript, use the AbortSignal from the third callback argument to handle cancellation gracefully when the timeout fires.

Tool blocked unexpectedly

  • Check all PreToolUse hooks for permissionDecision: 'deny' returns
  • Add logging to your hooks to see what permissionDecisionReason they’re returning
  • Verify matcher patterns aren’t too broad: an empty matcher matches all tools

Modified input not applied

  • Ensure updatedInput is inside hookSpecificOutput, not at the top level:
  • Don’t pair updatedInput with permissionDecision: 'defer', which drops the modified input. Omitting permissionDecision is fine: the modified input still applies through the normal permission evaluation. You can also return 'allow' to auto-approve the modified input or 'ask' to show it to the user for approval
  • Include hookEventName in hookSpecificOutput to identify which hook type the output is for

Session hooks not available in Python

SessionStart and SessionEnd can be registered as SDK callback hooks in TypeScript, but aren’t available in the Python SDK because its HookEvent type omits them. In Python, they are only available as shell command hooks defined in settings files such as .claude/settings.json. To load shell command hooks from your SDK application, include the appropriate setting source with setting_sources or settingSources:
To run initialization logic as a Python SDK callback instead, use the first message from client.receive_response() as your trigger.

Subagent permission prompts multiplying

When spawning multiple subagents, each one may request permissions separately for its own tool calls. To avoid repeated prompts, use PreToolUse hooks to auto-approve specific tools, or configure permission rules, which subagents inherit from the parent conversation.

Recursive hook loops with subagents

A UserPromptSubmit hook that spawns subagents can create infinite loops if those subagents trigger the same hook. To prevent this:
  • Check for a subagent indicator in the hook input before spawning
  • Use a shared variable or session state to track whether you’re already inside a subagent
  • Scope hooks to only run for the top-level agent session

systemMessage not appearing in output

The systemMessage field shows a message to the user, not the model. On Claude Code v2.1.227 or later, a hook’s systemMessage can surface in the message stream as an SDKInformationalMessage. Whether it does depends on the event. Each event’s section on the hooks page says how output surfaces. To pass context to the model instead, return additionalContext. Before v2.1.227, the SDK surfaced hook output in the message stream only for SessionStart and Setup hooks. For any other event, the output appeared only in the lifecycle events that includeHookEvents (include_hook_events in Python) adds. That option’s entry covers which lifecycle events each hook event produces. If you need to surface hook decisions to your application reliably, log them separately or use a dedicated output channel.