# Hook Reference Comprehensive technical reference for Claude Code event hooks. ## Table of Contents 1. [Hook Configuration Schema](#hook-configuration-schema) 2. [Hook Events](#hook-events) 3. [Matcher Patterns](#matcher-patterns) 4. [Input Format](#input-format) 5. [Output Format](#output-format) 6. [Environment Variables](#environment-variables) 7. [Exit Codes](#exit-codes) 8. [Hook Chaining](#hook-chaining) 9. [Security Best Practices](#security-best-practices) 10. [MCP Integration](#mcp-integration) 11. [Plugin Hooks](#plugin-hooks) 12. [Advanced Patterns](#advanced-patterns) ## Hook Configuration Schema ### Location Hooks are configured in JSON settings files: | Location | Scope | Committed | |----------|-------|-----------| | `~/.claude/settings.json` | Personal (all projects) | No | | `.claude/settings.json` | Project (shared with team) | Yes | | `.claude/settings.local.json` | Project (local overrides) | No | | `plugin/hooks/hooks.json` | Plugin | Yes | ### Basic Structure ```json { "hooks": { "": [ { "matcher": "", "hooks": [ { "type": "command", "command": "", "timeout": 30 } ] } ] } } ``` ### Field Reference #### `hooks` (root) **Type**: Object **Required**: Yes **Description**: Root object containing all hook definitions ```json { "hooks": { // Event configurations here } } ``` #### Event Name Keys **Type**: String (key) **Required**: At least one **Valid values**: - `PreToolUse` - `PostToolUse` - `UserPromptSubmit` - `Notification` - `Stop` - `SubagentStop` - `PreCompact` - `SessionStart` - `SessionEnd` **Description**: Event type that triggers the hook ```json { "hooks": { "PreToolUse": [...], "PostToolUse": [...], "SessionStart": [...] } } ``` #### Event Configuration Array **Type**: Array of objects **Description**: Array of matcher/hooks pairs for an event ```json { "hooks": { "PostToolUse": [ { "matcher": "Write(*.py)", "hooks": [...] }, { "matcher": "Edit(*.ts)", "hooks": [...] } ] } } ``` #### `matcher` **Type**: String **Required**: Yes **Description**: Pattern to match tools or event types **Syntax options:** - Simple: `"Write"` - Exact tool name - Regex: `"Edit|Write"` - OR pattern - Wildcard: `"*"` - All tools - File pattern: `"Write(*.py)"` - File extension - MCP: `"mcp__server__tool"` - MCP tool pattern ```json {"matcher": "Write|Edit"} ``` #### `hooks` (nested) **Type**: Array of objects **Required**: Yes **Description**: Commands to execute when matcher triggers ```json { "hooks": [ { "type": "command", "command": "black \"$file\"", "timeout": 30 } ] } ``` #### `type` **Type**: String **Required**: Yes **Valid values**: `"command"` **Description**: Hook execution type (currently only "command" supported) #### `command` **Type**: String **Required**: Yes **Description**: Shell command to execute **Features:** - Variable expansion: `$file`, `$CLAUDE_PROJECT_DIR` - Stdin: Receives JSON input - Stdout: Shown to user - Stderr: Error messages - Exit code: Controls behavior ```json { "type": "command", "command": "./.claude/hooks/format-code.sh" } ``` #### `timeout` **Type**: Number (seconds) **Required**: No **Default**: 30 **Description**: Maximum execution time ```json { "type": "command", "command": "./slow-operation.sh", "timeout": 60 } ``` ### Complete Example ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.sh", "timeout": 5 } ] }, { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-paths.sh", "timeout": 3 } ] } ], "PostToolUse": [ { "matcher": "Write(*.ts)", "hooks": [ { "type": "command", "command": "biome check --write \"$file\"", "timeout": 10 } ] }, { "matcher": "Write(*.py)", "hooks": [ { "type": "command", "command": "black \"$file\"", "timeout": 10 } ] } ], "SessionStart": [ { "matcher": "startup", "hooks": [ { "type": "command", "command": "echo 'Session started' && git status", "timeout": 5 } ] } ] } } ``` ## Hook Events ### PreToolUse Executes **before** a tool runs. Can block or modify execution. **Timing**: After Claude creates tool parameters, before tool execution **Input**: Tool name and full input parameters **Can block**: Yes (exit code 2) **Common matchers**: - `Bash` - Shell commands - `Write` - File writing - `Edit` - File editing - `Read` - File reading - `Grep` - Content search - `Glob` - File patterns - `WebFetch` - Web operations - `WebSearch` - Web search - `Task` - Subagent tasks - `*` - All tools **Use cases**: - Validate bash commands before execution - Check file paths for security issues - Block dangerous operations - Add context before execution - Enforce security policies - Log tool invocations **Example**: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "./.claude/hooks/validate-bash.sh", "timeout": 5 }] } ] } } ``` ### PostToolUse Executes **after** a tool completes successfully. **Timing**: Immediately after tool returns success **Input**: Tool name, input parameters, and execution result **Can block**: No (but can report issues) **Common matchers**: - `Write(*.ext)` - Specific file types - `Edit(*.ext)` - Specific file types - `Write|Edit` - Any file modification - `*` - All successful tools **Use cases**: - Auto-format code files - Run linters - Update documentation - Trigger builds - Send notifications - Update indexes **Example**: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit(*.ts)", "hooks": [{ "type": "command", "command": "biome check --write \"$file\"", "timeout": 10 }] } ] } } ``` ### UserPromptSubmit Executes when user submits a prompt to Claude. **Timing**: After user submits, before Claude processes **Input**: User prompt text and session metadata **Can block**: No **Matcher**: Always `*` **Use cases**: - Add timestamp or date context - Add environment information - Log user activity - Pre-process or augment prompts - Add project context **Example**: ```json { "hooks": { "UserPromptSubmit": [ { "matcher": "*", "hooks": [{ "type": "command", "command": "./.claude/hooks/add-context.sh", "timeout": 2 }] } ] } } ``` ### Notification Executes when Claude Code sends a notification. **Timing**: When notification is triggered **Input**: Notification message and metadata **Can block**: No **Matcher**: Always `*` **Use cases**: - Send to external systems (Slack, email) - Log notifications - Trigger alerts - Update dashboards - Archive important messages **Example**: ```json { "hooks": { "Notification": [ { "matcher": "*", "hooks": [{ "type": "command", "command": "./.claude/hooks/send-to-slack.sh", "timeout": 5 }] } ] } } ``` ### Stop Executes when main Claude agent finishes responding. **Timing**: After Claude completes response **Input**: Session metadata and completion reason **Can block**: No **Matcher**: Always `*` **Use cases**: - Clean up temporary resources - Send completion notifications - Update external systems - Log session metrics - Archive conversation **Example**: ```json { "hooks": { "Stop": [ { "matcher": "*", "hooks": [{ "type": "command", "command": "./.claude/hooks/on-completion.sh", "timeout": 5 }] } ] } } ``` ### SubagentStop Executes when a subagent (Task tool) finishes. **Timing**: After subagent completes **Input**: Subagent metadata and result **Can block**: No **Matcher**: Always `*` **Use cases**: - Track subagent usage - Log subagent results - Trigger follow-up actions - Update metrics - Debug subagent behavior **Example**: ```json { "hooks": { "SubagentStop": [ { "matcher": "*", "hooks": [{ "type": "command", "command": "./.claude/hooks/log-subagent.sh", "timeout": 3 }] } ] } } ``` ### PreCompact Executes before conversation compacts. **Timing**: Before compact operation starts **Input**: Compact trigger type **Can block**: No **Matchers**: - `manual` - User triggered via `/compact` - `auto` - Automatic compact **Use cases**: - Backup conversation - Archive important context - Update external summaries - Log compact events - Prepare for reset **Example**: ```json { "hooks": { "PreCompact": [ { "matcher": "manual", "hooks": [{ "type": "command", "command": "./.claude/hooks/backup-conversation.sh", "timeout": 10 }] } ] } } ``` ### SessionStart Executes when session starts or resumes. **Timing**: At session initialization **Input**: Session start reason **Can block**: No **Matchers**: - `startup` - Claude Code starts - `resume` - Session resumes (`--resume`, `--continue`) - `clear` - After `/clear` command - `compact` - After compact operation **Use cases**: - Display welcome message - Show git status - Load project context - Check for updates - Initialize resources **Example**: ```json { "hooks": { "SessionStart": [ { "matcher": "startup", "hooks": [{ "type": "command", "command": "echo 'Welcome!' && git status", "timeout": 5 }] } ] } } ``` ### SessionEnd Executes when session ends. **Timing**: Before session terminates **Input**: End reason **Can block**: No **Matchers** (reasons): - `clear` - User ran `/clear` - `logout` - User logged out - `prompt_input_exit` - Exited during prompt input - `other` - Other reasons **Use cases**: - Clean up resources - Save state - Log session metrics - Send completion notifications - Archive transcripts **Example**: ```json { "hooks": { "SessionEnd": [ { "matcher": "*", "hooks": [{ "type": "command", "command": "./.claude/hooks/cleanup.sh", "timeout": 5 }] } ] } } ``` ## Matcher Patterns ### Simple String Match Match exact tool name: ```json {"matcher": "Write"} // Only Write tool {"matcher": "Edit"} // Only Edit tool {"matcher": "Bash"} // Only Bash tool {"matcher": "Read"} // Only Read tool ``` ### Regex Patterns Use `|` for OR logic: ```json {"matcher": "Edit|Write"} // Edit OR Write {"matcher": "Read|Grep|Glob"} // Any read operation {"matcher": "Notebook.*"} // Any Notebook tool {"matcher": "Write|Edit|NotebookEdit"} // Multiple tools ``` **Regex features:** - `|` - OR operator - `.` - Any character - `*` - Zero or more - `+` - One or more - `^` - Start of string - `$` - End of string **Examples:** ```json {"matcher": "^Write$"} // Exactly "Write", no prefix/suffix {"matcher": ".*Edit.*"} // Contains "Edit" anywhere {"matcher": "Bash|WebFetch"} // Bash or WebFetch ``` ### Wildcard Match Match all tools: ```json {"matcher": "*"} // Matches everything ``` **Use cases:** - Logging all tool usage - Global validation - Universal context injection - Metrics collection ### File Pattern Match Match tools with specific file patterns: ```json {"matcher": "Write(*.py)"} // Write Python files {"matcher": "Edit(*.ts)"} // Edit TypeScript files {"matcher": "Write(*.md)"} // Write Markdown files {"matcher": "Write|Edit(*.js)"} // Write or Edit JavaScript ``` **Supported patterns:** - `*.ext` - Any file with extension - `path/*.ext` - Files in specific directory - `**/*.ext` - Recursive file match **Examples:** ```json {"matcher": "Write(*.tsx)"} // React components {"matcher": "Write|Edit(*.rs)"} // Rust files {"matcher": "Write(src/**/*.ts)"} // TS files in src/ {"matcher": "Edit(.env*)"} // .env files ``` ### MCP Tool Match Match MCP server tools: ```json {"matcher": "mcp__memory__.*"} // Any memory MCP tool {"matcher": "mcp__github__.*"} // Any GitHub MCP tool {"matcher": "mcp__.*__.*"} // Any MCP tool {"matcher": "mcp__linear__create_issue"} // Specific MCP tool ``` **MCP tool naming**: `mcp____` **Examples:** ```json // Match all memory operations {"matcher": "mcp__memory__.*"} // Match specific GitHub operations {"matcher": "mcp__github__(create_issue|create_comment)"} // Match all MCP tools {"matcher": "mcp__.*__.*"} // Match Linear issue creation {"matcher": "mcp__linear__create_issue"} ``` ### Complex Matchers Combine patterns with regex: ```json // Format Python or TypeScript files {"matcher": "Write|Edit(*.py)|Write|Edit(*.ts)"} // Format code files, exclude tests {"matcher": "Write|Edit(*.ts|*.py)"} // Bash or any MCP tool {"matcher": "Bash|mcp__.*__.*"} // Read operations (multiple tools) {"matcher": "Read|Grep|Glob|WebFetch"} ``` ## Input Format ### JSON Schema Hooks receive JSON on stdin: ```typescript interface HookInput { session_id: string; transcript_path: string; cwd: string; hook_event_name: string; tool_name?: string; tool_input?: Record; reason?: string; [key: string]: any; } ``` ### Common Fields #### All Events ```json { "session_id": "abc123-def456-ghi789", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/current/working/directory", "hook_event_name": "PreToolUse" } ``` #### Tool Events (PreToolUse, PostToolUse) ```json { "session_id": "abc123", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/project/root", "hook_event_name": "PreToolUse", "tool_name": "Write", "tool_input": { "file_path": "/project/root/src/file.ts", "content": "export const foo = 'bar';" } } ``` #### Session Events ```json { "session_id": "abc123", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/project/root", "hook_event_name": "SessionStart", "reason": "startup" } ``` ### Tool-Specific Input #### Bash Tool ```json { "tool_name": "Bash", "tool_input": { "command": "git status", "description": "Check git status" } } ``` #### Write Tool ```json { "tool_name": "Write", "tool_input": { "file_path": "/absolute/path/to/file.ts", "content": "file contents here" } } ``` #### Edit Tool ```json { "tool_name": "Edit", "tool_input": { "file_path": "/absolute/path/to/file.ts", "old_string": "const foo = 'old';", "new_string": "const foo = 'new';", "replace_all": false } } ``` #### Read Tool ```json { "tool_name": "Read", "tool_input": { "file_path": "/absolute/path/to/file.ts", "offset": 0, "limit": 2000 } } ``` ### Reading Input #### Bash ```bash #!/usr/bin/env bash set -euo pipefail # Read entire input INPUT=$(cat) # Parse with jq TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') # Check if field exists if [[ -z "$TOOL_NAME" ]]; then echo "Error: tool_name not found" >&2 exit 1 fi ``` #### Bun/TypeScript ```typescript #!/usr/bin/env bun import { stdin } from "process"; interface HookInput { session_id: string; tool_name?: string; tool_input?: Record; hook_event_name: string; } // Read stdin const chunks: Buffer[] = []; for await (const chunk of stdin) { chunks.push(chunk); } const input: HookInput = JSON.parse(Buffer.concat(chunks).toString()); // Access fields const toolName = input.tool_name; const filePath = input.tool_input?.file_path; // Validate if (!toolName) { console.error("Error: tool_name missing"); process.exit(1); } ``` #### Python ```python #!/usr/bin/env python3 import json import sys # Read input try: input_data = json.load(sys.stdin) except json.JSONDecodeError as e: print(f"Error parsing JSON: {e}", file=sys.stderr) sys.exit(1) # Access fields tool_name = input_data.get("tool_name", "") file_path = input_data.get("tool_input", {}).get("file_path", "") # Validate if not tool_name: print("Error: tool_name missing", file=sys.stderr) sys.exit(1) ``` ## Output Format ### Exit Codes (Simple) Most common approach: ```bash #!/usr/bin/env bash # Success - continue execution echo "Validation passed" exit 0 # Blocking error - show to Claude echo "Error: dangerous operation detected" >&2 exit 2 # Non-blocking error - show to user echo "Warning: minor issue detected" >&2 exit 1 ``` **Behavior:** | Exit Code | Behavior | Stdout | Stderr | |-----------|----------|--------|--------| | 0 | Success | Shown to user | Ignored | | 2 | Block (PreToolUse only) | Ignored | Shown to Claude | | 1 or other | Non-blocking error | Ignored | Shown to user | ### JSON Output (Advanced) For complex responses: ```json { "continue": true, "stopReason": "Optional stop message", "suppressOutput": false, "systemMessage": "Warning or info message", "decision": "block", "reason": "Explanation for decision", "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Dangerous operation", "additionalContext": "Context for Claude" } } ``` #### Field Reference **`continue`** (boolean) - `true`: Continue execution - `false`: Stop execution **`stopReason`** (string) - Message explaining why stopped - Shown to user **`suppressOutput`** (boolean) - `true`: Hide stdout from user - `false`: Show stdout **`systemMessage`** (string) - Info/warning message - Shown to user **`decision`** (string) - `"block"`: Block operation (PreToolUse) - `"approve"`: Approve operation - `undefined`: No decision **`reason`** (string) - Explanation for decision - Shown in context **`hookSpecificOutput`** (object) - Event-specific data - See below for details #### PreToolUse JSON Output ```json { "continue": false, "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Path traversal detected in file path", "additionalContext": "The file path contains '..' which could allow directory traversal" } } ``` **Permission decisions:** - `"allow"`: Approve tool use - `"deny"`: Block tool use - `"ask"`: Ask user for permission #### Example: Bash with JSON Output ```bash #!/usr/bin/env bash set -euo pipefail INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Check for path traversal if echo "$FILE_PATH" | grep -q '\.\.'; then # Output JSON response cat << EOF { "continue": false, "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Path traversal detected", "additionalContext": "File path contains '..' which is not allowed" } } EOF exit 0 fi # Approve echo "Path validation passed" exit 0 ``` ## Environment Variables ### Available Variables #### `$CLAUDE_PROJECT_DIR` **Type**: String **Availability**: All hooks **Description**: Absolute path to project root directory ```bash "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh" ``` **Use cases:** - Reference project scripts - Construct relative paths - Check project structure #### `$file` **Type**: String **Availability**: PostToolUse hooks for Write/Edit tools **Description**: Absolute path to affected file ```bash "biome check --write \"$file\"" ``` **Use cases:** - Auto-format files - Run linters - Update related files #### `${CLAUDE_PLUGIN_ROOT}` **Type**: String **Availability**: Plugin hooks only **Description**: Absolute path to plugin root directory ```json { "command": "${CLAUDE_PLUGIN_ROOT}/scripts/process.sh" } ``` **Use cases:** - Reference plugin scripts - Load plugin resources - Access plugin data ### Custom Variables Define in settings.json: ```json { "env": { "CUSTOM_VAR": "value", "API_KEY": "secret" }, "hooks": { "PostToolUse": [...] } } ``` Access in hooks: ```bash #!/usr/bin/env bash echo "Custom var: $CUSTOM_VAR" ``` ## Exit Codes ### Standard Exit Codes ```bash 0 - Success, continue 1 - Non-blocking error 2 - Blocking error (PreToolUse only) 3+ - Non-blocking error ``` ### Exit Code Behavior #### Exit 0 (Success) ```bash #!/usr/bin/env bash echo "Validation passed" exit 0 ``` **Behavior:** - Execution continues - Stdout shown to user - Stderr ignored **Use for:** - Successful validation - Informational output - Non-critical messages #### Exit 1 (Warning) ```bash #!/usr/bin/env bash echo "Warning: potential issue detected" >&2 exit 1 ``` **Behavior:** - Execution continues - Stderr shown to user - Stdout ignored **Use for:** - Warnings - Non-critical issues - Suggestions #### Exit 2 (Block) ```bash #!/usr/bin/env bash echo "Error: dangerous operation blocked" >&2 exit 2 ``` **Behavior:** - PreToolUse: Blocks tool execution - PostToolUse: Reports error (doesn't block) - Stderr shown to Claude - Stdout ignored **Use for:** - Security violations - Policy enforcement - Dangerous operations ### Error Handling Always handle errors gracefully: ```bash #!/usr/bin/env bash set -euo pipefail # Check dependencies if ! command -v jq &>/dev/null; then echo "Error: jq not installed" >&2 exit 1 fi # Validate input INPUT=$(cat) || { echo "Error: failed to read stdin" >&2 exit 1 } # Parse with error handling TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') || { echo "Error: failed to parse JSON" >&2 exit 1 } # Validate required fields if [[ -z "$TOOL_NAME" ]]; then echo "Error: tool_name missing" >&2 exit 1 fi ``` ## Hook Chaining ### Multiple Hooks per Event Execute multiple hooks sequentially: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write(*.ts)", "hooks": [ { "type": "command", "command": "biome check --write \"$file\"", "timeout": 10 }, { "type": "command", "command": "tsc --noEmit \"$file\"", "timeout": 15 }, { "type": "command", "command": "./.claude/hooks/update-index.sh", "timeout": 5 } ] } ] } } ``` **Execution:** - Runs in order - If one fails (non-zero exit), subsequent hooks still run - All output collected and shown ### Cross-Event Coordination Use shared state for coordination: ```bash # PreToolUse: Record operation #!/usr/bin/env bash INPUT=$(cat) TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') echo "$TOOL_NAME $(date +%s)" >> /tmp/claude-operations.log exit 0 ``` ```bash # PostToolUse: Update metrics #!/usr/bin/env bash OPERATIONS=$(wc -l < /tmp/claude-operations.log) echo "Total operations: $OPERATIONS" >&2 exit 0 ``` ## Security Best Practices ### 1. Input Validation Always validate and sanitize inputs: ```bash #!/usr/bin/env bash set -euo pipefail INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Check for path traversal if echo "$FILE_PATH" | grep -qE '\.\.|^/etc/|^/root/|^/home/[^/]+/\.ssh/'; then echo "❌ Dangerous path detected: $FILE_PATH" >&2 exit 2 fi # Check for sensitive files if echo "$FILE_PATH" | grep -qE '\.env$|\.git/config|id_rsa|credentials'; then echo "❌ Sensitive file access blocked: $FILE_PATH" >&2 exit 2 fi # Validate file extension if [[ "$FILE_PATH" =~ \.(exe|sh|bin)$ ]]; then echo "⚠ Warning: executable file" >&2 fi ``` ### 2. Command Injection Prevention Always quote variables: ```bash # ❌ WRONG - vulnerable to injection rm $FILE_PATH # ✅ CORRECT - properly quoted rm "$FILE_PATH" # ❌ WRONG - vulnerable eval "$COMMAND" # ✅ CORRECT - use array or avoid eval bash -c "$COMMAND" ``` ### 3. Path Security Use absolute paths and validate: ```bash #!/usr/bin/env bash # Get absolute path SCRIPT_PATH="$CLAUDE_PROJECT_DIR/.claude/hooks/helper.sh" # Validate script exists if [[ ! -f "$SCRIPT_PATH" ]]; then echo "Error: script not found: $SCRIPT_PATH" >&2 exit 1 fi # Validate script is executable if [[ ! -x "$SCRIPT_PATH" ]]; then echo "Error: script not executable: $SCRIPT_PATH" >&2 exit 1 fi # Execute safely "$SCRIPT_PATH" "$@" ``` ### 4. Sensitive Data Protection Never log or expose sensitive data: ```bash #!/usr/bin/env bash INPUT=$(cat) # ❌ WRONG - logs sensitive data echo "Input: $INPUT" >> /tmp/debug.log # ✅ CORRECT - log only safe fields TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') echo "Tool: $TOOL_NAME" >> /tmp/debug.log # ✅ Filter sensitive fields echo "$INPUT" | jq 'del(.tool_input.password, .tool_input.api_key)' >> /tmp/debug.log ``` ### 5. Timeout Protection Set appropriate timeouts: ```json { "hooks": { "PreToolUse": [ { "hooks": [{ "type": "command", "command": "./.claude/hooks/validate.sh", "timeout": 5 }] } ], "PostToolUse": [ { "hooks": [{ "type": "command", "command": "./.claude/hooks/format.sh", "timeout": 30 }] } ] } } ``` **Guidelines:** - Validation: 3-5 seconds - Formatting: 10-30 seconds - Network operations: 30-60 seconds - Heavy operations: Consider running async ### 6. Error Recovery Handle failures gracefully: ```bash #!/usr/bin/env bash set -euo pipefail # Trap errors trap 'echo "Error on line $LINENO" >&2' ERR # Validate dependencies for cmd in jq git; do if ! command -v "$cmd" &>/dev/null; then echo "Error: $cmd not installed" >&2 exit 1 fi done # Main logic with error handling if ! INPUT=$(cat 2>&1); then echo "Error: failed to read stdin" >&2 exit 1 fi # Parse with validation if ! TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name' 2>&1); then echo "Error: invalid JSON input" >&2 exit 1 fi ``` ## MCP Integration ### Matching MCP Tools MCP tools follow pattern: `mcp____` ```json { "hooks": { "PreToolUse": [ { "matcher": "mcp__memory__.*", "hooks": [{ "type": "command", "command": "./.claude/hooks/log-memory-ops.sh" }] }, { "matcher": "mcp__github__create_issue", "hooks": [{ "type": "command", "command": "./.claude/hooks/validate-issue.sh" }] } ] } } ``` ### Common MCP Servers ```json // Memory operations {"matcher": "mcp__memory__.*"} // GitHub operations {"matcher": "mcp__github__.*"} // Linear operations {"matcher": "mcp__linear__.*"} // Filesystem operations {"matcher": "mcp__filesystem__.*"} // All MCP tools {"matcher": "mcp__.*__.*"} ``` ### MCP Hook Example ```bash #!/usr/bin/env bash # Log MCP operations set -euo pipefail INPUT=$(cat) TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') SERVER=$(echo "$TOOL_NAME" | cut -d'_' -f3) OPERATION=$(echo "$TOOL_NAME" | cut -d'_' -f4-) echo "[$(date -Iseconds)] MCP $SERVER: $OPERATION" >> "$CLAUDE_PROJECT_DIR/.claude/mcp-operations.log" exit 0 ``` ## Plugin Hooks ### Plugin Hook Configuration Hooks are **auto-discovered** from `{plugin}/hooks/hooks.json`. Do NOT define hooks in `plugin.json`. **Location:** `{plugin}/hooks/hooks.json` **Important:** The file requires a root-level `"hooks"` wrapper around the event types: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format-code.sh", "timeout": 30 }] } ] } } ``` > **Note:** This differs from project-level hooks in `.claude/settings.json`, which also have a `"hooks"` wrapper but are configured differently. Plugin hooks use `${CLAUDE_PLUGIN_ROOT}` for paths. ### Plugin-Specific Variables Use `${CLAUDE_PLUGIN_ROOT}` for plugin paths: ```json { "command": "${CLAUDE_PLUGIN_ROOT}/scripts/helper.sh" } ``` ### Plugin Hook Best Practices **1. Use relative paths with variable:** ```json { "command": "${CLAUDE_PLUGIN_ROOT}/scripts/process.sh" } ``` **2. Include dependencies in plugin:** ``` plugin/ ├── .claude-plugin/ │ └── plugin.json ├── hooks/ │ └── hooks.json └── scripts/ ├── process.sh └── utils.sh ``` **3. Document hook requirements:** ```json { "name": "my-plugin", "description": "Plugin with auto-formatting", "requirements": { "binaries": ["jq", "black"], "notes": "PostToolUse hooks require black for Python formatting" } } ``` ## Advanced Patterns ### Conditional Execution ```bash #!/usr/bin/env bash set -euo pipefail INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Only format during work hours HOUR=$(date +%H) if [[ $HOUR -lt 9 || $HOUR -gt 17 ]]; then echo "Skipping format outside work hours" exit 0 fi # Only format if file is in src/ if [[ ! "$FILE_PATH" =~ ^.*/src/ ]]; then exit 0 fi # Format the file black "$FILE_PATH" ``` ### Async Operations ```bash #!/usr/bin/env bash # Run expensive operation in background INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Start background job ( sleep 2 expensive-operation "$FILE_PATH" echo "Background operation completed" >> /tmp/claude-bg.log ) & # Return immediately echo "Background operation started" exit 0 ``` ### State Management ```bash #!/usr/bin/env bash # Track state across hooks STATE_FILE="/tmp/claude-state.json" INPUT=$(cat) TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') # Load state if [[ -f "$STATE_FILE" ]]; then STATE=$(cat "$STATE_FILE") else STATE='{"operations": []}' fi # Update state STATE=$(echo "$STATE" | jq ".operations += [\"$TOOL_NAME\"]") echo "$STATE" > "$STATE_FILE" # Report COUNT=$(echo "$STATE" | jq '.operations | length') echo "Total operations: $COUNT" exit 0 ``` ### Multi-File Operations ```bash #!/usr/bin/env bash # Update related files INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # If component updated, update index if [[ "$FILE_PATH" =~ /components/.*\.tsx$ ]]; then INDEX_FILE="$(dirname "$FILE_PATH")/index.ts" # Regenerate index echo "// Auto-generated by hook" > "$INDEX_FILE" for file in "$(dirname "$FILE_PATH")"/*.tsx; do NAME=$(basename "$file" .tsx) echo "export { $NAME } from './$NAME';" >> "$INDEX_FILE" done echo "Updated $INDEX_FILE" fi exit 0 ``` ### Notification Integration ```bash #!/usr/bin/env bash # Send Slack notification INPUT=$(cat) TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name') FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Only notify for important files if [[ "$FILE_PATH" =~ /src/core/ ]]; then WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}" if [[ -n "$WEBHOOK_URL" ]]; then curl -X POST "$WEBHOOK_URL" \ -H 'Content-Type: application/json' \ -d "{\"text\":\"Core file modified: $(basename "$FILE_PATH")\"}" \ 2>/dev/null fi fi exit 0 ``` ### Validation Pipeline ```bash #!/usr/bin/env bash # Multi-stage validation INPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') # Stage 1: Check for dangerous commands if echo "$COMMAND" | grep -qE '\brm\s+-rf\s+/|\bmkfs\b|\bdd\s+if='; then echo "❌ Dangerous command blocked" >&2 exit 2 fi # Stage 2: Check for deprecated commands if echo "$COMMAND" | grep -qE '\bgrep\b|\bfind\b'; then echo "⚠ Consider using rg or fd instead" >&2 fi # Stage 3: Check for common mistakes if echo "$COMMAND" | grep -qE 'git\s+push\s+--force'; then echo "⚠ Force push detected - use with caution" >&2 fi exit 0 ``` ### Performance Monitoring ```bash #!/usr/bin/env bash # Track hook performance START=$(date +%s%N) # Hook logic here INPUT=$(cat) # ... process ... # Calculate duration END=$(date +%s%N) DURATION=$(( (END - START) / 1000000 )) # milliseconds # Log performance echo "[$(date -Iseconds)] Hook duration: ${DURATION}ms" >> /tmp/claude-perf.log exit 0 ```