CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-claude-agent-sdk

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

Quality

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

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

Overview
Eval results
Files

hook-system.mddocs/

Hook System

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.

Capabilities

Hook 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 stops
  • SubagentStop: When a sub-agent stops
  • PreCompact: Before transcript compaction

Hook Matcher

Configure 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 tools
    • None: 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]
    }
)

Hook Callback

Callback function type for hooks.

HookCallback = Callable[
    [HookInput, str | None, HookContext],
    Awaitable[HookJSONOutput]
]

Parameters:

  1. input (HookInput): Hook-specific input with discriminated union based on hook_event_name
  2. tool_use_id (str | None): Optional tool use identifier
  3. context (HookContext): Hook context with settings directory

Returns:

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 {}

Hook Context

Context information provided to hook callbacks.

class HookContext(TypedDict):
    """Context provided to hooks."""

    signal: Any | None

Fields:

  • 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 {}

Hook Input Types

All hook inputs extend BaseHookInput and use discriminated unions based on hook_event_name.

Base Hook Input

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 identifier
  • transcript_path (str): Path to conversation transcript file
  • cwd (str): Current working directory
  • permission_mode (str, optional): Current permission mode

PreToolUse Hook Input

Input 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 invoked
  • tool_input (dict[str, Any]): Tool input parameters

Usage 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 {}

PostToolUse Hook Input

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: Any

Fields:

  • hook_event_name: Always "PostToolUse"
  • tool_name (str): Name of tool that was invoked
  • tool_input (dict[str, Any]): Tool input parameters
  • tool_response (Any): Tool execution response

Usage 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 {}

UserPromptSubmit Hook Input

Input for UserPromptSubmit hooks (when user submits prompt).

class UserPromptSubmitHookInput(BaseHookInput):
    """Input data for UserPromptSubmit hook events."""

    hook_event_name: Literal["UserPromptSubmit"]
    prompt: str

Fields:

  • hook_event_name: Always "UserPromptSubmit"
  • prompt (str): User prompt text

Usage 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 {}

Stop Hook Input

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: bool

Fields:

  • hook_event_name: Always "Stop"
  • stop_hook_active (bool): Whether stop hook is active

Usage Example:

async def stop_hook(input, tool_use_id, context):
    if input["hook_event_name"] == "Stop":
        # Cleanup or logging
        return {}

    return {}

SubagentStop Hook Input

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: bool

Fields:

  • hook_event_name: Always "SubagentStop"
  • stop_hook_active (bool): Whether stop hook is active

Usage Example:

async def subagent_stop_hook(input, tool_use_id, context):
    if input["hook_event_name"] == "SubagentStop":
        # Handle sub-agent completion
        return {}

    return {}

PreCompact Hook Input

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 | None

Fields:

  • hook_event_name: Always "PreCompact"
  • trigger: Compaction trigger type ("manual" or "auto")
  • custom_instructions (str | None): Custom compaction instructions

Usage 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 {}

Hook Input Union

Union of all hook input types.

HookInput = (
    PreToolUseHookInput
    | PostToolUseHookInput
    | UserPromptSubmitHookInput
    | StopHookInput
    | SubagentStopHookInput
    | PreCompactHookInput
)

Hook Output Types

Hooks return either synchronous or asynchronous outputs.

Synchronous Hook Output

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
    }

Asynchronous Hook Output

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
    }

Hook Output Union

Union of hook output types.

HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput

Hook-Specific Output Types

Event-specific controls in hookSpecificOutput.

PreToolUse Specific Output

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 decision
  • updatedInput: Modified tool input parameters

Usage 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 {}

PostToolUse Specific Output

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 Claude

Usage 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 {}

UserPromptSubmit Specific Output

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 Claude

Usage 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 {}

Complete Examples

Security Hook

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])
        ]
    }
)

Logging 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])]
    }
)

Context Enhancement Hook

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])]
    }
)

File Protection Hook

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])
        ]
    }
)

Input Sanitization 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])]
    }
)

Important Notes

Python Keyword Conflicts

Use underscored versions in Python code:

  • async_ instead of async
  • continue_ instead of continue

These are automatically converted to the correct field names when sent to the CLI.

Hook Execution Order

When multiple hooks match, they execute in order defined in the hooks list.

Error Handling

Exceptions in hooks are caught and logged but don't break execution. Always handle errors gracefully.

Performance

Hooks are called synchronously in the agent loop. Keep hook logic fast to avoid delays.

Hook Limitations

The Python SDK does not support SessionStart, SessionEnd, and Notification hooks due to setup limitations.

docs

agent-definitions.md

agents.md

client.md

configuration-options.md

content-blocks.md

core-query-interface.md

custom-tools.md

error-handling.md

errors.md

hook-system.md

hooks.md

index.md

mcp-config.md

mcp-server-configuration.md

messages-and-content.md

messages.md

options.md

permission-control.md

permissions.md

query.md

transport.md

COMPLETION_SUMMARY.md

tile.json