The events I actually use
PreToolUse— fires before a tool runs. Can block the call.PostToolUse— fires after a tool runs. Cannot block, but can lint, format, or notify.Stop— fires when the assistant finishes a turn. Good for notifications.UserPromptSubmit— fires when the user sends a message. Good for injecting context.
Hook one: auto-format on edit
Every time Claude edits a file, run the formatter for that file type:
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "pnpm prettier --write \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true"
}]
}]
}
}
No more reminding the model to match code style. The formatter runs after every write. Zero friction.
Hook two: block accidental secret commits
{
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "if echo \"$CLAUDE_TOOL_COMMAND\" | grep -qE 'git (add|commit).*\\.env'; then echo 'Refusing to stage .env'; exit 1; fi"
}]
}]
}
If the model tries to stage a .env file, the hook exits non-zero and the tool call is blocked. Belt and suspenders against the "oops committed secrets" class of bug.
Hook three: run tests for the affected area
{
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "./scripts/test-for-file.sh \"$CLAUDE_TOOL_FILE_PATH\""
}]
}]
}
test-for-file.sh maps a source file to its test file and runs only that test. Fast feedback; no waiting for a full suite.
Hook four: notify on stop
{
"Stop": [{
"hooks": [{
"type": "command",
"command": "notify-send 'Claude' 'Turn complete'"
}]
}]
}
Long-running tasks — migrations, refactors across many files — fire a desktop notification when done. I can switch to another window and come back when it pings.
Where hooks live
User-level: ~/.claude/settings.json. Project-level: .claude/settings.json. Per-project local (not checked in): .claude/settings.local.json. Project settings override user settings; local overrides everything.
I keep general hooks (format, notify) in user settings and project-specific hooks (test runners, lint configs) in project settings.
The gotcha
Hooks run synchronously for PreToolUse. A slow hook blocks the tool call until it finishes. For formatters and linters this is fine. For anything that could hang, fire it in the background and let PostToolUse be fire-and-forget.
What hooks are not for
Hooks execute deterministically — they are not AI behavior. If you want the model to decide whether to do something, that belongs in a skill or in your prompt. Hooks are for "always do this when X happens." Treat the distinction seriously; fighting the model via hooks works for a week and breaks on the novel case.
