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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/data/claude-hook-builder-pur3v4d3r-pur3-pkb-codebase/SKILL.md
source content

Claude 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:

  • $CLAUDE_PROJECT_DIR
    - Absolute path to project root
  • $CLAUDE_CODE_REMOTE
    - "true" if remote/web, empty if local
  • $CLAUDE_ENV_FILE
    - (SessionStart only) File to persist env vars
  • 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:

  1. Hook is in
    settings.json
    correctly
  2. Matcher pattern is correct (case-sensitive)
  3. Script has execute permissions:
    chmod +x script.sh
  4. Script shebang is correct:
    #!/usr/bin/env bash
  5. 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:

  1. Script runs standalone:
    echo '{}' | ./script.sh
  2. Exit code is correct:
    echo $?
  3. JSON output is valid:
    ./script.sh | jq .
  4. 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