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:
The callback function
The logic that runs when the hook fires
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:
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 Event | Python | TypeScript | Trigger | Example |
|---|---|---|---|---|
PreToolUse | Tool call request (can block or modify) | Block dangerous shell commands | ||
PostToolUse | Tool execution result | Log all file changes to audit trail | ||
PostToolUseFailure | Tool execution failure | Handle or log tool errors | ||
UserPromptSubmit | User prompt submission | Inject additional context into prompts | ||
Stop | Agent execution stop | Save session state before exit | ||
SubagentStart | Subagent initialization | Track parallel task spawning | ||
SubagentStop | Subagent completion | Aggregate results from parallel tasks | ||
PreCompact | Conversation compaction request | Archive full transcript before summarizing | ||
PermissionRequest | Permission dialog would be displayed | Custom permission handling | ||
SessionStart | Session initialization | Initialize logging and telemetry | ||
SessionEnd | Session termination | Clean up temporary resources | ||
Notification | Agent status messages | Send 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():
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:
| Option | Type | Default | Description |
|---|---|---|---|
matcher | string | undefined | Regex pattern to match tool names |
hooks | HookCallback[] | - | Required. Array of callback functions |
timeout | number | 60 | Timeout 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:
const options = {
hooks: {
PreToolUse: [
{ matcher: 'Write|Edit', hooks: [validateFilePath] }
]
}
};Callback function inputs
Every hook callback receives three arguments:
Input data (dict / HookInput)
Event details including hook type, session ID, tool name, and tool inputs
Tool use ID (str | None / string | null)
Correlate PreToolUse and PostToolUse events
Context (HookContext)
In TypeScript, contains a signal property (AbortSignal) for cancellation
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
| Field | Type | Description |
|---|---|---|
continue | boolean | Whether the agent should continue (default: true) |
stopReason | string | Message shown when continue is false |
suppressOutput | boolean | Hide stdout from the transcript |
systemMessage | string | Message injected into the conversation |
Fields inside hookSpecificOutput
| Field | Type | Description |
|---|---|---|
hookEventName | string | Required. Use input.hook_event_name |
permissionDecision | 'allow' | 'deny' | 'ask' | Controls whether the tool executes |
permissionDecisionReason | string | Explanation shown to Claude |
updatedInput | object | Modified tool input (requires allow) |
This example blocks write operations to the /etc directory while injecting a system message:
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:
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:
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:
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 {};
};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:
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 {};
};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:
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:
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] }
]
}
};tool_input.file_path inside your hook callback.Tracking subagent activity
Use SubagentStop hooks to monitor subagent completion:
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:
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:
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, notpreToolUse) - •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
updatedInputis insidehookSpecificOutput - •You must also return
permissionDecision: 'allow' - •Include
hookEventNamein the output
Session hooks not available
SessionStart, SessionEnd, and Notification hooks are only available in the TypeScript SDK.