Skip to main content

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"
]
PreToolUse
HookEvent
Fires before a tool is executed. Can approve, deny, or modify tool input.
PostToolUse
HookEvent
Fires after successful tool execution. Can add context or modify output.
PostToolUseFailure
HookEvent
Fires when tool execution fails. Can add error context.
UserPromptSubmit
HookEvent
Fires when user submits a prompt. Can add context to the prompt.
Stop
HookEvent
Fires when the main session stops.
SubagentStop
HookEvent
Fires when a sub-agent (Task tool) completes.
PreCompact
HookEvent
Fires before conversation compaction.
Notification
HookEvent
Fires for system notifications.
SubagentStart
HookEvent
Fires when a sub-agent starts.
PermissionRequest
HookEvent
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:
  1. input: HookInput - Strongly-typed input data for the event
  2. tool_use_id: str | None - Optional tool use identifier
  3. 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
matcher
str | 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.
hooks
list[HookCallback]
List of callback functions to execute for this matcher.
timeout
float | None
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)

HookInput Types

Strongly-typed input for each hook event. All hook inputs extend BaseHookInput.

BaseHookInput

class BaseHookInput(TypedDict):
    session_id: str
    transcript_path: str
    cwd: str
    permission_mode: NotRequired[str]

PreToolUseHookInput

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]

UserPromptSubmitHookInput

class UserPromptSubmitHookInput(BaseHookInput):
    hook_event_name: Literal["UserPromptSubmit"]
    prompt: str

StopHookInput

class StopHookInput(BaseHookInput):
    hook_event_name: Literal["Stop"]
    stop_hook_active: bool

SubagentStopHookInput

class SubagentStopHookInput(BaseHookInput):
    hook_event_name: Literal["SubagentStop"]
    stop_hook_active: bool
    agent_id: str
    agent_transcript_path: str
    agent_type: str

PreCompactHookInput

class PreCompactHookInput(BaseHookInput):
    hook_event_name: Literal["PreCompact"]
    trigger: Literal["manual", "auto"]
    custom_instructions: str | None

NotificationHookInput

class NotificationHookInput(BaseHookInput):
    hook_event_name: Literal["Notification"]
    message: str
    title: NotRequired[str]
    notification_type: str

SubagentStartHookInput

class SubagentStartHookInput(BaseHookInput):
    hook_event_name: Literal["SubagentStart"]
    agent_id: str
    agent_type: str

PermissionRequestHookInput

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]
continue_
bool
Whether Claude should proceed after hook execution. Note: Use continue_ in Python (converted to continue for CLI).
suppressOutput
bool
Hide stdout from transcript mode.
stopReason
str
Message shown when continue_ is False.
decision
Literal['block']
Set to "block" to indicate blocking behavior.
systemMessage
str
Warning message displayed to the user.
reason
str
Feedback message for Claude about the decision.
hookSpecificOutput
HookSpecificOutput
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.

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.

Build docs developers (and LLMs) love