--- name: box-factory-hooks-design description: Interpretive guidance for designing Claude Code hooks. Helps you understand hook lifecycle, when to use hooks vs other patterns, and common pitfalls. Use when creating or reviewing hooks. --- # Hooks Design Skill This skill provides interpretive guidance and best practices for creating Claude Code hooks. **ALWAYS fetch current official documentation before creating hooks** - this skill helps you understand what the docs mean and how to create excellent hooks. ## Required Reading Before Creating Hooks Fetch these docs with WebFetch every time: - **https://code.claude.com/docs/en/hooks** - Complete hook reference ## Core Understanding ### Hooks Are Deterministic Control **Key insight:** Hooks provide guaranteed, deterministic execution at specific lifecycle events. **What this means:** - **Hooks** = Execute every single time (deterministic) - **Prompts/Instructions** = Claude might forget (probabilistic) - **Agents** = Context-dependent intelligence - **Use hooks when "always" matters** **Decision question:** Do you need this to happen every single time, or is "usually" okay? **Examples:** - Format after every file write → Hook - Suggest code improvements → Prompt to Claude - Run tests after code changes → Hook if mandatory, agent if discretionary - Security validation before bash → Hook (must be enforced) ### Hook Architecture (How It Fits) **Execution flow:** ``` User Input → Claude Thinks → Tool Execution ↑ ↓ Hooks fire here ────┘ ``` **Critical implications:** - **PreToolUse**: Can block/modify before tool runs - **PostToolUse**: Can react after tool completes successfully - **Stop**: Can't affect what Claude just did (it's done, only cleanup) - **UserPromptSubmit**: Runs before Claude sees the prompt ### Exit Codes Are Communication (Official Specification) **Exit 0**: Hook succeeded, continue execution - stdout displays in transcript mode (CTRL-R) - Exception: `UserPromptSubmit` and `SessionStart` where stdout becomes context for Claude **Exit 2**: Blocking error, stop and handle - stderr feeds to Claude for processing - Behavior varies by event: - **PreToolUse**: Blocks tool call - **Stop/SubagentStop**: Blocks stoppage - **UserPromptSubmit**: Blocks prompt, erases it, shows error to user only **Other exit codes**: Non-blocking error - stderr displays to user - Execution continues **Best practice:** Use exit 2 sparingly - it's powerful but disruptive. Use it for security/safety enforcement, not preferences. ## Hook Events (Official Specification) Complete list of available events: | Event | When It Fires | Matcher Applies | |-------|--------------|----------------| | **PreToolUse** | After Claude creates tool params, before processing | Yes | | **PostToolUse** | Immediately after successful tool completion | Yes | | **PermissionRequest** | When permission dialogs shown to users | No | | **Notification** | When Claude Code sends notifications | No | | **UserPromptSubmit** | When users submit prompts, before Claude processes | No | | **Stop** | When main Claude agent finishes responding | No | | **SubagentStop** | When subagents (Task tool calls) complete | No | | **PreCompact** | Before compacting operations | No | | **SessionStart** | When sessions start or resume | No | | **SessionEnd** | When sessions terminate | No | ## Hook Types (Official Specification) **Command Hooks** (`type: "command"`): - Execute bash scripts with file system access - Default timeout: 60 seconds (customizable per hook) - Fast, deterministic operations - Use for formatters, linters, file ops, git commands **Prompt-Based Hooks** (`type: "prompt"`): - Send queries to Claude Haiku for context-aware decisions - Available for: `Stop`, `SubagentStop`, `UserPromptSubmit`, `PreToolUse` - Use when judgment/context understanding needed - Natural language evaluation **Rule of thumb:** If you can write it as a bash script = command hook. If you need judgment = prompt hook. ## Hook Script Implementation Patterns (Best Practices) ### Bash vs Python for Hook Scripts **Bash is ideal for:** - Simple file operations (formatting, linting with external tools) - Calling CLI tools directly - Quick text processing with standard utilities - Minimal logic, mostly orchestration **Python is better for:** - Complex validation logic - JSON parsing and manipulation - Advanced text processing - Using Python libraries for analysis - Multi-step processing with error handling ### Python Hook Scripts with UV (Best Practice) For Python-based hooks requiring dependencies or complex logic, use UV's single-file script format with inline metadata. This provides self-contained, executable scripts without separate environment setup. **When to use Python hooks:** - Parsing complex JSON from stdin - Advanced validation requiring libraries (AST analysis, schema validation) - Multi-step processing beyond simple shell pipelines - Need for structured error handling and reporting **Pattern:** Use Skill tool: skill="box-factory:uv-scripts" The uv-scripts skill provides complete patterns for creating Python hook scripts with inline dependencies, proper shebangs, and Claude Code integration. **Quick example:** ```python #!/usr/bin/env -S uv run --script # /// script # dependencies = ["ruff"] # /// import subprocess import sys import os def main(): file_paths = os.environ.get("CLAUDE_FILE_PATHS", "").split() if not file_paths: sys.exit(0) result = subprocess.run(["ruff", "check", *file_paths]) sys.exit(result.returncode) if __name__ == "__main__": main() ``` **Key advantages:** - Self-contained (dependencies declared inline) - No separate virtual environment management - Executable directly with proper shebang - Fast startup with UV's performance - Perfect for plugin hooks (ships with dependencies) ## Matcher Syntax (Official Specification) Matchers specify which tools trigger hooks (applies to PreToolUse and PostToolUse only): **Exact matching:** ```json "matcher": "Write" ``` **Regex patterns with pipe:** ```json "matcher": "Edit|Write" "matcher": "Notebook.*" ``` **Wildcard (match all):** ```json "matcher": "*" ``` **Empty matcher:** Omit for events like `UserPromptSubmit` that don't apply to specific tools. **Note:** Matchers are case-sensitive. ## Configuration Structure (Official Specification) Located in `~/.claude/settings.json`, `.claude/settings.json`, or `.claude/settings.local.json`: ```json { "hooks": { "EventName": [ { "matcher": "ToolPattern", "hooks": [ { "type": "command", "command": "bash-command", "timeout": 30 } ] } ] } } ``` **Timeout field:** Optional, specified in seconds (default 60). ## Hook Input (stdin) - Official Specification All hooks receive JSON via stdin: **Base structure (all events):** ```json { "session_id": "string", "transcript_path": "path/to/transcript.jsonl", "cwd": "current/working/directory", "permission_mode": "default|plan|acceptEdits|bypassPermissions", "hook_event_name": "EventName" } ``` **Event-specific fields:** - **PreToolUse/PostToolUse**: Adds `tool_name`, `tool_input` - **UserPromptSubmit**: Adds `prompt` - Other events may include additional context **Best practice:** Parse stdin JSON to access context, don't rely only on environment variables. ## Hook Output (stdout) - Official Specification Two approaches for returning results: ### Simple Exit Code Approach Just use exit codes and stderr for errors. Most common for straightforward hooks. ### Advanced JSON Output Return structured JSON for sophisticated control: ```json { "continue": true, "stopReason": "Custom message", "suppressOutput": true, "systemMessage": "Warning to display", "hookSpecificOutput": { "hookEventName": "EventName", "additionalContext": "string" } } ``` ### PostToolUse Communication Pattern (CRITICAL) **Key insight:** PostToolUse hooks have two output channels with different visibility: **For messages visible DIRECTLY to users (no verbose mode required):** Use `systemMessage` field - displays immediately to users: ```json { "systemMessage": "Markdown formatted: path/to/file.md" } ``` **For messages visible ONLY to Claude (user must enable verbose mode):** Use `additionalContext` in `hookSpecificOutput`: ```json { "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Internal context for Claude's awareness" } } ``` **Complete output pattern:** ```python import json output = { "systemMessage": "Formatted successfully: file.md", # Shows to user directly "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Additional context for Claude" # Only in verbose mode } } print(json.dumps(output), flush=True) sys.exit(0) ``` **Common mistake:** Using only `additionalContext` when user feedback is needed. This requires users to enable verbose mode (CTRL-O) to see output. **Correct pattern:** - **User feedback needed:** Use `systemMessage` (visible immediately) - **Claude context only:** Use `additionalContext` (verbose mode only) - **Both:** Include both fields in the JSON output - **Blocking errors:** Use exit 2 with stderr (rare, security/safety only) ### PreToolUse Special Output For modifying or blocking tool execution: ```json { "permissionDecision": "allow|deny|ask", "updatedInput": { "modified": "tool parameters" } } ``` **Use case:** Modify tool inputs before execution (e.g., add safety flags to bash commands). ## Environment Variables (Official Specification) Available in command hooks: | Variable | Purpose | |----------|---------| | `$CLAUDE_PROJECT_DIR` | Absolute path to project root | | `$CLAUDE_ENV_FILE` | File path for persisting env vars (SessionStart only) | | `${CLAUDE_PLUGIN_ROOT}` | Plugin directory path (for plugin hooks) | | `$CLAUDE_CODE_REMOTE` | `"true"` for remote, empty for local execution | **Best practice:** Always quote variables: `"$CLAUDE_PROJECT_DIR"` not `$CLAUDE_PROJECT_DIR` ## Decision Framework ### Hook vs Agent vs Command **Use Hook when:** - Need guaranteed execution every time - Simple, deterministic rule (format, lint, validate) - Integrating with external tools - Performance/safety enforcement - Must happen at specific lifecycle event **Use Agent when:** - Complex decision-making involved - Need Claude's intelligence for analysis - Context-dependent logic - Natural language processing needed - Can be triggered proactively by context **Use Command (Slash Command) when:** - User wants explicit control over when it runs - Not tied to lifecycle events - One-off operations ## Common Use Patterns (Best Practices) ### SessionStart Pattern **Purpose:** Initialize session state, inject context ```json { "SessionStart": [ { "hooks": [ { "type": "command", "command": "cat .claude/project-context.md" } ] } ] } ``` **Key:** stdout becomes Claude's context. Use to load project guidelines, conventions, or state. ### UserPromptSubmit Pattern **Purpose:** Validate or enrich prompts before Claude sees them ```json { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "~/.claude/hooks/inject-security-reminders.sh" } ] } ] } ``` **Key:** stdout goes to Claude. Can add context or use exit 2 to block prompts. ### PreToolUse Pattern **Purpose:** Validate or modify before execution ```json { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "~/.claude/hooks/security-check.sh" } ] } ] } ``` **Power move:** Exit 2 to block dangerous commands and explain why to Claude. ### PostToolUse Pattern **Purpose:** React to successful tool completion ```json { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "prettier --write \"$CLAUDE_FILE_PATHS\" 2>/dev/null || true" } ] } ] } ``` **Common uses:** Format code, run linters, update documentation, cleanup. **CRITICAL for PostToolUse:** To communicate results to users, hooks must output JSON to stdout with `systemMessage`: ```python #!/usr/bin/env -S uv run --quiet --script # /// script # dependencies = [] # /// import json import sys def output_json_response(system_message=None, additional_context=None): """Output JSON response for Claude to process.""" response = {} if system_message: response["systemMessage"] = system_message # Visible directly to user if additional_context: response["hookSpecificOutput"] = { "hookEventName": "PostToolUse", "additionalContext": additional_context # Only visible in verbose mode } print(json.dumps(response), flush=True) # Read hook input from stdin hook_input = json.load(sys.stdin) file_path = hook_input.get("tool_input", {}).get("file_path") # Run linter/formatter # ... # Communicate result directly to user output_json_response(system_message=f"Formatted successfully: {file_path}") sys.exit(0) ``` **Common mistakes:** - Using only `additionalContext` when user feedback is needed (requires verbose mode) - Writing to stderr instead of JSON stdout (completely invisible) ### Stop Pattern **Purpose:** Session cleanup, final actions ```json { "Stop": [ { "hooks": [ { "type": "command", "command": "~/.claude/hooks/auto-commit.sh" } ] } ] } ``` **Note:** Claude already responded, can't change that. Use for cleanup, notifications, test runs. ## Common Pitfalls (Best Practices) ### Pitfall #1: Blocking Everything **Problem:** Overly aggressive hook blocks all operations ```json { "PreToolUse": [ { "matcher": "*", "hooks": [{"type": "command", "command": "exit 2"}] } ] } ``` **Result:** Claude can't do anything, unusable. **Better:** Selective blocking with clear criteria for security/safety only. ### Pitfall #2: Slow Hooks **Problem:** Hook takes 30+ seconds, blocks user experience ```bash npm install # Slow, blocking exit 0 ``` **Impact:** Claude waits, terrible UX. **Better:** Fast validation or background execution ```bash npm outdated | head -5 # Quick check exit 0 ``` **Or:** Adjust timeout for legitimately long operations: ```json { "type": "command", "command": "long-running-task.sh", "timeout": 120 } ``` ### Pitfall #3: Silent Failures **Problem:** Errors disappear into the void ```bash important-check || true exit 0 ``` **Result:** User never knows check failed. **Better:** Clear error communication ```bash if ! important-check; then echo "Check failed: [specific reason]" >&2 exit 1 # Non-blocking, but visible fi exit 0 ``` ### Pitfall #4: Assuming User Interaction **Problem:** Hook expects user input ```bash read -p "Confirm? " response exit 0 ``` **Result:** Hook hangs indefinitely (no user to respond). **Better:** Fully automated decisions based on stdin JSON or environment. ### Pitfall #5: Ignoring Security **Problem:** Hook doesn't validate inputs, vulnerable to path traversal ```bash cat "$SOME_PATH" # Dangerous if not validated ``` **Result:** Could access sensitive files outside project. **Better:** Validate and sanitize ```bash if [[ "$SOME_PATH" == *".."* ]]; then echo "Path traversal detected" >&2 exit 2 fi # Continue safely ``` **Official guidance:** Skip sensitive files (`.env`, `.git/`, credentials). Validate inputs from stdin. ### Pitfall #6: Not Quoting Variables **Problem:** Unquoted shell variables break with spaces ```bash prettier --write $CLAUDE_FILE_PATHS # Breaks if path has spaces ``` **Better:** Always quote variables ```bash prettier --write "$CLAUDE_FILE_PATHS" ``` ### Pitfall #7: PostToolUse Using Wrong Output Field **Problem:** Hook uses `additionalContext` when user feedback is needed ```python # Wrong - Only visible in verbose mode (CTRL-O) import json output = { "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Formatted successfully: file.md" } } print(json.dumps(output), flush=True) sys.exit(0) ``` **Result:** User must enable verbose mode to see feedback. **Better:** Use `systemMessage` for direct user visibility ```python # Correct - Visible immediately to user import json output = { "systemMessage": "Formatted successfully: file.md" } print(json.dumps(output), flush=True) sys.exit(0) ``` **Why:** - `systemMessage` displays directly to users (no verbose mode required) - `additionalContext` only visible in verbose mode (CTRL-O) or as Claude's context - stderr output is only for blocking errors (exit 2) ## Security Considerations (Official Guidance) **Critical warning from docs:** "Claude Code hooks execute arbitrary shell commands on your system automatically." **Implications:** - Hooks can access/modify any files your user account permits - You bear sole responsibility for configured commands - Test thoroughly in safe environments first - Review hooks from untrusted sources carefully **Protection mechanism:** - Configuration snapshots captured at startup - Hook changes require review via `/hooks` menu - Prevents mid-session malicious modifications **Best practices:** 1. Validate and sanitize all inputs from stdin 2. Block path traversal (`..` in paths) 3. Use absolute paths with `$CLAUDE_PROJECT_DIR` 4. Skip sensitive files (`.env`, credentials, `.git/`) 5. For prompt-based hooks, be specific about criteria 6. Set appropriate timeouts 7. Test in isolated environments first ## Debugging Hooks (Best Practices) **View hook execution:** Press **CTRL-R** in Claude Code to see: - Hook stdout/stderr - Execution flow - Exit codes - Timing information **Add logging to hooks:** ```bash echo "Hook triggered: $(date)" >> ~/.claude/hook-log.txt echo "Input: $SOME_VAR" >> ~/.claude/hook-log.txt # Continue with hook logic exit 0 ``` **Parse stdin for debugging:** ```bash # Save stdin to debug cat > /tmp/hook-debug.json cat /tmp/hook-debug.json | jq '.' # Pretty print exit 0 ``` ## Advanced Features (Official Specification) ### Modifying Tool Inputs (PreToolUse) Return JSON to modify tool parameters: ```bash #!/bin/bash # Read stdin INPUT=$(cat) # Add safety flag to bash commands MODIFIED=$(echo "$INPUT" | jq '.tool_input.command = .tool_input.command + " --safe-mode"') # Return modified input echo "$MODIFIED" | jq '{permissionDecision: "allow", updatedInput: .tool_input}' exit 0 ``` ### Custom Timeouts Adjust timeout per hook: ```json { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "./long-build.sh", "timeout": 300 } ] } ] } ``` ### Prompt-Based Intelligence Use Claude Haiku for context-aware decisions: ```json { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "prompt", "command": "Analyze this bash command for security risks. If dangerous, explain why and recommend safer alternative." } ] } ] } ``` ## Plugin Integration When creating hooks for plugins: **Structure:** ``` my-plugin/ ├── .claude-plugin/plugin.json └── hooks/ └── hooks.json ``` **Reference plugin root:** ```bash "${CLAUDE_PLUGIN_ROOT}/scripts/hook-script.sh" ``` See plugin-design skill for complete plugin context. ## Hook Quality Checklist Before deploying hooks: **Functionality (from official docs):** - ✓ Correct event type for use case - ✓ Valid matcher pattern (if applicable) - ✓ Proper JSON structure in settings - ✓ Appropriate timeout configured **Quality (best practices):** - ✓ Fast execution (< 60s or custom timeout) - ✓ Clear error messages to stderr - ✓ Appropriate exit codes (0, 2, other) - ✓ No user interaction required - ✓ Variables quoted properly - ✓ Inputs validated/sanitized **Security (best practices):** - ✓ Path traversal blocked - ✓ Sensitive files skipped - ✓ Absolute paths used - ✓ No secret exposure - ✓ Tested in safe environment ## Example: High-Quality Hook **Basic (hypothetical docs example):** ```json { "PostToolUse": [ { "matcher": "Write", "hooks": [{"type": "command", "command": "prettier --write"}] } ] } ``` **Issues:** - ❌ Missing file path variable - ❌ No error handling - ❌ Doesn't catch Edit operations **Excellent (applying best practices):** ```json { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "prettier --write \"$CLAUDE_FILE_PATHS\" 2>/dev/null || true", "timeout": 30 } ] } ] } ``` **Improvements:** - ✅ Uses `$CLAUDE_FILE_PATHS` variable - ✅ Quoted variable for spaces - ✅ Error suppression (|| true) prevents blocking - ✅ Catches both Write and Edit - ✅ Custom timeout for faster failures - ✅ Redirects stderr to avoid noise ## Documentation References These are the authoritative sources. Fetch them before creating hooks: **Core specifications:** - https://code.claude.com/docs/en/hooks - Complete hook reference **Related topics:** - See agent-design skill for when to use agents instead - See slash-command-design skill for user-triggered operations - See plugin-design skill for packaging hooks in plugins - See uv-scripts skill for Python-based hook implementation patterns **Remember:** Official docs provide structure and features. This skill provides best practices and patterns for creating excellent hooks.