--- name: julien-dev-hook-creator description: Guide for creating Claude Code hooks - shell commands that execute at specific lifecycle events (SessionStart, SessionEnd, PreToolUse, PostToolUse, etc.). Use when users want to automate actions, add validation, logging, or integrate external tools into Claude Code workflows. license: Apache-2.0 metadata: author: "Julien" version: "1.0.0" category: "development" triggers: - "create hook" - "new hook" - "add hook" - "hook template" - "write hook" - "build hook" - "crΓ©er hook" - "nouveau hook" - "ajouter hook" - "Γ©crire hook" - "SessionStart" - "SessionEnd" - "PreToolUse" - "PostToolUse" - "UserPromptSubmit" - "claude code hook" - "automation hook" --- # Hook Creator This skill guides the creation of Claude Code hooks - deterministic shell commands or LLM prompts that execute at specific points in Claude's lifecycle. ## What Are Hooks? ## Observability **First**: At the start of execution, display: ``` πŸ”§ Skill "julien-dev-hook-creator" activated ``` Hooks provide **deterministic control** over Claude's behavior. Unlike skills (which Claude chooses to use), hooks **always execute** at their designated lifecycle event. ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ HOOKS vs SKILLS β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ HOOKS: Deterministic, always run at lifecycle events β”‚ β”‚ SKILLS: Model-invoked, Claude decides when to use β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Available Hook Events | Event | When It Runs | Common Use Cases | |-------|--------------|------------------| | `SessionStart` | Session begins/resumes | Load context, sync data, set env vars | | `SessionEnd` | Session ends | Cleanup, save state, push changes | | `PreToolUse` | Before tool execution | Validate, block, modify tool input | | `PostToolUse` | After tool completes | Format output, log, trigger actions | | `PermissionRequest` | Permission dialog shown | Auto-approve or deny permissions | | `UserPromptSubmit` | User submits prompt | Add context, validate requests | | `Notification` | Claude sends notification | Custom alerts | | `Stop` | Claude finishes responding | Decide if Claude should continue | | `SubagentStop` | Subagent completes | Evaluate task completion | ## Hook Configuration Hooks are configured in `~/.claude/settings.json` (global) or `.claude/settings.json` (project). ### Basic Structure ```json { "hooks": { "EventName": [ { "matcher": "ToolPattern", "hooks": [ { "type": "command", "command": "your-command-here", "timeout": 60 } ] } ] } } ``` ### Configuration Fields | Field | Required | Description | |-------|----------|-------------| | `matcher` | For tool events | Pattern to match tool names (regex supported) | | `type` | Yes | `"command"` (shell) or `"prompt"` (LLM) | | `command` | For type:command | Shell command to execute | | `prompt` | For type:prompt | LLM prompt for evaluation | | `timeout` | No | Seconds before timeout (default: 60, max: 300) | ### Matcher Patterns ```json "matcher": "Write" // Exact match "matcher": "Edit|Write" // OR pattern (regex) "matcher": "Notebook.*" // Wildcard pattern "matcher": "*" // All tools (or omit matcher) ``` ## Hook Input (stdin) Hooks receive JSON via stdin with context about the event: ```json { "session_id": "abc123", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/current/working/directory", "hook_event_name": "PreToolUse", "tool_name": "Write", "tool_input": { "file_path": "/path/to/file.txt", "content": "file content" } } ``` ## Hook Output (Exit Codes) | Exit Code | Behavior | |-----------|----------| | `0` | Success - continue normally | | `2` | **Block** - stderr fed to Claude, action blocked | | Other | Non-blocking error (shown in verbose mode) | ### Advanced JSON Output (exit 0) ```json { "continue": true, "stopReason": "message if continue=false", "suppressOutput": true, "systemMessage": "warning shown to user" } ``` ### PreToolUse Decision Control ```json { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow|deny|ask", "permissionDecisionReason": "Reason here", "updatedInput": { "field": "modified value" } } } ``` ## Creating a Hook - Step by Step ### Step 1: Identify the Use Case Ask: - **When** should this run? (which event) - **What** should it do? (validate, log, transform, block) - **Scope**: Global (`~/.claude/settings.json`) or project (`.claude/settings.json`)? ### Step 2: Write the Script Create script in `~/.claude/scripts/` or `.claude/scripts/`: ```bash #!/bin/bash # ~/.claude/scripts/my-hook.sh # Read input from stdin 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') # Your logic here if [[ "$FILE_PATH" == *".env"* ]]; then echo "Blocked: Cannot modify .env files" >&2 exit 2 # Block the action fi exit 0 # Allow the action ``` **Important**: Make executable with `chmod +x` ### Step 3: Configure the Hook Add to settings.json: ```json { "hooks": { "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "bash ~/.claude/scripts/my-hook.sh", "timeout": 10 } ] } ] } } ``` ### Step 4: Test ```bash # Test script directly echo '{"tool_name":"Write","tool_input":{"file_path":"/test/.env"}}' | bash ~/.claude/scripts/my-hook.sh echo "Exit code: $?" ``` ## Real-World Example: Terminal Title Restoration **Problem**: `happy.cmd` and `claude.cmd` contain `title %COMSPEC%` which overwrites terminal title to "C:\WINDOWS\system32\cmd.exe" **Solution**: SessionStart hook that restores the title after launch **Script**: `~/.claude/scripts/restore-terminal-title-on-start.ps1` ```powershell # Restore terminal title on Claude Code SessionStart # This runs AFTER Claude has potentially overwritten the title try { # Get current directory name $dirName = if ($PWD.Path -eq $HOME) { "~" } else { Split-Path $PWD -Leaf } # Restore title using multiple methods for maximum compatibility # Method 1: PowerShell native $Host.UI.RawUI.WindowTitle = $dirName # Method 2: ANSI escape sequence (more reliable with Windows Terminal) Write-Host "$([char]27)]0;$dirName$([char]7)" -NoNewline # Exit with success exit 0 } catch { # Silent fail - don't break Claude startup exit 0 } ``` **Configuration**: `~/.claude/settings.json` (NOT repo `.claude/settings.json`) ```json { "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "node \"%USERPROFILE%\\.claude\\scripts\\session-start-banner.js\"", "timeout": 5 }, { "type": "command", "command": "powershell.exe -NoProfile -File \"%USERPROFILE%\\.claude\\scripts\\restore-terminal-title-on-start.ps1\"", "timeout": 2 } ] } ] } } ``` **Timeline**: 1. `happy.cmd` executes β†’ title becomes "C:\WINDOWS\system32\cmd.exe" 2. Happy/Claude starts 3. SessionStart hook: `session-start-banner.js` displays banner 4. SessionStart hook: **`restore-terminal-title-on-start.ps1` fixes the title** **Result**: Title restored to directory name despite npm CLI wrapper interference **Lesson**: Hooks can fix issues caused by external tools (npm wrappers, shell scripts)! ## Hook Languages: JavaScript vs Python vs PowerShell ### JavaScript Hooks (Fastest Startup) **Pros**: - Node.js already loaded by Claude Code - No interpreter startup cost - Faster execution (~50-200ms faster than Python) - Great async support **Cons**: - Limited system integration compared to PowerShell - JSON parsing requires external library or built-in JSON **Examples**: - `session-start-banner.js` - Fast banner display - `track-skill-invocation.js` - Performance-critical tracking - `fast-skill-router.js` - Routing must be instant **When to use**: Performance-critical hooks (SessionStart, UserPromptSubmit) ### Python Hooks (Rich Ecosystem) **Pros**: - Rich libraries (json, pathlib, subprocess) - Better for complex data processing - Easier multiline string handling - Great for ML/data tasks **Cons**: - Python interpreter startup cost (~100-300ms) - May not be installed on all systems **Examples**: - `session-end-delete-reserved.py` - Complex file operations - `save-session-for-memory.py` - Data processing - `cleanup-null-files.py` - File system traversal **When to use**: Complex logic, data processing, non-time-critical tasks ### PowerShell Hooks (Windows Native) **Pros**: - Native Windows API access - Can modify environment directly - Better integration with Windows Terminal - Access to .NET framework **Cons**: - Windows-only - Slower than JavaScript (~50-150ms startup) - CRLF line ending issues **Examples**: - `restore-terminal-title-on-start.ps1` - Terminal manipulation - `cleanup-null-files.ps1` - Windows file operations - `set-terminal-title.ps1` - Environment modification **When to use**: Windows-specific tasks, terminal manipulation, .NET integration ### Choosing the Right Language ``` Need speed? β†’ JavaScript Need Python libraries? β†’ Python Need Windows integration? β†’ PowerShell Need to modify terminal? β†’ PowerShell Need to call .NET APIs? β†’ PowerShell Need async operations? β†’ JavaScript ``` ## Common Hook Patterns ### 1. File Protection (PreToolUse) ```bash #!/bin/bash INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') PROTECTED=(".env" "package-lock.json" ".git/" "credentials") for pattern in "${PROTECTED[@]}"; do if [[ "$FILE_PATH" == *"$pattern"* ]]; then echo "Protected file: $pattern" >&2 exit 2 fi done exit 0 ``` ### 2. Auto-Format on Save (PostToolUse) ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "FILE=$(cat | jq -r '.tool_input.file_path') && npx prettier --write \"$FILE\" 2>/dev/null || true" } ] } ] } } ``` ### 3. Command Logging (PostToolUse) ```bash #!/bin/bash INPUT=$(cat) CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') DESC=$(echo "$INPUT" | jq -r '.tool_input.description // "No description"') echo "$(date +%Y-%m-%d_%H:%M:%S) | $CMD | $DESC" >> ~/.claude/logs/bash-commands.log exit 0 ``` ### 4. Session Sync (SessionStart/SessionEnd) ```json { "hooks": { "SessionStart": [ { "hooks": [{ "type": "command", "command": "bash ~/.claude/scripts/sync-marketplace.sh", "timeout": 30 }] } ], "SessionEnd": [ { "hooks": [{ "type": "command", "command": "bash ~/.claude/scripts/push-marketplace.sh", "timeout": 30 }] } ] } } ``` ### 5. Add Context to Prompts (UserPromptSubmit) ```bash #!/bin/bash # stdout is added as context to the prompt echo "Current git branch: $(git branch --show-current 2>/dev/null || echo 'not a git repo')" echo "Node version: $(node -v 2>/dev/null || echo 'not installed')" exit 0 ``` ### 6. LLM-based Stop Decision (Stop) ```json { "hooks": { "Stop": [ { "hooks": [{ "type": "prompt", "prompt": "Review if all tasks are complete. Check: 1) All todos marked done 2) Tests passing 3) No pending questions. Respond with decision: approve (stop) or block (continue).", "timeout": 30 }] } ] } } ``` ## Best Practices ### Do's - βœ… Always quote shell variables: `"$VAR"` not `$VAR` - βœ… Use absolute paths for scripts - βœ… Handle errors gracefully (exit 0 if non-critical) - βœ… Set appropriate timeouts - βœ… Test scripts independently before configuring - βœ… Use `tr -d '\r'` for Windows CRLF compatibility ### Don'ts - ❌ Don't block critical operations without good reason - ❌ Don't use long timeouts (blocks Claude) - ❌ Don't trust input blindly - validate paths - ❌ Don't expose secrets in logs - ❌ Don't use interactive commands (no stdin available) ## Debugging Hooks ```bash # Run with debug output bash -x ~/.claude/scripts/my-hook.sh # Test with sample input echo '{"tool_name":"Write","tool_input":{"file_path":"/test/file.txt"}}' | bash ~/.claude/scripts/my-hook.sh # Check hook errors in Claude Code # Look for "hook error" messages in the UI ``` For detailed troubleshooting of common errors (timeout, CRLF, jq not found, etc.), see `references/troubleshooting.md`. ## Environment Variables Available in hooks: - `CLAUDE_PROJECT_DIR` - Current project directory - `CLAUDE_CODE_REMOTE` - Remote mode indicator - `CLAUDE_ENV_FILE` - (SessionStart only) File path for persisting env vars ## File Locations - CRITICAL INFORMATION | Location | Scope | Usage | |----------|-------|-------| | **`~/.claude/settings.json`** | **Global (REAL FILE)** | **File USED by Claude Code** | | `.claude/settings.json` | Project (versioning) | Committed to repo, NOT used directly | | `.claude/settings.local.json` | Local overrides | Not committed | | `~/.claude/scripts/` | Global scripts | Used by hooks | | `.claude/scripts/` | Project scripts | Versioned with repo | ### ⚠️ CRITICAL WARNING **Claude Code uses `~/.claude/settings.json` (home directory)** **NOT the repo `.claude/settings.json`** These files are DIFFERENT and must be synchronized manually! **Best Practice**: 1. Modify `~/.claude/settings.json` first (real file) 2. Copy changes to `.claude/settings.json` (for versioning) 3. Commit repo version for documentation **Never assume** the repo version is active! **Verification**: ```bash # Check what Claude Code actually uses cat ~/.claude/settings.json | grep -A 5 "SessionStart" # Compare with repo version diff ~/.claude/settings.json .claude/settings.json ``` ## Quick Reference ``` Event Flow: SessionStart β†’ UserPromptSubmit β†’ PreToolUse β†’ [Tool] β†’ PostToolUse β†’ Stop β†’ SessionEnd Exit Codes: 0 = Success (continue) 2 = Block (stop action, feed stderr to Claude) * = Non-blocking error Matcher: "Write" = exact match "Edit|Write" = OR "Notebook.*" = regex "*" or omit = all tools ``` ## πŸ”— Skill Chaining ### Skills Required Before - Aucun (skill autonome) - Optionnel: Connaissance de base de bash/shell scripting ### Input Expected - **Use case description**: Quel Γ©vΓ©nement dΓ©clencher, quelle action effectuer - **Scope decision**: Global (`~/.claude/settings.json`) ou project (`.claude/settings.json`) - **Prerequisites**: `jq` installΓ© pour parsing JSON ### Output Produced - **Format**: - Script bash dans `~/.claude/scripts/` ou `.claude/scripts/` - Configuration JSON dans `settings.json` - **Side effects**: - CrΓ©ation/modification de fichiers scripts - Modification de settings.json - Hooks actifs au prochain Γ©vΓ©nement - **Duration**: 2-5 minutes pour un hook simple ### Compatible Skills After **RecommandΓ©s**: - **sync-personal-skills**: Si le hook modifie des fichiers du marketplace - **skill-creator**: Si crΓ©ation d'un skill qui intΓ¨gre des hooks **Optionnels**: - Git workflow: Committer les scripts et settings ### Called By - Direct user invocation: "CrΓ©e un hook pour...", "Je veux automatiser..." - Part of skill/workflow development ### Tools Used - `Read` (lecture settings.json existant) - `Write` (crΓ©ation scripts bash) - `Edit` (modification settings.json) - `Bash` (test du hook, chmod +x) ### Visual Workflow ``` User: "Je veux protΓ©ger les fichiers .env" ↓ hook-creator (this skill) β”œβ”€β–Ί Step 1: Identify event (PreToolUse) β”œβ”€β–Ί Step 2: Write script (protect-files.sh) β”œβ”€β–Ί Step 3: chmod +x script β”œβ”€β–Ί Step 4: Configure settings.json └─► Step 5: Test with sample input ↓ Hook active βœ… ↓ [Next: Test in real session] ``` ### Usage Example **Scenario**: CrΓ©er un hook de logging des commandes bash **Input**: "Log toutes les commandes bash exΓ©cutΓ©es" **Process**: 1. Event identifiΓ©: `PostToolUse` avec matcher `Bash` 2. Script créé: `~/.claude/scripts/log-bash.sh` 3. Settings.json mis Γ  jour avec hook config 4. Test avec sample JSON input **Result**: - Script logging actif - Commandes loguΓ©es dans `~/.claude/logs/bash-commands.log`