# Hook Patterns and Configuration This guide covers hook patterns for intercepting and modifying Claude Agent SDK behavior. ## Overview Hooks allow you to intercept SDK events and modify behavior at key points in execution. Common uses: - Control tool execution (approve/deny/modify) - Add context to prompts - Review tool outputs - Stop execution on errors - Log activity > **⚠️ IMPORTANT:** The Python SDK does **NOT** support `SessionStart`, `SessionEnd`, or `Notification` hooks due to setup limitations. Only the 6 hook types listed below are supported. Attempting to use unsupported hooks will result in them never firing. ## Hook Types | Hook | When It Fires | Common Uses | |------|---------------|-------------| | `PreToolUse` | Before tool execution | Approve/deny/modify tool calls | | `PostToolUse` | After tool execution | Review output, add context, stop on errors | | `UserPromptSubmit` | Before processing user prompt | Add context, modify prompt | | `Stop` | When execution stops | Cleanup, final logging | | `SubagentStop` | When a subagent stops | Capture subagent results, cleanup | | `PreCompact` | Before message compaction | Review/modify messages before compaction | ## Hook Configuration Hooks are configured via the `hooks` parameter in `ClaudeAgentOptions`: ```python from claude_agent_sdk import ClaudeAgentOptions, HookMatcher options = ClaudeAgentOptions( hooks={ "PreToolUse": [ HookMatcher(matcher="Bash", hooks=[check_bash_command]), ], "PostToolUse": [ HookMatcher(matcher="Bash", hooks=[review_output]), ], } ) ``` ## Hook Function Signature All hooks have the same signature: ```python from claude_agent_sdk.types import HookInput, HookContext, HookJSONOutput async def hook_function( input_data: HookInput, tool_use_id: str | None, context: HookContext ) -> HookJSONOutput: """ Args: input_data: Hook-specific data (tool_name, tool_input, etc.) tool_use_id: Unique ID for this tool use (if applicable) context: Additional context about the execution Returns: HookJSONOutput: Dict with hook-specific fields """ return {} # Empty dict = no action ``` ## HookJSONOutput Fields | Field | Type | Use Case | Description | |-------|------|----------|-------------| | `continue_` | `bool` | Stop execution | Set to `False` to halt execution | | `stopReason` | `str` | Stop execution | Explanation for why execution stopped | | `reason` | `str` | Logging/debugging | Explanation of hook decision | | `systemMessage` | `str` | User feedback | Message shown to user | | `hookSpecificOutput` | `dict` | Hook-specific data | Additional hook-specific fields | ### PreToolUse Hook-Specific Fields ```python { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow" | "deny", # Control tool execution "permissionDecisionReason": "Explanation for decision", "modifiedInput": {...} # Optional: Modified tool input } } ``` ### PostToolUse Hook-Specific Fields ```python { "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Extra context based on tool output" } } ``` ### UserPromptSubmit Hook-Specific Fields ```python { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "updatedPrompt": "Modified prompt text" } } ``` ### Stop Hook-Specific Fields ```python { "hookSpecificOutput": { "hookEventName": "Stop" } } ``` ### SubagentStop Hook-Specific Fields ```python { "hookSpecificOutput": { "hookEventName": "SubagentStop" } } ``` ### PreCompact Hook-Specific Fields ```python { "hookSpecificOutput": { "hookEventName": "PreCompact", "additionalContext": "Context to preserve during compaction" } } ``` ## Common Patterns ### 1. Block Dangerous Commands (PreToolUse) ```python async def check_bash_command( input_data: HookInput, tool_use_id: str | None, context: HookContext ) -> HookJSONOutput: """Prevent dangerous bash commands.""" if input_data["tool_name"] != "Bash": return {} command = input_data["tool_input"].get("command", "") dangerous_patterns = ["rm -rf", "sudo", "chmod 777", "dd if="] for pattern in dangerous_patterns: if pattern in command: return { "reason": f"Blocked dangerous command pattern: {pattern}", "systemMessage": f"🚫 Blocked: {pattern}", "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": f"Command contains: {pattern}" } } return {} # Allow by default ``` ### 2. Review Tool Output (PostToolUse) ```python async def review_tool_output( input_data: HookInput, tool_use_id: str | None, context: HookContext ) -> HookJSONOutput: """Add context based on tool output.""" tool_response = input_data.get("tool_response", "") if "error" in str(tool_response).lower(): return { "systemMessage": "⚠️ Command produced an error", "reason": "Tool execution failed", "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Consider checking command syntax or permissions." } } return {} ``` ### 4. Stop on Critical Errors (PostToolUse) ```python async def stop_on_critical_error( input_data: HookInput, tool_use_id: str | None, context: HookContext ) -> HookJSONOutput: """Halt execution on critical errors.""" tool_response = input_data.get("tool_response", "") if "critical" in str(tool_response).lower(): return { "continue_": False, "stopReason": "Critical error detected - halting for safety", "systemMessage": "🛑 Execution stopped: critical error" } return {"continue_": True} ``` ### 5. Redirect File Writes (PreToolUse) ```python async def safe_file_writes( input_data: HookInput, tool_use_id: str | None, context: HookContext ) -> HookJSONOutput: """Redirect writes to safe directory.""" if input_data["tool_name"] not in ["Write", "Edit"]: return {} file_path = input_data["tool_input"].get("file_path", "") # Block writes to system directories if file_path.startswith("/etc/") or file_path.startswith("/usr/"): return { "reason": f"Blocked write to system directory: {file_path}", "systemMessage": "🚫 Cannot write to system directories", "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "System directory protection" } } # Redirect to safe directory if not file_path.startswith("./safe/"): safe_path = f"./safe/{file_path.split('/')[-1]}" modified_input = input_data["tool_input"].copy() modified_input["file_path"] = safe_path return { "reason": f"Redirected write from {file_path} to {safe_path}", "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "Redirected to safe directory", "modifiedInput": modified_input } } return {} ``` ## Hook Matcher Patterns `HookMatcher` determines when hooks fire: ```python # Match specific tool HookMatcher(matcher="Bash", hooks=[check_bash]) # Match all tools HookMatcher(matcher=None, hooks=[log_all_tools]) # Multiple hooks for same matcher HookMatcher( matcher="Write", hooks=[check_permissions, log_write, backup_file] ) ``` ## Complete Example ```python from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher async def block_dangerous_bash(input_data, tool_use_id, context): if input_data["tool_name"] != "Bash": return {} command = input_data["tool_input"].get("command", "") if "rm -rf" in command: return { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Dangerous rm -rf detected" } } return {} options = ClaudeAgentOptions( allowed_tools=["Bash", "Read", "Write"], hooks={ "PreToolUse": [ HookMatcher(matcher="Bash", hooks=[block_dangerous_bash]) ] } ) async with ClaudeSDKClient(options=options) as client: await client.query("Run a safe bash command") async for message in client.receive_response(): # Process messages pass ``` ## Best Practices 1. **Return early** - Return empty dict `{}` when hook doesn't apply 2. **Be specific** - Clear `reason` and `systemMessage` fields help debugging 3. **Use matchers** - Filter hooks to relevant tools with `matcher` parameter 4. **Chain hooks** - Multiple hooks can process same event 5. **Handle errors** - Hooks should be defensive and handle missing data 6. **Log decisions** - Use `reason` field to explain hook decisions ## Anti-Patterns ❌ **Blocking without explanation** ```python return { "hookSpecificOutput": { "permissionDecision": "deny" # Missing permissionDecisionReason } } ``` ✅ **Clear explanations** ```python return { "reason": "Command contains dangerous pattern: rm -rf", "systemMessage": "🚫 Blocked dangerous command", "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Safety: rm -rf detected" } } ``` ❌ **Ignoring tool_name in PreToolUse** ```python # This will fire for ALL tools async def hook(input_data, tool_use_id, context): command = input_data["tool_input"]["command"] # Crashes on non-Bash tools ``` ✅ **Filter by tool_name** ```python async def hook(input_data, tool_use_id, context): if input_data["tool_name"] != "Bash": return {} command = input_data["tool_input"].get("command", "") ```