Hook Authoring — Official Best Practices
SkillAI & modelsCreates 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.
No other account needed.
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
| Location | Scope | Shareable |
|---|---|---|
~/.claude/settings.json | All projects | No |
.claude/settings.json | Single project | Yes (commit) |
.claude/settings.local.json | Single project | No (gitignored) |
| Agent/skill frontmatter | While component active | Yes |
Plugin hooks/hooks.json | When plugin enabled | Yes |
Hook Types
| Type | How it works | Use when |
|---|---|---|
command | Runs shell command, reads stdin JSON, uses exit codes | Deterministic validation, formatting, logging |
prompt | Single-turn LLM call, returns {ok, reason} | Judgment-based decisions without tool access |
agent | Multi-turn subagent with tool access | Verification requiring file reads or commands |
http | POSTs event data to URL endpoint | External service integration, audit logging |
Hook Events
See quick-ref/events-reference.md for full input/output schemas.
| Event | Matcher input | Can block? | Common use |
|---|---|---|---|
SessionStart | startup/resume/clear/compact | No | Re-inject context after compaction |
UserPromptSubmit | (none) | Yes | Validate/transform user input |
PreToolUse | Tool name | Yes | Block commands, validate operations |
PermissionRequest | Tool name | Yes | Auto-allow/deny permissions |
PostToolUse | Tool name | No* | Auto-format files, logging |
PostToolUseFailure | Tool name | No | Error handling |
Notification | Notification type | No | Desktop alerts |
SubagentStart | Agent type | No | Setup before agent runs |
SubagentStop | Agent type | No | Cleanup after agent |
Stop | (none) | Yes | Verify completeness |
ConfigChange | Config source | Yes | Audit, block unauthorized changes |
PreCompact | manual/auto | No | Save context before compaction |
SessionEnd | Exit reason | No | Cleanup |
*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 code | Effect |
|---|---|
0 | Allow — action proceeds. Stdout added to context (SessionStart, UserPromptSubmit) |
2 | Block — action cancelled. Stderr sent to Claude as feedback |
| Other | Allow — 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-Pattern | Fix |
|---|---|
Shell profile echo breaks JSON | Wrap in if [[ $- == *i* ]] |
| Stop hook without loop guard | Check stop_hook_active field |
| Using PostToolUse to undo actions | Too late — use PreToolUse to block instead |
| Relying on PermissionRequest in headless mode | Doesn'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
jqfor 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