This skill should be used when creating, modifying, or debugging Claude Code hooks. Triggers on phrases like "create a hook", "add a hook", "make a PreToolUse hook", "automate before tool runs", "block tool execution", "validate tool input", "log tool output", "hook for Write/Edit/Bash", "PostToolUse handler", "session start hook", or when working with files in .claude/hooks/ directory.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
reference/event-payloads.mdreference/hook-types.mdreference/hooks-config.mdreference/matcher-types.mdreference/response-patterns.mdreference/tdd-testing-workflow.mdreference/templates/output-to-user/README.mdreference/templates/output-to-user/compact-info.shreference/templates/output-to-user/dashboard-rich.shreference/templates/output-to-user/error-warning.shreference/templates/output-to-user/minimal-status.shreference/templates/output-to-user/progress-steps.shreference/templates/output-to-user/style-flags.shreference/templates/output-to-user/table-data.shreference/templates/verbose-comparison.shreference/test-template.sh| Resource | Path |
|---|---|
| Shared Library | .claude/hooks/_shared/hook-lib.sh |
| Config | .claude/hooks/hooks-config.json |
| Settings | .claude/settings.json |
Reference Files: Refer to and read fully with progressive disclosure
Skip this step if the optimal path is clear from the user's request. Otherwise, ask clarifying questions:
| Category | Question | Why Ask |
|---|---|---|
| Event Type | "Should this BLOCK the action or just OBSERVE/LOG it?" | PreToolUse blocks, PostToolUse observes |
| Event Type | "Should this happen BEFORE or AFTER the tool runs?" | Determines PreToolUse vs PostToolUse |
| Matcher | "Should this apply to ALL tools or specific ones?" | Prevents performance issues from broad matchers |
| Response | "If validation fails, should Claude RETRY with guidance or ABANDON the action?" | Exit 2 (retry) vs deny (abandon) |
| Response | "Should Claude see the result (costs tokens) or just the user?" | additionalContext vs systemMessage |
| Output | "Verbose logs, minimal notifications, or silent execution?" | Determines logging strategy |
| Modification | "Should this MODIFY the input or BLOCK the action?" | updatedInput vs permissionDecision |
| Performance | "Is this critical path (<50ms) or can it be slower?" | Determines timeout and optimization needs |
| Failure | "If this hook fails, should the action be BLOCKED or ALLOWED?" | Fail-secure vs fail-safe |
Tip: Prioritize questions that impact architecture (event type, blocking behavior) over cosmetic ones (output verbosity).
1. Choose Hook Type:
| Type | Use When | Timeout |
|---|---|---|
command (default) | Block, format, log, validate | seconds (default 60) |
prompt | Intelligent completion check | seconds (default 30) |
⚠️
prompttype only works forStopandSubagentStopevents
2. Determine Event Type (Keywords in user request): IMPORTANT: First read event-payloads.md & matcher-types.md (PreToolUse/PostToolUse/PermissionRequest only) to understand the event types and matcher patterns
mcp-cli tools | mcp-cli grep "keyword" | mcp-cli info server/tool| Keywords | Event Type | Can Block | Can Modify |
|---|---|---|---|
| "before", "block", "prevent", "validate" | PreToolUse | ✓ exit 2 | ✓ updatedInput |
| "after", "format", "log", "analyze result" | PostToolUse | - | ✓ additionalContext |
| "tool failed", "handle error" | PostToolUseFailure | - | ✓ feedback |
| "permission", "allow", "deny dialog" | PermissionRequest | ✓ decision | ✓ updatedInput |
| "prompt", "enhance prompt", "add context" | UserPromptSubmit | ✓ exit 2 | ✓ additionalContext |
| "startup", "initialize", "env vars" | SessionStart | - | ✓ stdout→Claude |
| "cleanup", "commit", "session end" | SessionEnd | - | - |
| "notification", "alert" | Notification | - | - |
| "stop", "check completion" | Stop | ✓ exit 2 | - |
| "subagent start", "task start" | SubagentStart | - | ✓ stdout→subagent |
| "subagent stop", "task complete" | SubagentStop | ✓ exit 2 | - |
| "compact", "before compaction" | PreCompact | ✓ exit 2 | ✓ stdout→instructions |
| Tool Type | Pattern Example |
|---|---|
| Single tool | "Bash", "Edit", "Write" |
| Multiple tools | "Edit|Write" |
| MCP tools | "mcp__supabase__.*" |
| All tools | "*" |
3. Define Response Strategy:
| Goal | Method |
|---|---|
| Block tool (retry with guidance) | exit 2 + stderr message |
| Block tool (permanently) | JSON permissionDecision: "deny" |
| Allow and modify input | JSON permissionDecision: "allow" + updatedInput |
| Add context to Claude | JSON additionalContext in hookSpecificOutput |
| Silent pass-through | exit 0 with no output |
4. Determine User Output Strategy:
5. DOs and DON'Ts:
| DO | Why | Implementation |
|---|---|---|
| Read stdin once & cache | Stream consumed on read | input=$(cat) |
| Use Exit 0 for success | Only Exit 0 parses JSON | exit 0 |
| Use Exit 2 for blocking | Blocks tool, stderr → Claude | echo "Reason" >&2; exit 2 |
| Quote shell variables | Prevent injection | "$var" |
Use jq for JSON | Safe parsing | jq -r '.field' |
| Validate JSON output | Invalid JSON fails silently | echo "$json" | jq empty |
Check stop_hook_active | Prevent infinite loops | Stop hooks only |
| Use absolute paths | Relative paths fail | $CLAUDE_PROJECT_DIR |
| Keep PreToolUse fast | Delays execution | Target <100ms |
| DON'T | Why | Fix |
|---|---|---|
| Output JSON on Exit 2 | Stdout ignored on error | Use stderr text only |
| Use "deny" for retry | Abandons tool completely | Use Exit 2 instead |
| Assume fields exist | Payloads vary | Check with jq -e |
| Use deprecated fields | decision/reason gone | Use permissionDecision |
| Parse JSON with grep | Fragile/Unsafe | Always use jq |
| Hardcode paths | Breaks envs | Use env vars |
| Trust user input | Security risk | Validate everything |
| Mix Exit codes & JSON | Exit 2 overrides JSON | Pick one strategy |
| DO | Why |
|---|---|
Source hook-lib.sh | Shared library access |
Use is_feature_enabled | Config toggle support |
Use return_* functions | Standardized output |
Use json_get wrapper | Safe jq abstraction |
Log with log_* | Standardized logging |
6. Verify & Ask (Gap Analysis): If details are ambiguous, ask the user (prioritize questions that impact architecture):
| Ambiguity Category | Key Question | Why Ask |
|---|---|---|
| Event Type | "Should this BLOCK the action (PreToolUse) or just OBSERVE/LOG it (PostToolUse)?" | Critical for security/validation vs logging |
| Matcher Scope | "Should this apply to ALL tools, or only specific ones (e.g., just 'Write' or 'Bash')?" | Prevents performance issues from broad matchers |
| Response Strategy | "If validation fails, should Claude be blocked (Exit 2) or told to retry (JSON deny)?" | Determines exit code vs JSON strategy |
| Output Verbosity | "Do you want verbose logs, minimal notifications, or silent execution?" | Impacts user experience/noise |
| Performance | "Is this a critical path operation (must be <50ms) or background task?" | Determines timeout and async needs |
Show: hook purpose, event type, target tools, expected behavior.
Do NOT read reference docs yet - wait for RED phase to reveal what you actually need.
Goal: See what Claude does WITHOUT the hook. Document exact failures.
Now read the docs - targeted by what you observed in RED:
.claude/hooks/utils/{eventType}/{feature-name}.sh:#!/usr/bin/env bash
# Hook: {feature-name} | Event: {EventType} | Matcher: {pattern}
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../_shared/hook-lib.sh"
[[ "$(is_feature_enabled "{eventType}.{featureName}")" == "true" ]] || return_silent
payload="$(cat)"
tool_name="$(json_get "$payload" ".tool_name")"
# --- HOOK LOGIC ---
return_silent
Copy reference/test-template.sh and customize.
bash .claude/hooks/utils/{eventType}/{feature-name}.test.sh
bash .claude/hooks/tests/ensure-implementation.sh .claude/hooks/utils/{eventType}/{feature-name}.sh
{
"hooks": {
"{EventType}": [{
"matcher": "Write|Edit",
"hooks": [{"type": "command", "command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/utils/{eventType}/{feature-name}.sh", "timeout": 5}]
}]
}
}
bash .claude/hooks/tests/run-tests.sh --allPLAN (Steps 1-3):
TDD TEST (Steps 4-6):
FINALIZE (Steps 7-9):
run-tests.sh --all passes