Hook Creator
This skill provides comprehensive guidance for creating and managing Claude Code hooks—powerful automation tools that intercept and respond to Claude Code events.
Quick Start
Create a simple PreToolUse hook that validates Bash commands:
~/.claude/settings.json or .claude/settings.json
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ""$CLAUDE_PROJECT_DIR"/.claude/hooks/validate-command.sh", "timeout": 30 } ] } ] } }
#!/bin/bash
.claude/hooks/validate-command.sh
input=$(cat) command=$(echo "$input" | jq -r '.tool_input.command // empty')
Block destructive commands
if [[ "$command" =~ ^rm\ -rf ]]; then echo "Destructive command blocked" >&2 exit 2 fi
exit 0
What Are Hooks?
Hooks are event-driven automation points that intercept Claude Code execution at specific moments. They receive JSON input via stdin and communicate back through exit codes and stdout.
Hook Capabilities
-
Validate or block tool calls before execution (PreToolUse)
-
Auto-approve permission requests (PermissionRequest)
-
Run post-processing after tool execution (PostToolUse)
-
Add context to conversations (UserPromptSubmit, SessionStart)
-
Control session lifecycle (Stop, SubagentStop)
-
Handle notifications from Claude Code (Notification)
-
Integrate with MCP tools using pattern matching
Hook Types
Command hooks (type: "command" ): Execute bash scripts
-
Fast, deterministic, ideal for validation rules
-
Have access to environment variables
-
Return status via exit codes and JSON output
Prompt hooks (type: "prompt" ): Use LLM for intelligent decisions
-
Context-aware evaluation
-
Best for complex, nuanced decisions
-
Only supported for Stop and SubagentStop events
Hook Events
PreToolUse
Runs after tool parameters are created but before execution.
Common matchers: Bash , Write , Edit , Read , Task , Glob , Grep
Use cases:
-
Validate or modify tool inputs before execution
-
Auto-approve specific tool calls
-
Block dangerous operations
Decision control:
-
allow : Bypass permission system
-
deny : Prevent tool execution
-
ask : Show user confirmation dialog
PermissionRequest
Runs when user is shown a permission dialog.
Use cases:
-
Auto-approve known-safe operations
-
Deny specific permission requests
-
Modify tool inputs before approval
PostToolUse
Runs immediately after successful tool completion.
Use cases:
-
Trigger notifications or logging
-
Validate tool outputs
-
Add feedback for Claude to consider
UserPromptSubmit
Runs when user submits a prompt, before Claude processes it.
Use cases:
-
Add contextual information to conversations
-
Validate prompts for security issues
-
Block inappropriate prompts
Stop / SubagentStop
Runs when Claude or a subagent finishes responding.
Use cases:
-
Prevent premature stopping
-
Ensure task completion
-
Continue work automatically
SessionStart
Runs when Claude starts or resumes a session.
Matchers: startup , resume , clear , compact
Use cases:
-
Load development context (recent issues, changes)
-
Install dependencies or configure environment
-
Persist environment variables via $CLAUDE_ENV_FILE
SessionEnd
Runs when a Claude Code session ends.
Use cases:
-
Cleanup tasks or logging
-
Save session state or statistics
Notification
Runs when Claude Code sends notifications.
Matchers: permission_prompt , idle_prompt , auth_success , elicitation_dialog
Use cases:
-
Custom notification handling
-
Forward notifications to external systems
Configuration Structure
Basic Structure
{ "hooks": { "EventName": [ { "matcher": "ToolPattern|*", "hooks": [ { "type": "command|prompt", "command": "bash-command", "prompt": "llm-prompt", "timeout": 60 } ] } ] } }
Configuration Locations
Hooks are configured in settings files (priority order):
-
~/.claude/settings.json
-
User settings
-
.claude/settings.json
-
Project settings
-
.claude/settings.local.json
-
Local project (not committed)
-
Plugin hooks - Auto-merged when plugins enabled
Matcher Patterns
-
Exact match: Write matches only Write tool
-
Multiple tools: Edit|Write matches Edit or Write
-
Wildcards: .* or * matches all tools
-
Empty/omitted: Matches all tools (for events without matchers)
Environment Variables
Available in all hook commands:
-
$CLAUDE_PROJECT_DIR : Project root directory
-
$CLAUDE_PLUGIN_ROOT : Plugin directory (plugin hooks only)
-
$CLAUDE_CODE_REMOTE : "true" in remote web environment (unset locally)
-
$CLAUDE_ENV_FILE : Persist environment vars (SessionStart only)
Hook Input/Output
Input Format
Hooks receive JSON via stdin:
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/current/working/directory", "permission_mode": "default", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "echo 'hello'" }, "tool_use_id": "toolu_01ABC123..." }
Output: Exit Codes
-
0: Success (stdout shown in verbose mode, except UserPromptSubmit/SessionStart where stdout is added as context)
-
2: Blocking error (stderr fed back to Claude, blocks action for applicable events)
-
Other: Non-blocking error (stderr shown in verbose mode)
Output: JSON Control
Return structured JSON in stdout for advanced control:
{ "continue": true, "stopReason": "Message shown when stopping", "suppressOutput": true, "systemMessage": "Warning to user" }
PreToolUse JSON Output
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow|deny|ask", "permissionDecisionReason": "Explanation", "updatedInput": { "field_to_modify": "new value" } } }
PostToolUse JSON Output
{ "decision": "block", "reason": "Explanation", "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Extra information for Claude" } }
Stop/SubagentStop JSON Output
{ "decision": "block", "reason": "Why Claude should continue (required when blocking)" }
Common Patterns
Pattern 1: Command Validation
Validate bash commands before execution:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ""$CLAUDE_PROJECT_DIR"/.claude/hooks/validate-bash.sh", "timeout": 30 } ] } ] } }
#!/bin/bash
.claude/hooks/validate-bash.sh
input=$(cat) command=$(echo "$input" | jq -r '.tool_input.command // empty')
Enforce better tools
if echo "$command" | grep -qE '\bgrep\b(?!.*|)'; then echo "Use 'rg' (ripgrep) instead of 'grep'" >&2 exit 2 fi
if echo "$command" | grep -qE '\bfind\s+\S+\s+-name\b'; then echo "Use 'rg --files' instead of 'find -name'" >&2 exit 2 fi
exit 0
Pattern 2: Auto-Approve Safe Operations
Auto-approve documentation file reads:
{ "hooks": { "PermissionRequest": [ { "matcher": "Read", "hooks": [ { "type": "command", "command": ""$CLAUDE_PROJECT_DIR"/.claude/hooks/auto-approve-docs.sh" } ] } ] } }
#!/usr/bin/env python3
.claude/hooks/auto-approve-docs.sh
import json import sys
input_data = json.load(sys.stdin) file_path = input_data.get("tool_input", {}).get("file_path", "")
Auto-approve documentation files
if file_path.endswith((".md", ".mdx", ".txt", ".json")): output = { "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "allow" } }, "suppressOutput": True } print(json.dumps(output)) sys.exit(0)
sys.exit(0)
Pattern 3: Add Session Context
Load recent changes on session start:
{ "hooks": { "SessionStart": [ { "matcher": "startup|resume", "hooks": [ { "type": "command", "command": ""$CLAUDE_PROJECT_DIR"/.claude/hooks/load-context.sh" } ] } ] } }
#!/bin/bash
.claude/hooks/load-context.sh
cd "$CLAUDE_PROJECT_DIR" || exit 0
Get recent commits (last 5)
recent_changes=$(git log -5 --oneline --pretty=format:"- %s" 2>/dev/null)
Get current branch
current_branch=$(git branch --show-current 2>/dev/null)
if [ -n "$recent_changes" ] || [ -n "$current_branch" ]; then echo "# Repository Context" echo "" echo "Current branch: $current_branch" echo "" echo "Recent changes:" echo "$recent_changes" fi
exit 0
Pattern 4: Prompt-Based Stop Hook
Use LLM to intelligently decide if Claude should stop:
{ "hooks": { "Stop": [ { "hooks": [ { "type": "prompt", "prompt": "Analyze the conversation context: $ARGUMENTS\n\nDetermine if:\n1. All requested tasks are complete\n2. All tests pass\n3. No errors remain unaddressed\n\nRespond with JSON: {"decision": "approve"|"block", "reason": "explanation"}", "timeout": 30 } ] } ] } }
Pattern 5: MCP Tool Integration
Target specific MCP tools:
{ "hooks": { "PreToolUse": [ { "matcher": "mcp__memory__.", "hooks": [ { "type": "command", "command": "echo 'Memory operation' >> ~/hooks.log" } ] }, { "matcher": "mcp__.__write.*", "hooks": [ { "type": "command", "command": "/path/to/validate-mcp-write.sh" } ] } ] } }
Security Considerations
Disclaimer
USE AT YOUR OWN RISK: Hooks execute arbitrary shell commands automatically. You are solely responsible for hook commands. Malicious or poorly written hooks can cause data loss or system damage.
Best Practices
-
Validate inputs: Never trust input data blindly
-
Quote variables: Use "$VAR" not $VAR
-
Block path traversal: Check for .. in file paths
-
Use absolute paths: Specify full paths via $CLAUDE_PROJECT_DIR
-
Skip sensitive files: Avoid .env , .git/ , keys
-
Test thoroughly: Test hooks in safe environment first
Configuration Safety
Direct edits to hooks don't take effect immediately. Claude Code:
-
Captures hook snapshot at startup
-
Uses snapshot throughout session
-
Warns if hooks modified externally
-
Requires review in /hooks menu for changes
Debugging
Basic Troubleshooting
-
Check configuration: Run /hooks to verify hook registration
-
Verify syntax: Ensure JSON settings are valid
-
Test commands: Run hook commands manually first
-
Check permissions: Ensure scripts are executable (chmod +x )
-
Review logs: Use claude --debug for execution details
Common Issues
-
Quotes not escaped: Use " inside JSON strings
-
Wrong matcher: Tool names are case-sensitive
-
Command not found: Use absolute paths for scripts
-
Exit code confusion: Exit code 2 blocks, others warn
Debug Output
Run claude --debug to see detailed hook execution:
[DEBUG] Executing hooks for PostToolUse:Write [DEBUG] Found 1 hook matchers in settings [DEBUG] Matched 1 hooks for query "Write" [DEBUG] Executing hook command: <command> with timeout 60000ms [DEBUG] Hook command completed with status 0
Plugin Hooks
Plugins can provide hooks that integrate with user/project hooks:
{ "description": "Automatic code formatting", "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh", "timeout": 30 } ] } ] } }
Place in plugins/your-plugin/hooks/hooks.json or specify custom path in plugin metadata.
Execution Details
-
Timeout: 60 seconds default (configurable per command)
-
Parallelization: All matching hooks run in parallel
-
Deduplication: Identical hook commands auto-deduplicated
-
Environment: Current directory with Claude Code's environment
-
Input: JSON via stdin
-
Output: Varies by event (see above)
Guidelines
-
Keep hooks simple: Prefer bash scripts for straightforward logic
-
Use prompt hooks sparingly: Only for nuanced, context-aware decisions
-
Set appropriate timeouts: Adjust based on expected execution time
-
Handle errors explicitly: Scripts should validate inputs and handle errors
-
Test thoroughly: Verify hooks work as expected before production use
-
Document purpose: Add comments explaining hook behavior
-
Use environment variables: Leverage $CLAUDE_PROJECT_DIR for portability
Examples
See examples.md for complete working examples of common hook patterns.
Reference
For complete API details, see:
-
official-hooks-docs.md - Full Claude Code hooks documentation
-
hook-input-schemas.md - Detailed input/output schemas
-
security-guide.md - Security best practices