Python SDK for programmatically interacting with Claude Code, enabling AI-powered automation workflows with support for bidirectional conversations, custom in-process tools, hooks, and fine-grained permission control
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
—
The risk profile of this skill
This plugin was archived by the owner on Jun 3, 2026
Reason: Retiring all tiles created prior to the transition to plugin support
The hook system allows you to intercept and control Claude's operations at specific points in the agent loop. Hooks can modify inputs, add context, block execution, or trigger custom logic during PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, and PreCompact events.
Six hook event types are supported.
HookEvent = Literal[
"PreToolUse",
"PostToolUse",
"UserPromptSubmit",
"Stop",
"SubagentStop",
"PreCompact"
]Event Types:
PreToolUse: Before a tool is executed (can modify input or block execution)PostToolUse: After a tool is executed (can add context to results)UserPromptSubmit: When user submits a prompt (can add context)Stop: When agent execution stopsSubagentStop: When a sub-agent stopsPreCompact: Before transcript compactionConfigure hooks with pattern matching.
@dataclass
class HookMatcher:
"""Hook configuration with pattern matching."""
matcher: str | None = None
hooks: list[HookCallback] = field(default_factory=list)Fields:
matcher (str | None): Tool name pattern for PreToolUse and PostToolUse. Examples:
"Bash": Match only Bash tool"Write|Edit": Match Write or Edit tools"Write|MultiEdit|Edit": Match multiple toolsNone: Match all tools (for PreToolUse/PostToolUse) or not applicable (for other hooks)hooks (list[HookCallback]): List of callback functions to execute for this matcher.
Usage Example:
from claude_agent_sdk import HookMatcher, ClaudeAgentOptions
async def my_hook(input, tool_use_id, context):
return {}
# Match specific tool
matcher = HookMatcher(matcher="Bash", hooks=[my_hook])
# Match multiple tools
matcher = HookMatcher(matcher="Write|Edit", hooks=[my_hook])
# Match all tools
matcher = HookMatcher(matcher=None, hooks=[my_hook])
# Use in options
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [matcher]
}
)Callback function type for hooks.
HookCallback = Callable[
[HookInput, str | None, HookContext],
Awaitable[HookJSONOutput]
]Parameters:
input (HookInput): Hook-specific input with discriminated union based on hook_event_nametool_use_id (str | None): Optional tool use identifiercontext (HookContext): Hook context with settings directoryReturns:
HookJSONOutput: Either AsyncHookJSONOutput or SyncHookJSONOutput
Signature Example:
async def my_hook_callback(
input: HookInput,
tool_use_id: str | None,
context: HookContext
) -> HookJSONOutput:
# Hook logic
return {}Context information provided to hook callbacks.
class HookContext(TypedDict):
"""Context provided to hooks."""
signal: Any | NoneFields:
signal (Any | None): Reserved for future abort signal support. Currently always None.Usage Example:
async def my_hook(input, tool_use_id, context):
signal = context["signal"]
# Future: Use signal for abort operations
return {}All hook inputs extend BaseHookInput and use discriminated unions based on hook_event_name.
Base fields present in all hook inputs.
class BaseHookInput(TypedDict):
"""Base hook input fields present across many hook events."""
session_id: str
transcript_path: str
cwd: str
permission_mode: NotRequired[str]Fields:
session_id (str): Session identifiertranscript_path (str): Path to conversation transcript filecwd (str): Current working directorypermission_mode (str, optional): Current permission modeInput for PreToolUse hooks (before tool execution).
class PreToolUseHookInput(BaseHookInput):
"""Input data for PreToolUse hook events."""
hook_event_name: Literal["PreToolUse"]
tool_name: str
tool_input: dict[str, Any]Fields:
hook_event_name: Always "PreToolUse"tool_name (str): Name of tool being invokedtool_input (dict[str, Any]): Tool input parametersUsage Example:
async def pre_tool_use_hook(input, tool_use_id, context):
if input["hook_event_name"] == "PreToolUse":
tool_name = input["tool_name"]
tool_input = input["tool_input"]
# Validate or modify input
if tool_name == "Bash":
command = tool_input.get("command", "")
if "rm -rf" in command:
return {
"decision": "block",
"reason": "Dangerous command blocked"
}
# Allow with modified input
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": tool_input
}
}
return {}Input for PostToolUse hooks (after tool execution).
class PostToolUseHookInput(BaseHookInput):
"""Input data for PostToolUse hook events."""
hook_event_name: Literal["PostToolUse"]
tool_name: str
tool_input: dict[str, Any]
tool_response: AnyFields:
hook_event_name: Always "PostToolUse"tool_name (str): Name of tool that was invokedtool_input (dict[str, Any]): Tool input parameterstool_response (Any): Tool execution responseUsage Example:
async def post_tool_use_hook(input, tool_use_id, context):
if input["hook_event_name"] == "PostToolUse":
tool_name = input["tool_name"]
tool_response = input["tool_response"]
# Add context based on results
if tool_name == "Bash":
return {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Command executed successfully"
}
}
return {}Input for UserPromptSubmit hooks (when user submits prompt).
class UserPromptSubmitHookInput(BaseHookInput):
"""Input data for UserPromptSubmit hook events."""
hook_event_name: Literal["UserPromptSubmit"]
prompt: strFields:
hook_event_name: Always "UserPromptSubmit"prompt (str): User prompt textUsage Example:
async def user_prompt_hook(input, tool_use_id, context):
if input["hook_event_name"] == "UserPromptSubmit":
prompt = input["prompt"]
# Add context to prompt
return {
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": f"Processing prompt: {len(prompt)} characters"
}
}
return {}Input for Stop hooks (when agent execution stops).
class StopHookInput(BaseHookInput):
"""Input data for Stop hook events."""
hook_event_name: Literal["Stop"]
stop_hook_active: boolFields:
hook_event_name: Always "Stop"stop_hook_active (bool): Whether stop hook is activeUsage Example:
async def stop_hook(input, tool_use_id, context):
if input["hook_event_name"] == "Stop":
# Cleanup or logging
return {}
return {}Input for SubagentStop hooks (when sub-agent stops).
class SubagentStopHookInput(BaseHookInput):
"""Input data for SubagentStop hook events."""
hook_event_name: Literal["SubagentStop"]
stop_hook_active: boolFields:
hook_event_name: Always "SubagentStop"stop_hook_active (bool): Whether stop hook is activeUsage Example:
async def subagent_stop_hook(input, tool_use_id, context):
if input["hook_event_name"] == "SubagentStop":
# Handle sub-agent completion
return {}
return {}Input for PreCompact hooks (before transcript compaction).
class PreCompactHookInput(BaseHookInput):
"""Input data for PreCompact hook events."""
hook_event_name: Literal["PreCompact"]
trigger: Literal["manual", "auto"]
custom_instructions: str | NoneFields:
hook_event_name: Always "PreCompact"trigger: Compaction trigger type ("manual" or "auto")custom_instructions (str | None): Custom compaction instructionsUsage Example:
async def pre_compact_hook(input, tool_use_id, context):
if input["hook_event_name"] == "PreCompact":
trigger = input["trigger"]
# Perform pre-compaction actions
return {}
return {}Union of all hook input types.
HookInput = (
PreToolUseHookInput
| PostToolUseHookInput
| UserPromptSubmitHookInput
| StopHookInput
| SubagentStopHookInput
| PreCompactHookInput
)Hooks return either synchronous or asynchronous outputs.
Standard hook response with control and decision fields.
class SyncHookJSONOutput(TypedDict):
"""Synchronous hook output with control and decision fields."""
# Common control fields
continue_: NotRequired[bool]
suppressOutput: NotRequired[bool]
stopReason: NotRequired[str]
# Decision fields
decision: NotRequired[Literal["block"]]
systemMessage: NotRequired[str]
reason: NotRequired[str]
# Hook-specific outputs
hookSpecificOutput: NotRequired[HookSpecificOutput]Fields:
continue_ (bool, optional): Whether Claude should proceed after hook execution. Default: True. Note: Use continue_ in Python code; it's automatically converted to "continue" for CLI.
suppressOutput (bool, optional): Hide stdout from transcript mode. Default: False.
stopReason (str, optional): Message shown when continue_ is False.
decision (Literal["block"], optional): Set to "block" to indicate blocking behavior.
systemMessage (str, optional): Warning message displayed to the user.
reason (str, optional): Feedback message for Claude about the decision.
hookSpecificOutput (HookSpecificOutput, optional): Event-specific controls (see below).
Usage Example:
# Allow execution
async def allow_hook(input, tool_use_id, context):
return {} # Empty dict means continue normally
# Block execution
async def block_hook(input, tool_use_id, context):
return {
"decision": "block",
"systemMessage": "This operation is not allowed",
"reason": "Security policy violation"
}
# Stop execution
async def stop_hook(input, tool_use_id, context):
return {
"continue_": False, # Note the underscore
"stopReason": "User intervention required"
}
# Suppress output
async def quiet_hook(input, tool_use_id, context):
return {
"suppressOutput": True
}Deferred hook execution response.
class AsyncHookJSONOutput(TypedDict):
"""Async hook output that defers hook execution."""
async_: Literal[True]
asyncTimeout: NotRequired[int]Fields:
async_ (Literal[True]): Set to True to defer hook execution. Note: Use async_ in Python code; it's automatically converted to "async" for CLI.
asyncTimeout (int, optional): Timeout in milliseconds for the async operation.
Usage Example:
async def async_hook(input, tool_use_id, context):
# Defer execution
return {
"async_": True, # Note the underscore
"asyncTimeout": 5000 # 5 second timeout
}Union of hook output types.
HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutputEvent-specific controls in hookSpecificOutput.
class PreToolUseHookSpecificOutput(TypedDict):
"""Output specific to PreToolUse hooks."""
hookEventName: Literal["PreToolUse"]
permissionDecision: NotRequired[Literal["allow", "deny", "ask"]]
permissionDecisionReason: NotRequired[str]
updatedInput: NotRequired[dict[str, Any]]Fields:
hookEventName: Must be "PreToolUse"permissionDecision: Permission decision ("allow", "deny", or "ask")permissionDecisionReason: Reason for the decisionupdatedInput: Modified tool input parametersUsage Example:
async def pre_tool_hook(input, tool_use_id, context):
if input["hook_event_name"] == "PreToolUse":
# Modify tool input
modified_input = input["tool_input"].copy()
modified_input["safe_mode"] = True
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Added safety flag",
"updatedInput": modified_input
}
}
return {}class PostToolUseHookSpecificOutput(TypedDict):
"""Output specific to PostToolUse hooks."""
hookEventName: Literal["PostToolUse"]
additionalContext: NotRequired[str]Fields:
hookEventName: Must be "PostToolUse"additionalContext: Additional context to provide to ClaudeUsage Example:
async def post_tool_hook(input, tool_use_id, context):
if input["hook_event_name"] == "PostToolUse":
return {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Tool executed successfully with no errors"
}
}
return {}class UserPromptSubmitHookSpecificOutput(TypedDict):
"""Output specific to UserPromptSubmit hooks."""
hookEventName: Literal["UserPromptSubmit"]
additionalContext: NotRequired[str]Fields:
hookEventName: Must be "UserPromptSubmit"additionalContext: Additional context to provide to ClaudeUsage Example:
async def prompt_submit_hook(input, tool_use_id, context):
if input["hook_event_name"] == "UserPromptSubmit":
return {
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Context: User is working on a Python project"
}
}
return {}Block dangerous bash commands:
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
DANGEROUS_PATTERNS = ["rm -rf", "sudo rm", "> /dev/sda", ":(){ :|:& };:"]
async def security_hook(input, tool_use_id, context):
if input["hook_event_name"] == "PreToolUse":
if input["tool_name"] == "Bash":
command = input["tool_input"].get("command", "")
for pattern in DANGEROUS_PATTERNS:
if pattern in command:
return {
"decision": "block",
"systemMessage": f"Blocked dangerous command: {pattern}",
"reason": "Security policy violation",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"Command contains dangerous pattern: {pattern}"
}
}
return {}
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Bash"],
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[security_hook])
]
}
)Log all tool usage:
import logging
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def pre_tool_logger(input, tool_use_id, context):
if input["hook_event_name"] == "PreToolUse":
logger.info(f"Tool {input['tool_name']} called with: {input['tool_input']}")
return {}
async def post_tool_logger(input, tool_use_id, context):
if input["hook_event_name"] == "PostToolUse":
logger.info(f"Tool {input['tool_name']} completed: {input['tool_response']}")
return {}
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Bash"],
hooks={
"PreToolUse": [HookMatcher(matcher=None, hooks=[pre_tool_logger])],
"PostToolUse": [HookMatcher(matcher=None, hooks=[post_tool_logger])]
}
)Add context based on tool results:
async def context_enhancer(input, tool_use_id, context):
if input["hook_event_name"] == "PostToolUse":
tool_name = input["tool_name"]
response = input["tool_response"]
if tool_name == "Read":
# Add context about file type
content = response.get("content", "")
if "import" in content and "def" in content:
return {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "This appears to be Python code with imports and functions"
}
}
elif tool_name == "Bash":
# Add context about command success
if response.get("exit_code") == 0:
return {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Command executed successfully"
}
}
return {}
options = ClaudeAgentOptions(
hooks={
"PostToolUse": [HookMatcher(matcher=None, hooks=[context_enhancer])]
}
)Protect critical files from modification:
PROTECTED_FILES = ["/etc/passwd", "/etc/shadow", "~/.ssh/id_rsa"]
async def file_protection_hook(input, tool_use_id, context):
if input["hook_event_name"] == "PreToolUse":
tool_name = input["tool_name"]
if tool_name in ["Write", "Edit", "MultiEdit"]:
file_path = input["tool_input"].get("file_path", "")
for protected in PROTECTED_FILES:
if protected in file_path:
return {
"decision": "block",
"systemMessage": f"Cannot modify protected file: {file_path}",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "File is protected by policy"
}
}
return {}
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(matcher="Write|Edit|MultiEdit", hooks=[file_protection_hook])
]
}
)Sanitize tool inputs:
async def input_sanitizer(input, tool_use_id, context):
if input["hook_event_name"] == "PreToolUse":
if input["tool_name"] == "Bash":
# Remove potentially dangerous env vars
tool_input = input["tool_input"].copy()
command = tool_input.get("command", "")
# Strip environment variable exports
if "export" in command.lower():
safe_command = command.replace("export", "# export")
tool_input["command"] = safe_command
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Sanitized environment exports",
"updatedInput": tool_input
}
}
return {}
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [HookMatcher(matcher="Bash", hooks=[input_sanitizer])]
}
)Use underscored versions in Python code:
async_ instead of asynccontinue_ instead of continueThese are automatically converted to the correct field names when sent to the CLI.
When multiple hooks match, they execute in order defined in the hooks list.
Exceptions in hooks are caught and logged but don't break execution. Always handle errors gracefully.
Hooks are called synchronously in the agent loop. Keep hook logic fast to avoid delays.
The Python SDK does not support SessionStart, SessionEnd, and Notification hooks due to setup limitations.
docs