creating-hooks

📁 lexler/skill-factory 📅 13 days ago
3
总安装量
3
周安装量
#58071
全站排名
安装命令
npx skills add https://github.com/lexler/skill-factory --skill creating-hooks

Agent 安装分布

opencode 3
gemini-cli 3
antigravity 3
junie 3
claude-code 3
github-copilot 3

Skill 文档

STARTER_CHARACTER = 🪝

Setup

First, update the reference docs to get the latest from Anthropic:

python ~/.claude/skills/creating-hooks/scripts/update-docs.py

What Hooks Are

Shell commands that execute at lifecycle points in Claude Code. Unlike prompts, hooks are deterministic—they always run when triggered.

Configuration

Hooks live in settings files:

  • ~/.claude/settings.json – User settings (all projects)
  • .claude/settings.json – Project settings (shared via git)
  • .claude/settings.local.json – Local project settings (not committed)
{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "your-command-here"
          }
        ]
      }
    ]
  }
}

Matcher: Pattern to match tool names (case-sensitive)

  • Exact match: Write
  • Regex: Edit|Write
  • All tools: * or omit

Environment variables:

  • $CLAUDE_PROJECT_DIR – Absolute path to project root
  • $CLAUDE_ENV_FILE – File path for persisting env vars (SessionStart only)

Hook Events

Tool events (matcher applies):

  • PreToolUse – Before tool executes
  • PostToolUse – After tool completes
  • PermissionRequest – Permission dialog shown

Session events:

  • SessionStart – Session begins/resumes (matcher: startup/resume/clear/compact)
  • SessionEnd – Session ends
  • PreCompact – Before compaction (matcher: manual/auto)

Other events:

  • UserPromptSubmit – User submits prompt
  • Stop – Agent finishes
  • SubagentStop – Subagent finishes
  • Notification – Alerts sent (matcher: notification type)

Exit Codes

  • 0: Success. stdout shown in verbose mode. For UserPromptSubmit/SessionStart, stdout added to context.
  • 2: Block. stderr fed to Claude as error message. Blocks the action.
  • Other: Non-blocking error. stderr shown to user.

JSON Output

For advanced control, return JSON to stdout with exit code 0:

{
  "continue": false,
  "stopReason": "Message shown when stopping"
}

PreToolUse Control

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Auto-approved",
    "updatedInput": { "field": "modified value" }
  }
}

Decisions: "allow" (bypass permission), "deny" (block), "ask" (prompt user)

PostToolUse Feedback

{
  "decision": "block",
  "reason": "Explanation fed to Claude"
}

Stop/SubagentStop Control

{
  "decision": "block",
  "reason": "Must fix X before stopping"
}

Hook Input

Hooks receive JSON via stdin:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/dir",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": { "file_path": "/path", "content": "..." }
}

Common Patterns

Auto-format after edit:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path' | xargs -I{} sh -c 'echo {} | grep -q \"\\.ts$\" && npx prettier --write {}'"
      }]
    }]
  }
}

Block dangerous commands:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-bash.py"
      }]
    }]
  }
}

Inject context on prompt:

{
  "hooks": {
    "UserPromptSubmit": [{
      "hooks": [{
        "type": "command",
        "command": "echo '[REMINDER: Follow TDD]'"
      }]
    }]
  }
}

Desktop notification:

{
  "hooks": {
    "Notification": [{
      "hooks": [{
        "type": "command",
        "command": "osascript -e 'display notification \"Claude needs input\" with title \"Claude Code\"'"
      }]
    }]
  }
}

Hook Scripts

For complex logic, use external scripts. UV single-file format works well:

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = []
# requires-python = ">=3.11"
# ///

import json
import sys

data = json.load(sys.stdin)
tool_input = data.get("tool_input", {})

# Validation logic here

if should_block:
    print("Error message", file=sys.stderr)
    sys.exit(2)

sys.exit(0)

Anti-Patterns

  • Using exit code 2 without stderr message (Claude gets no feedback)
  • Forgetting to handle JSON parsing errors in scripts
  • Blocking without explaining why (Claude will retry the same thing)
  • Long-running hooks without timeout (default is 60s)
  • Modifying files in PreToolUse (use PostToolUse for modifications)

Reference