Overview
Hooks allow you to intercept and control Claude’s behavior at key lifecycle events like tool usage, prompt submission, and task execution.
from claude_agent_sdk.types import HookCallback, HookMatcher, HookEvent
hooks = {
"PreToolUse" : [
HookMatcher(
matcher = "Bash" ,
hooks = [my_bash_hook],
timeout = 30.0
)
]
}
HookEvent
Supported hook event types.
HookEvent = Literal[
"PreToolUse" ,
"PostToolUse" ,
"PostToolUseFailure" ,
"UserPromptSubmit" ,
"Stop" ,
"SubagentStop" ,
"PreCompact" ,
"Notification" ,
"SubagentStart" ,
"PermissionRequest"
]
Fires before a tool is executed. Can approve, deny, or modify tool input.
Fires after successful tool execution. Can add context or modify output.
Fires when tool execution fails. Can add error context.
Fires when user submits a prompt. Can add context to the prompt.
Fires when the main session stops.
Fires when a sub-agent (Task tool) completes.
Fires before conversation compaction.
Fires for system notifications.
Fires when a sub-agent starts.
Fires when permission is requested for a tool.
HookCallback
Function signature for hook callbacks.
HookCallback = Callable[
[HookInput, str | None , HookContext],
Awaitable[HookJSONOutput]
]
Hook callbacks receive:
input: HookInput - Strongly-typed input data for the event
tool_use_id: str | None - Optional tool use identifier
context: HookContext - Hook context (currently contains signal placeholder)
And must return a HookJSONOutput dictionary.
Example
async def my_hook (
input : HookInput,
tool_use_id : str | None ,
context : HookContext
) -> HookJSONOutput:
if input [ "hook_event_name" ] == "PreToolUse" :
tool_name = input [ "tool_name" ]
print ( f "About to use tool: { tool_name } " )
return {
"continue_" : True ,
"hookSpecificOutput" : {
"hookEventName" : "PreToolUse" ,
"permissionDecision" : "allow"
}
}
return { "continue_" : True }
HookMatcher
Configuration for matching and handling hook events.
@dataclass
class HookMatcher :
matcher: str | None = None
hooks: list[HookCallback] = field( default_factory = list )
timeout: float | None = None
Pattern to match against. For PreToolUse, this can be a tool name like "Bash" or a regex pattern like "Write|Edit|MultiEdit". See hook matcher documentation for details.
List of callback functions to execute for this matcher.
Timeout in seconds for all hooks in this matcher (default: 60).
Example
hooks = {
"PreToolUse" : [
HookMatcher(
matcher = "Bash" ,
hooks = [bash_safety_check],
timeout = 30.0
),
HookMatcher(
matcher = "Write|Edit" ,
hooks = [file_write_logger],
timeout = 10.0
)
],
"PostToolUse" : [
HookMatcher(
matcher = None , # Match all tools
hooks = [log_all_tool_uses]
)
]
}
options = ClaudeAgentOptions( hooks = hooks)
Strongly-typed input for each hook event. All hook inputs extend BaseHookInput.
class BaseHookInput ( TypedDict ):
session_id: str
transcript_path: str
cwd: str
permission_mode: NotRequired[ str ]
class PreToolUseHookInput ( BaseHookInput ):
hook_event_name: Literal[ "PreToolUse" ]
tool_name: str
tool_input: dict[ str , Any]
tool_use_id: str
agent_id: NotRequired[ str ] # Present in sub-agents
agent_type: NotRequired[ str ] # Present with --agent or in sub-agents
PostToolUseHookInput
class PostToolUseHookInput ( BaseHookInput ):
hook_event_name: Literal[ "PostToolUse" ]
tool_name: str
tool_input: dict[ str , Any]
tool_response: Any
tool_use_id: str
agent_id: NotRequired[ str ]
agent_type: NotRequired[ str ]
PostToolUseFailureHookInput
class PostToolUseFailureHookInput ( BaseHookInput ):
hook_event_name: Literal[ "PostToolUseFailure" ]
tool_name: str
tool_input: dict[ str , Any]
tool_use_id: str
error: str
is_interrupt: NotRequired[ bool ]
agent_id: NotRequired[ str ]
agent_type: NotRequired[ str ]
class UserPromptSubmitHookInput ( BaseHookInput ):
hook_event_name: Literal[ "UserPromptSubmit" ]
prompt: str
class StopHookInput ( BaseHookInput ):
hook_event_name: Literal[ "Stop" ]
stop_hook_active: bool
class SubagentStopHookInput ( BaseHookInput ):
hook_event_name: Literal[ "SubagentStop" ]
stop_hook_active: bool
agent_id: str
agent_transcript_path: str
agent_type: str
class PreCompactHookInput ( BaseHookInput ):
hook_event_name: Literal[ "PreCompact" ]
trigger: Literal[ "manual" , "auto" ]
custom_instructions: str | None
class NotificationHookInput ( BaseHookInput ):
hook_event_name: Literal[ "Notification" ]
message: str
title: NotRequired[ str ]
notification_type: str
class SubagentStartHookInput ( BaseHookInput ):
hook_event_name: Literal[ "SubagentStart" ]
agent_id: str
agent_type: str
class PermissionRequestHookInput ( BaseHookInput ):
hook_event_name: Literal[ "PermissionRequest" ]
tool_name: str
tool_input: dict[ str , Any]
permission_suggestions: NotRequired[list[Any]]
agent_id: NotRequired[ str ]
agent_type: NotRequired[ str ]
HookJSONOutput Types
Output types for hook callbacks. Can be synchronous or asynchronous.
SyncHookJSONOutput
class SyncHookJSONOutput ( TypedDict ):
continue_: NotRequired[ bool ] # Default: True
suppressOutput: NotRequired[ bool ] # Default: False
stopReason: NotRequired[ str ]
decision: NotRequired[Literal[ "block" ]]
systemMessage: NotRequired[ str ]
reason: NotRequired[ str ]
hookSpecificOutput: NotRequired[HookSpecificOutput]
Whether Claude should proceed after hook execution. Note: Use continue_ in Python (converted to continue for CLI).
Hide stdout from transcript mode.
Message shown when continue_ is False.
Set to "block" to indicate blocking behavior.
Warning message displayed to the user.
Feedback message for Claude about the decision.
Event-specific controls (see below).
AsyncHookJSONOutput
class AsyncHookJSONOutput ( TypedDict ):
async_: Literal[ True ] # Use async_ in Python
asyncTimeout: NotRequired[ int ] # Milliseconds
Defers hook execution. Use for long-running operations.
HookSpecificOutput
Event-specific output for fine-grained control.
Show PreToolUseHookSpecificOutput
class PreToolUseHookSpecificOutput ( TypedDict ):
hookEventName: Literal[ "PreToolUse" ]
permissionDecision: NotRequired[Literal[ "allow" , "deny" , "ask" ]]
permissionDecisionReason: NotRequired[ str ]
updatedInput: NotRequired[dict[ str , Any]]
additionalContext: NotRequired[ str ]
permissionDecision
Literal['allow', 'deny', 'ask']
Override permission decision for this tool use.
Reason for the permission decision.
Modified tool input parameters.
Additional context to provide to Claude.
Show PostToolUseHookSpecificOutput
class PostToolUseHookSpecificOutput ( TypedDict ):
hookEventName: Literal[ "PostToolUse" ]
additionalContext: NotRequired[ str ]
updatedMCPToolOutput: NotRequired[Any]
Additional context to provide to Claude.
Show PostToolUseFailureHookSpecificOutput
class PostToolUseFailureHookSpecificOutput ( TypedDict ):
hookEventName: Literal[ "PostToolUseFailure" ]
additionalContext: NotRequired[ str ]
Show Other Hook-Specific Outputs
Similar structures exist for:
UserPromptSubmitHookSpecificOutput
SessionStartHookSpecificOutput
NotificationHookSpecificOutput
SubagentStartHookSpecificOutput
PermissionRequestHookSpecificOutput
HookContext
Context information passed to hook callbacks.
class HookContext ( TypedDict ):
signal: Any | None # Future: abort signal support
Currently a placeholder for future abort signal support.
Complete Example
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
from claude_agent_sdk.types import (
HookCallback,
HookInput,
HookContext,
HookJSONOutput,
HookMatcher,
)
async def bash_safety_hook (
input : HookInput,
tool_use_id : str | None ,
context : HookContext,
) -> HookJSONOutput:
"""Prevent dangerous bash commands."""
if input [ "hook_event_name" ] != "PreToolUse" :
return { "continue_" : True }
command = input [ "tool_input" ].get( "command" , "" )
# Block destructive commands
if any (cmd in command for cmd in [ "rm -rf" , "mkfs" , "dd if=" ]):
return {
"continue_" : False ,
"stopReason" : "Dangerous command blocked" ,
"hookSpecificOutput" : {
"hookEventName" : "PreToolUse" ,
"permissionDecision" : "deny" ,
"permissionDecisionReason" : "Command contains dangerous operations"
}
}
return { "continue_" : True }
async def tool_logger (
input : HookInput,
tool_use_id : str | None ,
context : HookContext,
) -> HookJSONOutput:
"""Log all tool uses."""
if input [ "hook_event_name" ] == "PostToolUse" :
tool_name = input [ "tool_name" ]
print ( f "✓ Tool completed: { tool_name } " )
elif input [ "hook_event_name" ] == "PostToolUseFailure" :
tool_name = input [ "tool_name" ]
error = input [ "error" ]
print ( f "✗ Tool failed: { tool_name } - { error } " )
return { "continue_" : True }
options = ClaudeAgentOptions(
hooks = {
"PreToolUse" : [
HookMatcher(
matcher = "Bash" ,
hooks = [bash_safety_hook],
timeout = 10.0
)
],
"PostToolUse" : [
HookMatcher(
matcher = None , # All tools
hooks = [tool_logger]
)
],
"PostToolUseFailure" : [
HookMatcher(
matcher = None ,
hooks = [tool_logger]
)
]
}
)
client = ClaudeSDKClient(options)
Python uses async_ and continue_ (with underscores) to avoid keyword conflicts. These are automatically converted to async and continue when sent to the CLI.