Hook Types
The SDK supports several hook types:- PreToolUse - Runs before a tool is executed (permission control)
- PostToolUse - Runs after a tool completes (result validation)
- UserPromptSubmit - Runs when a user prompt is submitted (context injection)
- SessionStart - Runs when a session starts (initialization)
Basic PreToolUse Hook
Prevent specific bash commands from executing:import logging
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
HookContext,
HookInput,
HookJSONOutput,
HookMatcher,
)
logger = logging.getLogger(__name__)
async def check_bash_command(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Prevent certain bash commands from being executed."""
tool_name = input_data["tool_name"]
tool_input = input_data["tool_input"]
if tool_name != "Bash":
return {}
command = tool_input.get("command", "")
block_patterns = ["foo.sh"]
for pattern in block_patterns:
if pattern in command:
logger.warning(f"Blocked command: {command}")
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"Command contains invalid pattern: {pattern}",
}
}
return {}
# Configure hooks using ClaudeAgentOptions
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[check_bash_command]),
],
}
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Run the bash command: ./foo.sh --help")
async for msg in client.receive_response():
# Handle messages...
pass
UserPromptSubmit Hook
Add custom context when prompts are submitted:async def add_custom_instructions(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Add custom instructions when a session starts."""
return {
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "My favorite color is hot pink",
}
}
options = ClaudeAgentOptions(
hooks={
"UserPromptSubmit": [
HookMatcher(matcher=None, hooks=[add_custom_instructions]),
],
}
)
async with ClaudeSDKClient(options=options) as client:
await client.query("What's my favorite color?")
async for msg in client.receive_response():
# Claude will know your favorite color from the hook
pass
When
matcher=None, the hook applies to all operations regardless of tool name.PostToolUse Hook
Review tool output and provide feedback:async def review_tool_output(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Review tool output and provide additional context or warnings."""
tool_response = input_data.get("tool_response", "")
# If the tool produced an error, add helpful context
if "error" in str(tool_response).lower():
return {
"systemMessage": "⚠️ The command produced an error",
"reason": "Tool execution failed - consider checking the command syntax",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "The command encountered an error. You may want to try a different approach.",
}
}
return {}
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={
"PostToolUse": [
HookMatcher(matcher="Bash", hooks=[review_tool_output]),
],
}
)
Permission Decision Hook
UsepermissionDecision to explicitly allow or deny operations:
async def strict_approval_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Demonstrates using permissionDecision to control tool execution."""
tool_name = input_data.get("tool_name")
tool_input = input_data.get("tool_input", {})
# Block any Write operations to specific files
if tool_name == "Write":
file_path = tool_input.get("file_path", "")
if "important" in file_path.lower():
logger.warning(f"Blocked Write to: {file_path}")
return {
"reason": "Writes to files containing 'important' are not allowed",
"systemMessage": "🚫 Write operation blocked by security policy",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Security policy blocks writes to important files",
},
}
# Allow everything else explicitly
return {
"reason": "Tool use approved after security review",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Tool passed security checks",
},
}
options = ClaudeAgentOptions(
allowed_tools=["Write", "Bash"],
hooks={
"PreToolUse": [
HookMatcher(matcher="Write", hooks=[strict_approval_hook]),
],
}
)
Stopping Execution
Usecontinue_=False to halt execution on critical errors:
async def stop_on_error_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Demonstrates using continue=False to stop execution on certain conditions."""
tool_response = input_data.get("tool_response", "")
# Stop execution if we see a critical error
if "critical" in str(tool_response).lower():
logger.error("Critical error detected - stopping execution")
return {
"continue_": False,
"stopReason": "Critical error detected in tool output - execution halted for safety",
"systemMessage": "🛑 Execution stopped due to critical error",
}
return {"continue_": True}
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={
"PostToolUse": [
HookMatcher(matcher="Bash", hooks=[stop_on_error_hook]),
],
}
)
Hook Response Format
Hooks return aHookJSONOutput dictionary with optional fields:
return {
# Display a message to the user
"systemMessage": "⚠️ Warning message",
# Explain why this decision was made
"reason": "Detailed explanation for logging",
# Control execution flow
"continue_": True, # False to stop execution
"stopReason": "Why execution was stopped",
# Hook-specific output
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow", # or "deny"
"permissionDecisionReason": "Why this decision was made",
"additionalContext": "Extra context for Claude",
}
}
Multiple Hooks
You can attach multiple hooks to the same event:options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
# First matcher: check bash commands
HookMatcher(matcher="Bash", hooks=[check_bash_command]),
# Second matcher: strict approval for writes
HookMatcher(matcher="Write", hooks=[strict_approval_hook]),
],
"PostToolUse": [
# Review all tool outputs
HookMatcher(matcher=None, hooks=[review_tool_output]),
],
}
)
Complete Example
Full working example with multiple hook types
Full working example with multiple hook types
#!/usr/bin/env python
"""Example of using hooks with Claude Code SDK via ClaudeAgentOptions.
This file demonstrates various hook patterns using the hooks parameter
in ClaudeAgentOptions.
"""
import asyncio
import logging
import sys
from typing import Any
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from claude_agent_sdk.types import (
AssistantMessage,
HookContext,
HookInput,
HookJSONOutput,
HookMatcher,
Message,
ResultMessage,
TextBlock,
)
# Set up logging to see what's happening
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
logger = logging.getLogger(__name__)
def display_message(msg: Message) -> None:
"""Standardized message display function."""
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(msg, ResultMessage):
print("Result ended")
##### Hook callback functions
async def check_bash_command(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Prevent certain bash commands from being executed."""
tool_name = input_data["tool_name"]
tool_input = input_data["tool_input"]
if tool_name != "Bash":
return {}
command = tool_input.get("command", "")
block_patterns = ["foo.sh"]
for pattern in block_patterns:
if pattern in command:
logger.warning(f"Blocked command: {command}")
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"Command contains invalid pattern: {pattern}",
}
}
return {}
async def add_custom_instructions(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Add custom instructions when a session starts."""
return {
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "My favorite color is hot pink",
}
}
async def review_tool_output(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Review tool output and provide additional context or warnings."""
tool_response = input_data.get("tool_response", "")
# If the tool produced an error, add helpful context
if "error" in str(tool_response).lower():
return {
"systemMessage": "⚠️ The command produced an error",
"reason": "Tool execution failed - consider checking the command syntax",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "The command encountered an error. You may want to try a different approach.",
}
}
return {}
async def strict_approval_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Demonstrates using permissionDecision to control tool execution."""
tool_name = input_data.get("tool_name")
tool_input = input_data.get("tool_input", {})
# Block any Write operations to specific files
if tool_name == "Write":
file_path = tool_input.get("file_path", "")
if "important" in file_path.lower():
logger.warning(f"Blocked Write to: {file_path}")
return {
"reason": "Writes to files containing 'important' are not allowed for safety",
"systemMessage": "🚫 Write operation blocked by security policy",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Security policy blocks writes to important files",
},
}
# Allow everything else explicitly
return {
"reason": "Tool use approved after security review",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Tool passed security checks",
},
}
async def stop_on_error_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Demonstrates using continue=False to stop execution on certain conditions."""
tool_response = input_data.get("tool_response", "")
# Stop execution if we see a critical error
if "critical" in str(tool_response).lower():
logger.error("Critical error detected - stopping execution")
return {
"continue_": False,
"stopReason": "Critical error detected in tool output - execution halted for safety",
"systemMessage": "🛑 Execution stopped due to critical error",
}
return {"continue_": True}
async def example_pretooluse() -> None:
"""Basic example demonstrating hook protection."""
print("=== PreToolUse Example ===")
print("This example demonstrates how PreToolUse can block some bash commands but not others.\n")
# Configure hooks using ClaudeAgentOptions
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[check_bash_command]),
],
}
)
async with ClaudeSDKClient(options=options) as client:
# Test 1: Command with forbidden pattern (will be blocked)
print("Test 1: Trying a command that our PreToolUse hook should block...")
print("User: Run the bash command: ./foo.sh --help")
await client.query("Run the bash command: ./foo.sh --help")
async for msg in client.receive_response():
display_message(msg)
print("\n" + "=" * 50 + "\n")
# Test 2: Safe command that should work
print("Test 2: Trying a command that our PreToolUse hook should allow...")
print("User: Run the bash command: echo 'Hello from hooks example!'")
await client.query("Run the bash command: echo 'Hello from hooks example!'")
async for msg in client.receive_response():
display_message(msg)
print("\n")
async def main() -> None:
"""Run the PreToolUse example."""
await example_pretooluse()
if __name__ == "__main__":
print("Starting Claude SDK Hooks Example...")
print("=" * 50 + "\n")
asyncio.run(main())
Hook Execution Order
- SessionStart - When a new session begins
- UserPromptSubmit - When a user submits a prompt
- PreToolUse - Before each tool is executed
- PostToolUse - After each tool completes
Key Takeaways
- Use hooks for advanced control over SDK behavior
- PreToolUse hooks control permissions before execution
- PostToolUse hooks validate and respond to results
- UserPromptSubmit hooks inject context
- Use
permissionDecisionto explicitly allow/deny operations - Use
continue_=Falseto stop execution on critical conditions - Configure hooks in the
hooksparameter ofClaudeAgentOptions
Next Steps
Tool Permissions
Simpler permission control with callbacks
API Reference
Complete hooks API documentation