Chapter 14

Control Execution with Hooks

Intercept and customize agent behavior at key execution points

Hooks let you intercept agent execution at key points to add validation, logging, security controls, or custom logic. With hooks, you can:

Block dangerous operations

Prevent destructive shell commands or unauthorized file access before they execute

Log and audit

Track every tool call for compliance, debugging, or analytics

Transform inputs/outputs

Sanitize data, inject credentials, or redirect file paths

Require human approval

Gate sensitive actions like database writes or API calls

Track session lifecycle

Manage state, clean up resources, or send notifications

A hook has two parts:

1

The callback function

The logic that runs when the hook fires

2

The hook configuration

Tells the SDK which event to hook into and which tools to match

The following example blocks the agent from modifying .env files. First, define a callback that checks the file path, then pass it to query() to run before any Write or Edit tool call:

protect-env.ts
import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

// Define a hook callback with the HookCallback type
const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
  // Cast input to the specific hook type for type safety
  const preInput = input as PreToolUseHookInput;

  // Extract the file path from the tool's input arguments
  const filePath = preInput.tool_input?.file_path as string;
  const fileName = filePath?.split('/').pop();

  // Block the operation if targeting a .env file
  if (fileName === '.env') {
    return {
      hookSpecificOutput: {
        hookEventName: input.hook_event_name,
        permissionDecision: 'deny',
        permissionDecisionReason: 'Cannot modify .env files'
      }
    };
  }

  // Return empty object to allow the operation
  return {};
};

for await (const message of query({
  prompt: "Update the database configuration",
  options: {
    hooks: {
      // Register the hook for PreToolUse events
      // The matcher filters to only Write and Edit tool calls
      PreToolUse: [{ matcher: 'Write|Edit', hooks: [protectEnvFiles] }]
    }
  }
})) {
  console.log(message);
}

This is a PreToolUse hook. It runs before the tool executes and can block or allow operations based on your logic. The rest of this guide covers all available hooks, their configuration options, and patterns for common use cases.

Available hooks

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

Hook EventPythonTypeScriptTriggerExample
PreToolUseTool call request (can block or modify)Block dangerous shell commands
PostToolUseTool execution resultLog all file changes to audit trail
PostToolUseFailureTool execution failureHandle or log tool errors
UserPromptSubmitUser prompt submissionInject additional context into prompts
StopAgent execution stopSave session state before exit
SubagentStartSubagent initializationTrack parallel task spawning
SubagentStopSubagent completionAggregate results from parallel tasks
PreCompactConversation compaction requestArchive full transcript before summarizing
PermissionRequestPermission dialog would be displayedCustom permission handling
SessionStartSession initializationInitialize logging and telemetry
SessionEndSession terminationClean up temporary resources
NotificationAgent status messagesSend agent status updates to Slack

Common use cases

Hooks are flexible enough to handle many different scenarios. Here are some of the most common patterns organized by category.

  • Block dangerous commands (like rm -rf /, destructive SQL)
  • Validate file paths before write operations
  • Enforce allowlists/blocklists for tool usage

Configure hooks

To configure a hook for your agent, pass the hook in the options.hooks parameter when calling query():

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

The hooks option is a dictionary (Python) or object (TypeScript) where:

  • Keysare hook event names (e.g., 'PreToolUse', 'PostToolUse')
  • Valuesare arrays of matchers, each containing an optional filter pattern and your callback functions

Matchers

Use matchers to filter which tools trigger your callbacks:

OptionTypeDefaultDescription
matcherstringundefinedRegex pattern to match tool names
hooksHookCallback[]-Required. Array of callback functions
timeoutnumber60Timeout in seconds

Discovering tool names

Built-in tools: Check the tools array in the initial system message, or add a hook without a matcher to log all tool calls.

MCP tools: Always start with mcp__ followed by the server name and action: mcp__<server>__<action>

This example uses a matcher to run a hook only for file-modifying tools:

matcher.ts
const options = {
  hooks: {
    PreToolUse: [
      { matcher: 'Write|Edit', hooks: [validateFilePath] }
    ]
  }
};

Callback function inputs

Every hook callback receives three arguments:

1

Input data (dict / HookInput)

Event details including hook type, session ID, tool name, and tool inputs

2

Tool use ID (str | None / string | null)

Correlate PreToolUse and PostToolUse events

3

Context (HookContext)

In TypeScript, contains a signal property (AbortSignal) for cancellation

log-tools.ts
const logToolCalls: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name === 'PreToolUse') {
    const preInput = input as PreToolUseHookInput;
    console.log(`Tool: ${preInput.tool_name}`);
    console.log(`Input:`, preInput.tool_input);
  }
  return {};
};

Callback outputs

Your callback function returns an object that tells the SDK how to proceed. Return an empty object {} to allow the operation without changes.

Top-level fields

FieldTypeDescription
continuebooleanWhether the agent should continue (default: true)
stopReasonstringMessage shown when continue is false
suppressOutputbooleanHide stdout from the transcript
systemMessagestringMessage injected into the conversation

Fields inside hookSpecificOutput

FieldTypeDescription
hookEventNamestringRequired. Use input.hook_event_name
permissionDecision'allow' | 'deny' | 'ask'Controls whether the tool executes
permissionDecisionReasonstringExplanation shown to Claude
updatedInputobjectModified tool input (requires allow)

This example blocks write operations to the /etc directory while injecting a system message:

block-etc.ts
const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
  const filePath = (input as PreToolUseHookInput).tool_input?.file_path as string;

  if (filePath?.startsWith('/etc')) {
    return {
      // Top-level field: inject guidance into the conversation
      systemMessage: 'Remember: system directories like /etc are protected.',
      // hookSpecificOutput: block the operation
      hookSpecificOutput: {
        hookEventName: input.hook_event_name,
        permissionDecision: 'deny',
        permissionDecisionReason: 'Writing to /etc is not allowed'
      }
    };
  }
  return {};
};

