What hooks enable
Hooks add an automation layer to Claude Code without modifying its code. Common uses:
- Auto-format files after writing
- Block dangerous commands (
rm -rf,git push --force) - Generate session reports on stop
- Preserve key info before compaction
- Ask an LLM “is this change safe?” and block on the answer
Configuration locations
| File | Scope |
|---|---|
~/.claude/settings.json | User-global |
.claude/settings.json | Project (committed) |
.claude/settings.local.json | Personal local (gitignored) |
Basic structure
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; prettier --write \"$f\"; } 2>/dev/null || true"
}
]
}
]
}
}
Common events
| Event | When | Use case |
|---|---|---|
| PreToolUse | Before a tool runs | Block dangerous commands, permission checks |
| PostToolUse | After a tool runs | Auto-format, run tests |
| PostToolUseFailure | After a tool fails | Error logging, notifications |
| UserPromptSubmit | When user submits | Prompt analysis, context injection |
| Stop | Session end | Generate reports, cleanup |
| PreCompact | Before compaction | Preserve key info separately |
| SessionStart | Session start | Environment check, intro message |
Three hook types
1. command — run a shell command
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/bash-log.txt"
}
Receives the event JSON on stdin. Use jq to extract fields.
2. prompt — ask an LLM
{
"type": "prompt",
"prompt": "Decide if this command makes a destructive system change. Answer 'block' or 'allow': $ARGUMENTS"
}
The LLM answer drives block/allow. Runs on a small fast model.
3. agent — run an agent
{
"type": "agent",
"prompt": "Verify that unit tests were added alongside the file changes"
}
For complex verification. Higher token cost.
Example: block dangerous commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"prompt": "Decide if this shell command permanently deletes data or makes hard-to-reverse changes. Answer 'block' or 'allow': $ARGUMENTS"
}
]
}
]
}
}
Example: auto-format after write
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; case \"$f\" in *.ts|*.tsx|*.js|*.jsx|*.json|*.md) prettier --write \"$f\";; esac; } 2>/dev/null || true"
}
]
}
]
}
}
Per-extension formatter selection.
Example: stop-time report
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "echo '{\"systemMessage\": \"Session ended — review changed files before closing.\"}'"
}
]
}
]
}
}
JSON output with systemMessage is shown to the user.
Debugging checklist
- JSON syntax —
jq . settings.jsonto validate - Matcher pattern — exact tool name (e.g.
Write|Edit) - Test the command directly —
echo '{"tool_input":{"file_path":"test.ts"}}' | <hook command> - Restart — new hooks need a Claude Code restart or one open of
/hooksso the watcher picks them up - Debug mode —
claude --debugshows hook execution logs
Use hookify
hookify generates hooks from natural language. If hand-writing JSON feels heavy, draft with hookify first and apply the result to settings.json.
Next steps
- hookify — define hooks in plain language
- Claude Code Setup — project-aware hook recommendations
- Writing a Great CLAUDE.md — separate hook automation from CLAUDE.md rules