> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/anthropics/claude-agent-sdk-python/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks Implementation Examples

> Customize SDK behavior with hooks for advanced control over tool execution

Hooks provide advanced control over the SDK's behavior at different stages of execution. Use hooks to add custom logic, validate inputs, modify behavior, or stop execution based on conditions.

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

```python theme={null}
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:

```python theme={null}
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
```

<Note>
  When `matcher=None`, the hook applies to all operations regardless of tool name.
</Note>

## PostToolUse Hook

Review tool output and provide feedback:

```python theme={null}
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

Use `permissionDecision` to explicitly allow or deny operations:

```python theme={null}
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

Use `continue_=False` to halt execution on critical errors:

```python theme={null}
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 a `HookJSONOutput` dictionary with optional fields:

```python theme={null}
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:

```python theme={null}
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

<Accordion title="Full working example with multiple hook types">
  ```python theme={null}
  #!/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())
  ```
</Accordion>

## Hook Execution Order

1. **SessionStart** - When a new session begins
2. **UserPromptSubmit** - When a user submits a prompt
3. **PreToolUse** - Before each tool is executed
4. **PostToolUse** - After each tool completes

Multiple hooks of the same type execute in the order they're defined.

## 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 `permissionDecision` to explicitly allow/deny operations
* Use `continue_=False` to stop execution on critical conditions
* Configure hooks in the `hooks` parameter of `ClaudeAgentOptions`

## Next Steps

<CardGroup cols={2}>
  <Card title="Tool Permissions" icon="shield-check" href="/examples/tool-permissions">
    Simpler permission control with callbacks
  </Card>

  <Card title="API Reference" icon="book" href="/api/types/hooks">
    Complete hooks API documentation
  </Card>
</CardGroup>
