hook-creator
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 systemdeny: Prevent tool executionask: 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:
Writematches only Write tool - Multiple tools:
Edit|Writematches 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
/hooksmenu for changes
Debugging
Basic Troubleshooting
- Check configuration: Run
/hooksto 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 --debugfor 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_DIRfor 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