Hook Authoring — Official Best Practices

SkillAI & models

Creates and configures Claude Code hooks for lifecycle automation. Covers all 17 hook events, 4 hook types (command, prompt, agent, http), matchers, input/output formats, and exit codes. Follows official Anthropic best practices.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Hook Authoring — Official Best Practices skill

What this skill tells your AI

The instructions your AI receives, as published by claude-dev-suite/claude-dev-suite in skills/claude-code-authoring/hook-authoring/SKILL.md and read by ahel’s review.

What Hooks Do

Hooks are deterministic shell commands (or LLM prompts) that execute at specific lifecycle points. They provide guaranteed behavior — not relying on the LLM to choose to run them.

Configuration Locations

LocationScopeShareable
~/.claude/settings.jsonAll projectsNo
.claude/settings.jsonSingle projectYes (commit)
.claude/settings.local.jsonSingle projectNo (gitignored)
Agent/skill frontmatterWhile component activeYes
Plugin hooks/hooks.jsonWhen plugin enabledYes

Hook Types

TypeHow it worksUse when
commandRuns shell command, reads stdin JSON, uses exit codesDeterministic validation, formatting, logging
promptSingle-turn LLM call, returns {ok, reason}Judgment-based decisions without tool access
agentMulti-turn subagent with tool accessVerification requiring file reads or commands
httpPOSTs event data to URL endpointExternal service integration, audit logging

Hook Events

See quick-ref/events-reference.md for full input/output schemas.

EventMatcher inputCan block?Common use
SessionStartstartup/resume/clear/compactNoRe-inject context after compaction
UserPromptSubmit(none)YesValidate/transform user input
PreToolUseTool nameYesBlock commands, validate operations
PermissionRequestTool nameYesAuto-allow/deny permissions
PostToolUseTool nameNo*Auto-format files, logging
PostToolUseFailureTool nameNoError handling
NotificationNotification typeNoDesktop alerts
SubagentStartAgent typeNoSetup before agent runs
SubagentStopAgent typeNoCleanup after agent
Stop(none)YesVerify completeness
ConfigChangeConfig sourceYesAudit, block unauthorized changes
PreCompactmanual/autoNoSave context before compaction
SessionEndExit reasonNoCleanup

*PostToolUse Stop hooks can return {"decision": "block"} to keep Claude working.

Configuration Format

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Input/Output Protocol

Input (stdin JSON)

Every hook receives JSON on stdin with common fields + event-specific data:

{
  "session_id": "abc123",
  "cwd": "/path/to/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "npm test" }
}

Output (exit codes)

Exit codeEffect
0Allow — action proceeds. Stdout added to context (SessionStart, UserPromptSubmit)
2Block — action cancelled. Stderr sent to Claude as feedback
OtherAllow — stderr logged (visible in verbose mode Ctrl+O)

Structured JSON output (exit 0 + JSON on stdout)

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use rg instead of grep"
  }
}

PreToolUse decisions: "allow", "deny", "ask".

Common Patterns

Auto-format after edits

{
  "PostToolUse": [{
    "matcher": "Edit|Write",
    "hooks": [{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }]
  }]
}

Block protected files

#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
for pattern in ".env" "package-lock.json" ".git/"; do
  if [[ "$FILE" == *"$pattern"* ]]; then
    echo "Blocked: matches protected pattern '$pattern'" >&2
    exit 2
  fi
done
exit 0

Re-inject context after compaction

{
  "SessionStart": [{
    "matcher": "compact",
    "hooks": [{ "type": "command", "command": "echo 'Reminder: use Bun, not npm. Run tests before commits.'" }]
  }]
}

Notification on idle

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

Stop Hook Infinite Loop Prevention

Always check stop_hook_active to avoid loops:

INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0  # Let Claude stop
fi
# ... your logic

Anti-Patterns

Anti-PatternFix
Shell profile echo breaks JSONWrap in if [[ $- == *i* ]]
Stop hook without loop guardCheck stop_hook_active field
Using PostToolUse to undo actionsToo late — use PreToolUse to block instead
Relying on PermissionRequest in headless modeDoesn't fire in -p mode. Use PreToolUse

Checklist

  • Correct event chosen for the use case
  • Matcher pattern tested (case-sensitive, regex)
  • Script is executable (chmod +x)
  • Uses jq for JSON parsing (or Python/Node)
  • Exit code 2 for blocking, 0 for allowing
  • Stop hooks check stop_hook_active
  • Tested with sample JSON piped to stdin
  • Hook script uses absolute paths or $CLAUDE_PROJECT_DIR

Reference

Signals

GitHub stars
33
Forks
6
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
hook-authoring-claude-dev-suite
Source
github.com/claude-dev-suite/claude-dev-suite