Permission decision flow

When multiple hooks or permission rules apply, the SDK evaluates them in this order:

1. Deny
2. Ask
3. Allow
4. Default to Ask

If any hook returns deny, the operation is blocked—other hooks returning allow won't override it.

Block a tool

Return a deny decision to prevent tool execution:

block-dangerous.ts
const blockDangerousCommands: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name !== 'PreToolUse') return {};

  const command = (input as PreToolUseHookInput).tool_input.command as string;

  if (command?.includes('rm -rf /')) {
    return {
      hookSpecificOutput: {
        hookEventName: input.hook_event_name,
        permissionDecision: 'deny',
        permissionDecisionReason: 'Dangerous command blocked: rm -rf /'
      }
    };
  }
  return {};
};

Modify tool input

Return updated input to change what the tool receives:

redirect-sandbox.ts
const redirectToSandbox: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name !== 'PreToolUse') return {};

  const preInput = input as PreToolUseHookInput;
  if (preInput.tool_name === 'Write') {
    const originalPath = preInput.tool_input.file_path as string;
    return {
      hookSpecificOutput: {
        hookEventName: input.hook_event_name,
        permissionDecision: 'allow',
        updatedInput: {
          ...preInput.tool_input,
          file_path: `/sandbox${originalPath}`
        }
      }
    };
  }
  return {};
};
When using updatedInput, you must also include permissionDecision. Always return a new object rather than mutating the original tool_input.

Auto-approve specific tools

Bypass permission prompts for trusted tools:

auto-approve.ts
const autoApproveReadOnly: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name !== 'PreToolUse') return {};

  const preInput = input as PreToolUseHookInput;
  const readOnlyTools = ['Read', 'Glob', 'Grep', 'LS'];
  if (readOnlyTools.includes(preInput.tool_name)) {
    return {
      hookSpecificOutput: {
        hookEventName: input.hook_event_name,
        permissionDecision: 'allow',
        permissionDecisionReason: 'Read-only tool auto-approved'
      }
    };
  }
  return {};
};
The permissionDecision field accepts three values: 'allow' (auto-approve), 'deny' (block), or 'ask' (prompt for confirmation).

Handle advanced scenarios

These patterns help you build more sophisticated hook systems for complex use cases.

Chaining multiple hooks

Hooks execute in the order they appear in the array. Keep each hook focused on a single responsibility:

chaining.ts
const options = {
  hooks: {
    'PreToolUse': [
      { hooks: [rateLimiter] },        // First: check rate limits
      { hooks: [authorizationCheck] }, // Second: verify permissions
      { hooks: [inputSanitizer] },     // Third: sanitize inputs
      { hooks: [auditLogger] }         // Last: log the action
    ]
  }
};

Tool-specific matchers with regex

Use regex patterns to match multiple tools:

regex-matchers.ts
const options = {
  hooks: {
    'PreToolUse': [
      // Match file modification tools
      { matcher: 'Write|Edit|Delete', hooks: [fileSecurityHook] },

      // Match all MCP tools
      { matcher: '^mcp__', hooks: [mcpAuditHook] },

      // Match everything (no matcher)
      { hooks: [globalLogger] }
    ]
  }
};
Matchers only match tool names, not file paths or other arguments. To filter by file path, check tool_input.file_path inside your hook callback.

Tracking subagent activity

Use SubagentStop hooks to monitor subagent completion:

subagent-tracker.ts
const subagentTracker: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name === 'SubagentStop') {
    console.log(`[SUBAGENT] Completed`);
    console.log(`  Tool use ID: ${toolUseID}`);
    console.log(`  Stop hook active: ${input.stop_hook_active}`);
  }
  return {};
};

const options = {
  hooks: {
    SubagentStop: [{ hooks: [subagentTracker] }]
  }
};

Async operations in hooks

Hooks can perform async operations like HTTP requests. In TypeScript, pass the signal to fetch() for proper cancellation:

async-hook.ts
const webhookNotifier: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name !== 'PostToolUse') return {};

  try {
    // Pass signal for proper cancellation
    await fetch('https://api.example.com/webhook', {
      method: 'POST',
      body: JSON.stringify({
        tool: (input as PostToolUseHookInput).tool_name,
        timestamp: new Date().toISOString()
      }),
      signal
    });
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      console.log('Webhook request cancelled');
    }
  }

  return {};
};

Sending notifications TypeScript only

Use Notification hooks to receive status updates and forward them to external services:

notification-handler.ts
import { query, HookCallback, NotificationHookInput } from "@anthropic-ai/claude-agent-sdk";

const notificationHandler: HookCallback = async (input, toolUseID, { signal }) => {
  const notification = input as NotificationHookInput;

  await fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', {
    method: 'POST',
    body: JSON.stringify({
      text: `Agent status: ${notification.message}`
    }),
    signal
  });

  return {};
};

for await (const message of query({
  prompt: "Analyze this codebase",
  options: {
    hooks: {
      Notification: [{ hooks: [notificationHandler] }]
    }
  }
})) {
  console.log(message);
}

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
  • For lifecycle hooks (Stop, SessionStart, etc.), matchers are ignored

Matcher not filtering as expected

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

Modified input not applied

  • Ensure updatedInput is inside hookSpecificOutput
  • You must also return permissionDecision: 'allow'
  • Include hookEventName in the output

Session hooks not available

SessionStart, SessionEnd, and Notification hooks are only available in the TypeScript SDK.