hook-creator

Create native Claude Code hooks in ~/.claude/hooks/. Use when adding event-triggered automation (PreToolUse, PostToolUse, Stop, etc.).

$ 설치

git clone https://github.com/HTRamsey/claude-config /tmp/claude-config && cp -r /tmp/claude-config/skills/hook-creator ~/.claude/skills/claude-config

// tip: Run this command in your terminal to install the skill


name: hook-creator description: Create native Claude Code hooks in ~/.claude/hooks/. Use when adding event-triggered automation (PreToolUse, PostToolUse, Stop, etc.).

Hook Creator (Tier 3 - Full Reference)

Persona: Defensive programmer creating fail-safe hooks - prioritizes silent failure over breaking workflows.

Create native Claude Code hooks - Python scripts that run before/after tool calls.

Note: Core workflow is in instructions.md. This file contains detailed templates, context fields, and registration details.

Hook Types

EventWhen It RunsCommon Uses
PreToolUseBefore tool executesBlock, warn, suggest alternatives
PostToolUseAfter tool completesCache results, chain to other tools, log
UserPromptSubmitWhen user sends messageContext monitoring, input validation
StopWhen Claude stopsSession persistence, uncommitted reminders

Critical: Output Format

All hooks must return JSON with hookSpecificOutput:

# PreToolUse - approve/deny/block
result = {
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "approve",  # or "deny", "block"
        "permissionDecisionReason": "Message shown to Claude"
    }
}

# PostToolUse - informational message
result = {
    "hookSpecificOutput": {
        "hookEventName": "PostToolUse",
        "message": "Message shown to Claude"
    }
}

# UserPromptSubmit - same as PreToolUse format
result = {
    "hookSpecificOutput": {
        "hookEventName": "UserPromptSubmit",
        "permissionDecision": "approve",
        "permissionDecisionReason": "Message if not approved"
    }
}

WRONG formats (will cause errors):

# BAD - old format
{"decision": "approve", "message": "..."}

# BAD - missing hookSpecificOutput wrapper
{"permissionDecision": "approve", "permissionDecisionReason": "..."}

Context Fields Available

FieldAvailable InContains
tool_namePre/PostTool name: "Bash", "Edit", "Task", etc.
tool_inputPre/PostTool parameters (dict)
tool_resultPostToolUse onlyTool output (dict or str)
cwdAllCurrent working directory
session_idAllSession identifier

Detect Pre vs Post:

if "tool_result" in ctx:
    # PostToolUse
else:
    # PreToolUse

Hook Template

#!/usr/bin/env python3
"""
{Description of what this hook does}.

{Event} hook for {Tool} tool.
"""
import json
import sys

def main():
    try:
        ctx = json.load(sys.stdin)
    except json.JSONDecodeError:
        sys.exit(0)  # Silent failure

    tool_name = ctx.get("tool_name", "")
    tool_input = ctx.get("tool_input", {})

    # Early exit if not our target tool
    if tool_name != "TargetTool":
        sys.exit(0)

    # Your logic here
    should_warn = False  # Replace with actual check

    if should_warn:
        result = {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "approve",
                "permissionDecisionReason": "Warning message here"
            }
        }
        print(json.dumps(result))

    sys.exit(0)

if __name__ == "__main__":
    main()

Settings.json Configuration

Add hook to ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/my_hook.py",
            "timeout": 1,
            "once": true
          }
        ]
      }
    ]
  }
}

Hook options:

  • type: "command" (required)
  • command: Path to hook script (required)
  • timeout: Max execution time in seconds (default: 30)
  • once: If true, hook runs only once per session (NEW)

Matcher patterns:

  • Single tool: "Bash"
  • Multiple tools: "Bash|Edit|Write"
  • All tools: "*" or omit matcher

PreToolUse Middleware Pattern (NEW)

Hooks can now return updatedInput with ask permission to modify tool input while still requesting user consent:

result = {
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "ask",  # Request user consent
        "permissionDecisionReason": "Modified input - please confirm",
        "updatedInput": {
            "command": "safer_command_here"
        }
    }
}

Permission Decisions

DecisionEffect
approveAllow tool, show message
denyBlock tool, show reason
blockHard block (same as deny)
askRequest user consent (can include updatedInput)
(no output)Silent approval

Should NOT Attempt

  • Complex logic that might timeout (keep under 1s)
  • Side effects in PreToolUse hooks (only decide, don't act)
  • Blocking without clear reason (frustrates workflow)
  • Denying based on uncertain heuristics
  • Network calls in hooks (too slow, may fail)
  • Reading large files (use caching instead)

Failure Behavior

Always fail silently. A broken hook should never block work:

def main():
    try:
        ctx = json.load(sys.stdin)
        # ... your logic ...
    except Exception:
        pass  # Silent failure
    finally:
        sys.exit(0)  # Always exit 0

Never:

  • Exit with non-zero codes
  • Print error messages to stdout
  • Let exceptions propagate

Escalation Triggers

SituationEscalate To
Hook denies frequentlyRethink rule - consider skill or agent instead
Logic too complex for 1s timeoutagent-creator skill for subagent
Multiple hooks conflictUser to resolve priority/ordering
Requires human judgmentUser clarification or manual intervention

Best Practices

  1. Silent on success: Return nothing if no action needed
  2. Timeout of 1s: Keep hooks fast, use 1-5s timeout max
  3. Graceful errors: Catch all exceptions, exit 0 on failure
  4. No side effects in Pre: PreToolUse should only decide, not act
  5. Cache expensive ops: Use /tmp for caching

Examples

Block Dangerous Commands (PreToolUse)

#!/usr/bin/env python3
import json, sys, re

DANGEROUS = [r"rm\s+-rf\s+/", r"chmod\s+777", r">\s*/dev/sd"]

try:
    ctx = json.load(sys.stdin)
    if ctx.get("tool_name") != "Bash":
        sys.exit(0)

    cmd = ctx.get("tool_input", {}).get("command", "")
    for pattern in DANGEROUS:
        if re.search(pattern, cmd):
            print(json.dumps({
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"Blocked dangerous command: {cmd[:50]}"
                }
            }))
            break
