Agent Skills: Hook Creator (Tier 3 - Full Reference)

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

UncategorizedID: HTRamsey/claude-config/hook-creator

Skill Files

Browse the full folder contents for hook-creator.

Download Skill

Loading file tree…

skills/hook-creator/SKILL.md

Skill Metadata

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

| Event | When It Runs | Common Uses | |-------|--------------|-------------| | PreToolUse | Before tool executes | Block, warn, suggest alternatives | | PostToolUse | After tool completes | Cache results, chain to other tools, log | | UserPromptSubmit | When user sends message | Context monitoring, input validation | | Stop | When Claude stops | Session 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

| Field | Available In | Contains | |-------|--------------|----------| | tool_name | Pre/Post | Tool name: "Bash", "Edit", "Task", etc. | | tool_input | Pre/Post | Tool parameters (dict) | | tool_result | PostToolUse only | Tool output (dict or str) | | cwd | All | Current working directory | | session_id | All | Session 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

| Decision | Effect | |----------|--------| | approve | Allow tool, show message | | deny | Block tool, show reason | | block | Hard block (same as deny) | | ask | Request 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

| Situation | Escalate To | |-----------|-------------| | Hook denies frequently | Rethink rule - consider skill or agent instead | | Logic too complex for 1s timeout | agent-creator skill for subagent | | Multiple hooks conflict | User to resolve priority/ordering | | Requires human judgment | User 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

| Mistake | Fix | |---------|-----| | Wrong output format | Use hookSpecificOutput wrapper | | Checking hook_type field | Check for tool_result instead | | Using tool_response | Use tool_result | | Exit code 1/2 on error | Always sys.exit(0) | | Long timeouts | Keep under 5s, prefer 1s | | Printing debug output | Only print JSON result | | No exception handling | Wrap 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

| Symptom | Cause | Fix | |---------|-------|-----| | "Hook failed" in logs | Non-zero exit or exception | Add try/except, always exit(0) | | No effect from hook | Wrong output format | Use hookSpecificOutput wrapper | | Hook blocks everything | Matcher too broad | Use specific tool name | | Permission denied | Script not executable | chmod +x hook.py | | ModuleNotFoundError | Wrong Python env | Check 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

| Do | Don't | |----|-------| | Use dispatchers for PreToolUse/PostToolUse | Register hooks individually | | Fail gracefully (hook_utils.py patterns) | Let errors crash | | Target specific tools only | Watch all tools unnecessarily | | Keep under 100ms latency | Block on slow operations | | Log to hook-events.jsonl | Print to stdout |