Claude-skill-registry claude-hook-builder
Interactive hook creator for Claude Code. Triggers when user mentions creating hooks, PreToolUse, PostToolUse, hook validation, hook configuration, settings.json hooks, or wants to automate tool execution workflows.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/claude-hook-builder-pur3v4d3r-pur3-pkb-codebase" ~/.claude/skills/majiayu000-claude-skill-registry-claude-hook-builder && rm -rf "$T"
skills/data/claude-hook-builder-pur3v4d3r-pur3-pkb-codebase/SKILL.mdClaude Code Hook Builder
Purpose
Guide users through creating effective Claude Code hooks for tool validation, automation, and workflow enhancement. Auto-invokes when users want to create or configure hooks.
When to Use
Auto-invoke when users mention:
- Creating hooks - "create hook", "make hook", "new hook", "add hook"
- Hook events - "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "SessionStart"
- Validation - "validate", "check", "prevent", "block", "approve"
- Automation - "auto-format", "auto-lint", "automatic", "trigger"
- Hook configuration - "settings.json hooks", "hook matcher", "hook command"
Knowledge Base
- Official docs:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks.md - Hook guide:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks-guide.md - Project guide:
.claude/docs/creating-components.md
Process
1. Gather Requirements
Ask the user:
Let me help you create a Claude Code hook! I need some details: 1. **What should this hook do?** Examples: - Auto-format code after editing files - Validate bash commands before execution - Add context when user submits prompts - Prevent access to sensitive files - Run tests after file changes 2. **When should it trigger?** - PreToolUse (before tool execution) - PostToolUse (after tool execution) - UserPromptSubmit (when user sends message) - Stop (when Claude finishes responding) - SubagentStop (when subagent finishes) - SessionStart (when session begins) - SessionEnd (when session ends) - Notification (when notification sent) - PermissionRequest (when permission requested) 3. **Which tools should it match?** - Specific tool (Write, Edit, Bash, Read, etc.) - Multiple tools (Write|Edit) - All tools (*) - MCP tools (mcp__server__tool) 4. **What should it return?** - Simple exit code (0 = success, 2 = block) - JSON with decision control - Additional context for Claude - Modified tool inputs 5. **Scope:** - User-level (`~/.claude/settings.json`) - Project-level (`.claude/settings.json`) - Local project (`.claude/settings.local.json`)
2. Determine Hook Type
Bash Command Hook:
{ "type": "command", "command": "/path/to/script.sh" }
- Runs a shell command
- Fast, deterministic
- Good for validation, formatting
Prompt-based Hook:
{ "type": "prompt", "prompt": "Evaluate if Claude should stop: $ARGUMENTS" }
- Uses LLM for decision
- Context-aware, intelligent
- Good for complex decisions (Stop, SubagentStop)
3. Choose Hook Event
PreToolUse
Runs before tool executes.
Use for:
- Validate inputs
- Auto-approve safe operations
- Block dangerous commands
- Modify tool parameters
JSON Output:
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow" | "deny" | "ask", "permissionDecisionReason": "Why this decision", "updatedInput": { "field": "new value" } } }
PostToolUse
Runs after tool completes.
Use for:
- Auto-format code
- Run linters
- Validate outputs
- Log operations
JSON Output:
{ "decision": "block" | undefined, "reason": "Why blocking", "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Extra info for Claude" } }
UserPromptSubmit
Runs when user submits prompt.
Use for:
- Add context automatically
- Validate prompts
- Block sensitive prompts
- Inject current time/date
JSON Output:
{ "decision": "block" | undefined, "reason": "Why blocking", "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "Extra context" } }
Stop / SubagentStop
Runs when Claude/subagent finishes.
Use for:
- Verify tasks completed
- Continue if work remains
- Intelligent stoppage control
JSON Output:
{ "decision": "block" | undefined, "reason": "Why must continue" }
SessionStart
Runs when session starts.
Use for:
- Load environment variables
- Set up development context
- Install dependencies
- Inject initial context
JSON Output:
{ "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": "Initial context" } }
Special: Can persist environment variables:
#!/bin/bash if [ -n "$CLAUDE_ENV_FILE" ]; then echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE" fi
SessionEnd
Runs when session ends.
Use for:
- Cleanup tasks
- Save session stats
- Log session data
4. Create Hook Script
For bash command hooks, create a script:
Template:
#!/usr/bin/env bash # Read JSON input from stdin INPUT=$(cat) # Parse JSON (requires jq) TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Your validation logic here if [[ condition ]]; then echo "Error message" >&2 exit 2 # Block operation fi # Success exit 0
Python Template:
#!/usr/bin/env python3 import json import sys # Read JSON input try: input_data = json.load(sys.stdin) except json.JSONDecodeError as e: print(f"Error: Invalid JSON: {e}", file=sys.stderr) sys.exit(1) tool_name = input_data.get("tool_name", "") tool_input = input_data.get("tool_input", {}) # Your logic here if condition: # Block with error print("Error message", file=sys.stderr) sys.exit(2) # Or return JSON for control output = { "decision": "approve", "reason": "Auto-approved" } print(json.dumps(output)) sys.exit(0)
5. Configure in settings.json
Add hook configuration:
Basic Hook:
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh" } ] } ] } }
Multiple Hooks:
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh", "timeout": 30 }, { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/lint.sh", "timeout": 60 } ] } ], "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py" } ] } ] } }
No Matcher (events without tools):
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/add-context.sh" } ] } ] } }
6. Hook Input Reference
Each event receives JSON on stdin:
Common fields:
{ "session_id": "abc123", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/current/working/dir", "permission_mode": "default", "hook_event_name": "PostToolUse" }
PreToolUse/PostToolUse:
{ "tool_name": "Write", "tool_input": { "file_path": "/path/to/file.txt", "content": "file content" }, "tool_response": { /* PostToolUse only */ "success": true } }
UserPromptSubmit:
{ "prompt": "User's submitted message" }
Stop/SubagentStop:
{ "stop_hook_active": false }
7. Exit Codes
-
0: Success
- stdout shown in verbose mode (Ctrl+O)
- For UserPromptSubmit/SessionStart: stdout added to context
- JSON parsed if present
-
2: Blocking error
- stderr shown to Claude
- Operation blocked (behavior varies by event)
- JSON in stdout ignored
-
Other: Non-blocking warning
- stderr shown in verbose mode
- Execution continues
8. Test the Hook
Test script directly:
# Create test input echo '{ "tool_name": "Write", "tool_input": { "file_path": "test.txt", "content": "hello" } }' | .claude/hooks/your-hook.sh # Check exit code echo $?
Test in Claude Code:
1. Add hook to settings.json 2. Restart Claude Code 3. Run /hooks to verify it's loaded 4. Trigger the hook (e.g., write a file) 5. Check verbose mode (Ctrl+O) for output
Debug mode:
claude --debug # Shows hook execution details
9. Provide Configuration
Show the complete configuration:
{ "hooks": { "EventName": [ { "matcher": "ToolPattern", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/script.sh", "timeout": 60 } ] } ] } }
Hook Examples
Example 1: Auto-Format Python Files
Hook script (
.claude/hooks/format-python.sh):
#!/usr/bin/env bash INPUT=$(cat) # Get file path FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Only process .py files if [[ "$FILE_PATH" == *.py ]]; then # Run black formatter python -m black "$FILE_PATH" 2>&1 if [[ $? -eq 0 ]]; then echo "Formatted: $FILE_PATH" >&2 fi fi exit 0
Configuration:
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-python.sh" } ] } ] } }
Example 2: Validate Bash Commands
Hook script (
.claude/hooks/validate-bash.py):
#!/usr/bin/env python3 import json import sys import re # Dangerous patterns DANGEROUS = [ (r'\brm\s+-rf\s+/', 'Dangerous: rm -rf on root'), (r'>\s*/dev/sd[a-z]', 'Dangerous: writing to block device'), (r'\bcurl\s+.*\|\s*bash', 'Dangerous: piping curl to bash'), ] try: data = json.load(sys.stdin) except: sys.exit(1) if data.get('tool_name') != 'Bash': sys.exit(0) command = data.get('tool_input', {}).get('command', '') # Check for dangerous patterns for pattern, message in DANGEROUS: if re.search(pattern, command): print(f"⚠️ {message}", file=sys.stderr) print(f"Command: {command}", file=sys.stderr) sys.exit(2) # Block sys.exit(0) # Allow
Configuration:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py" } ] } ] } }
Example 3: Add Timestamp to Prompts
Hook script (
.claude/hooks/add-timestamp.sh):
#!/usr/bin/env bash # Output current timestamp echo "Current time: $(date '+%Y-%m-%d %H:%M:%S %Z')" exit 0
Configuration:
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/add-timestamp.sh" } ] } ] } }
Example 4: Auto-Approve Documentation Reads
Hook script (
.claude/hooks/auto-approve-docs.py):
#!/usr/bin/env python3 import json import sys data = json.load(sys.stdin) if data.get('tool_name') != 'Read': sys.exit(0) file_path = data.get('tool_input', {}).get('file_path', '') # Auto-approve docs if any(file_path.endswith(ext) for ext in ['.md', '.txt', '.json', '.yaml']): output = { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "Documentation file auto-approved" }, "suppressOutput": True } print(json.dumps(output)) sys.exit(0) sys.exit(0)
Configuration:
{ "hooks": { "PreToolUse": [ { "matcher": "Read", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/auto-approve-docs.py" } ] } ] } }
Example 5: Prevent Sensitive File Access
Hook script (
.claude/hooks/block-secrets.sh):
#!/usr/bin/env bash INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Block sensitive files if [[ "$FILE_PATH" =~ \.env || "$FILE_PATH" =~ secrets/ || "$FILE_PATH" =~ \.aws/ ]]; then echo "⛔ Access to sensitive file blocked: $FILE_PATH" >&2 exit 2 fi exit 0
Configuration:
{ "hooks": { "PreToolUse": [ { "matcher": "Read|Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-secrets.sh" } ] } ] } }
Example 6: Intelligent Stop Hook (Prompt-based)
Configuration:
{ "hooks": { "Stop": [ { "hooks": [ { "type": "prompt", "prompt": "Evaluate whether Claude should stop. Context: $ARGUMENTS\n\nCheck if:\n1. All tasks are complete\n2. Tests are passing\n3. No errors need addressing\n\nRespond with JSON: {\"decision\": \"approve\" or \"block\", \"reason\": \"explanation\"}", "timeout": 30 } ] } ] } }
Example 7: Session Setup Hook
Hook script (
.claude/hooks/session-setup.sh):
#!/usr/bin/env bash # Set up environment for session if [ -n "$CLAUDE_ENV_FILE" ]; then # Load nvm source ~/.nvm/nvm.sh nvm use 20 # Capture environment changes export -p >> "$CLAUDE_ENV_FILE" # Add custom variables echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE" fi # Add context echo "Development environment initialized" echo "Node version: $(node --version)" exit 0
Configuration:
{ "hooks": { "SessionStart": [ { "matcher": "startup", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-setup.sh" } ] } ] } }
Matcher Patterns
Exact match:
"matcher": "Write"
Multiple tools (regex):
"matcher": "Write|Edit|NotebookEdit"
All tools:
"matcher": "*"
Or:
"matcher": ""
MCP tools:
"matcher": "mcp__github__.*" "matcher": "mcp__.*__write.*"
Event-specific matchers:
Notification:
"matcher": "permission_prompt" "matcher": "idle_prompt"
PreCompact:
"matcher": "manual" "matcher": "auto"
SessionStart:
"matcher": "startup" "matcher": "resume" "matcher": "clear"
Environment Variables
Available in hook scripts:
- Absolute path to project root$CLAUDE_PROJECT_DIR
- "true" if remote/web, empty if local$CLAUDE_CODE_REMOTE
- (SessionStart only) File to persist env vars$CLAUDE_ENV_FILE- Standard environment variables
Best Practices
DO:
✅ Keep hooks fast (<100ms recommended) ✅ Provide clear error messages ✅ Use appropriate exit codes ✅ Quote variables in bash:
"$VAR"
✅ Validate inputs before processing
✅ Test thoroughly before deploying
✅ Use $CLAUDE_PROJECT_DIR for portability
✅ Document what your hook does
DON'T:
❌ Run slow operations (full test suites) ❌ Block legitimate operations unnecessarily ❌ Use hooks for everything (be selective) ❌ Forget to handle errors ❌ Skip input validation ❌ Hardcode absolute paths ❌ Leave debug output in production
Security Considerations
⚠️ USE AT YOUR OWN RISK
Hooks execute arbitrary commands:
- Can modify/delete any files
- Can access sensitive data
- Can cause data loss
- Anthropic provides no warranty
Best practices:
- Validate and sanitize inputs
- Quote all variables
- Block path traversal (
).. - Use absolute paths
- Skip sensitive files
- Test in safe environment first
Troubleshooting
Hook Not Running
Check:
- Hook is in
correctlysettings.json - Matcher pattern is correct (case-sensitive)
- Script has execute permissions:
chmod +x script.sh - Script shebang is correct:
#!/usr/bin/env bash - Restart Claude Code after config changes
Debug:
# Run with debug mode claude --debug # Check hook execution in output # Shows: "Executing hooks for PostToolUse:Write"
Hook Errors
Check:
- Script runs standalone:
echo '{}' | ./script.sh - Exit code is correct:
echo $? - JSON output is valid:
./script.sh | jq . - Timeout is sufficient (default: 60s)
View errors:
- Verbose mode: Ctrl+O
- Debug mode:
claude --debug - Check stderr output
Permissions Issues
Check:
# Make script executable chmod +x .claude/hooks/script.sh # Verify permissions ls -la .claude/hooks/
JSON Parse Errors
Validate JSON:
# Test JSON output echo '{}' | ./script.sh | jq . # Common issues: # - Missing quotes # - Trailing commas # - Single quotes instead of double
Resources
- Official Hook Docs:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks.md - Hook Guide:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks-guide.md - Settings Reference:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_settings.md - Project Guide:
.claude/docs/creating-components.md