except Exception:
    pass
sys.exit(0)

Log Tool Usage (PostToolUse)

#!/usr/bin/env python3
import json, sys
from datetime import datetime
from pathlib import Path

try:
    ctx = json.load(sys.stdin)
    log = Path("/tmp/claude_tool_log.jsonl")
    entry = {
        "time": datetime.now().isoformat(),
        "tool": ctx.get("tool_name"),
        "success": "error" not in str(ctx.get("tool_result", "")).lower()
    }
    with open(log, "a") as f:
        f.write(json.dumps(entry) + "\n")
except Exception:
    pass
sys.exit(0)

Unified Pre/Post Hook

#!/usr/bin/env python3
import json, sys

try:
    ctx = json.load(sys.stdin)
    tool_name = ctx.get("tool_name", "")

    if "tool_result" in ctx:
        # PostToolUse logic
        pass
    else:
        # PreToolUse logic
        pass
except Exception:
    pass
sys.exit(0)

Validation

Test hook before adding to settings:

echo '{"tool_name": "Bash", "tool_input": {"command": "ls"}}' | python3 ~/.claude/hooks/my_hook.py

Verify syntax:

python3 -m py_compile ~/.claude/hooks/my_hook.py

Common Mistakes

MistakeFix
Wrong output formatUse hookSpecificOutput wrapper
Checking hook_type fieldCheck for tool_result instead
Using tool_responseUse tool_result
Exit code 1/2 on errorAlways sys.exit(0)
Long timeoutsKeep under 5s, prefer 1s
Printing debug outputOnly print JSON result
No exception handlingWrap everything in try/except

Troubleshooting

Hook Not Triggered

# 1. Check hook is registered in settings.json
jq '.hooks' ~/.claude/settings.json

# 2. Verify matcher pattern matches tool
# "Bash" matches Bash tool, "Bash|Edit" matches both

# 3. Check hook is executable
ls -la ~/.claude/hooks/my_hook.py
chmod +x ~/.claude/hooks/my_hook.py

# 4. Test hook manually
echo '{"tool_name": "Bash", "tool_input": {"command": "ls"}}' | python3 ~/.claude/hooks/my_hook.py

Hook Not Producing Output

# 1. Verify JSON output format
echo '{"tool_name": "Bash", "tool_input": {"command": "rm -rf /"}}' | python3 ~/.claude/hooks/my_hook.py | jq .

# 2. Check for exceptions silently swallowed
# Add temporary logging:
import sys
sys.stderr.write(f"Debug: {variable}\n")

# 3. Verify hookSpecificOutput wrapper is present
# Must be: {"hookSpecificOutput": {...}}
# NOT: {"decision": ...} or {"permissionDecision": ...}

Hook Timing Out

# 1. Check current timeout in settings.json
jq '.hooks.PreToolUse[].hooks[].timeout' ~/.claude/settings.json

# 2. Profile hook execution time
time echo '{"tool_name": "Bash"}' | python3 ~/.claude/hooks/my_hook.py

# 3. Move slow operations to PostToolUse or async
# PreToolUse should complete in <100ms

Common Errors

SymptomCauseFix
"Hook failed" in logsNon-zero exit or exceptionAdd try/except, always exit(0)
No effect from hookWrong output formatUse hookSpecificOutput wrapper
Hook blocks everythingMatcher too broadUse specific tool name
Permission deniedScript not executablechmod +x hook.py
ModuleNotFoundErrorWrong Python envCheck shebang, use venv

Debugging Commands

# View hook execution logs
tail -100 ~/.claude/data/hook-events.jsonl | jq .

# Benchmark hook latency
~/.claude/scripts/diagnostics/hook-benchmark.sh my_hook.py

# Test all hooks
~/.claude/scripts/diagnostics/test-hooks.sh

# Check hook status
~/.claude/scripts/diagnostics/hook-cli.sh status my_hook

Related Skills

  • skill-creator: Create skills that use hooks
  • agent-creator: Create agents that complement hooks
  • command-creator: Create commands that trigger hooks

When Blocked

If unable to create a working hook:

  • Verify the hook event type exists
  • Check if the use case is better suited for an agent
  • Consider if a command/skill is more appropriate
  • Simplify the logic to meet timeout constraints

Design Patterns

DoDon't
Use dispatchers for PreToolUse/PostToolUseRegister hooks individually
Fail gracefully (hook_utils.py patterns)Let errors crash
Target specific tools onlyWatch all tools unnecessarily
Keep under 100ms latencyBlock on slow operations
Log to hook-events.jsonlPrint to stdout