Claude Code Hooks
Configure a behavior once and it runs on its own: format on every edit, block secrets before they commit, load context at session start.
You keep forgetting. Hooks don't.
Every time you tell Claude Code "remember to run Prettier after editing" or "always check for secrets before committing," you're burning a prompt. And you're going to forget to say it eventually.
Hooks fix this permanently. They're scripts that fire automatically before a tool runs, after a tool runs, or when a session starts. Set them up once and they work silently forever.
Think of them like git hooks, but for Claude Code. And frankly, they're more useful than most git hooks you've actually written.
The Prettier and ESLint hooks also ran after the Write; they print nothing when they pass.
The hook events you'll use first
Claude Code has more than two dozen hook events. These three cover most of what you need on day one.
PreToolUse
Fires before Claude Code does something. This is your gatekeeper.
Use it to block writes to protected files, catch secrets before they land in code, or add a confirmation step before anything destructive.
PostToolUse
Fires after Claude Code does something. This is your cleanup crew.
Auto-format code after every edit. Run a linter on new files. Log tool usage for auditing. The stuff you'd otherwise have to nag about constantly.
SessionStart
Fires when a session begins. This is your context loader.
Remind Claude Code to read memory files. Check that env vars are set. Load the latest handoff so you never start a session cold. This one is criminally underused.
Two more worth knowing early: Stop fires when Claude finishes responding, and UserPromptSubmit fires before Claude reads each prompt you send.
Since v2.1.299, UserPromptSubmit also fires before a skill or custom slash command expands. A hook that blocks the prompt now stops the command's embedded shell commands as well. One exception: a UserPromptSubmit hook defined in a skill's own frontmatter first runs on the prompt after the one that invoked the skill.
How to configure them
Hooks live in your .claude/settings.json. Each event holds a list of groups. A group has a matcher that says which tools it applies to, and a hooks list that says what to run.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "echo 'Checking file write...'" }
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
],
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "echo 'Remember: Read MEMORY.md and check handoffs/ for context.'" }
]
}
]
}
}The matcher field controls which tools trigger the hook. Pipe-separate tool names like "Write|Edit" to match multiple. Leave it out and the hook fires on every tool call.
Your hook gets the details of the tool call as JSON on stdin, not as environment variables. That's why the Prettier hook pipes through jq to pull out tool_input.file_path.
If your settings file already has a hooks key, add the new event next to the existing ones. Don't replace the whole object.
Your first hook (30 seconds, huge payoff)
The single highest-value hook ensures Claude Code never starts a session cold. I've used this one for months and it's saved me more time than any other single config change.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "cat ~/.claude/session-reminder.txt 2>/dev/null || echo 'No session reminder found.'" }
]
}
]
}
}Whatever a SessionStart hook prints goes to Claude as context, so the checklist below lands in front of Claude before your first message. Create ~/.claude/session-reminder.txt with your startup checklist:
Session startup checklist:
1. Read MEMORY.md for persistent context
2. Check _context/handoffs/ for the latest handoff
3. Resume from where the last session left off
4. If no handoff exists, ask what we're working on todayNo more "wait, you forgot about what we did yesterday." Done.
Auto-format on every edit
The second hook everyone should have. Prettier runs after every file edit so formatting is never a conversation:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true" }
]
}
]
}
}The || true keeps things moving if Prettier can't handle a file type. Always add a fallback for non-critical hooks.
The SessionStart hook's checklist told Claude to open the latest handoff, so it knew about PR #142 without you explaining it.
Tips that actually matter
Keep hooks fast. They block the action they're attached to. A 10-second hook means Claude Code pauses 10 seconds before every tool call. Under 1 second is the target. Profile your hooks if they feel slow. Prompt and agent hooks ("type": "prompt" and "type": "agent") run at most 12 per event at a time since v2.1.302, and the rest wait their turn. A waiting hook's timeout starts only when that hook starts.
Test with echo first. Before wiring up a real script, use echo to verify the hook fires when you expect. Confirm the timing, then swap in the real command.
Don't go overboard. Three or four well-chosen hooks beat twenty noisy ones. Start with a SessionStart reminder and one PostToolUse formatter. Add more only when you feel actual friction.
Scope them right. Hooks in ~/.claude/settings.json fire in every project. Hooks in .claude/settings.json fire only in that project and can be committed for your team. Hooks in .claude/settings.local.json fire only in that project and stay on your machine. Project-specific formatting goes at the project level. Universal safety checks go global.
Know the exit codes. Exit 0 means success. Exit 2 means block: on a PreToolUse hook, the tool call doesn't run and Claude sees your stderr message. Any other exit code doesn't block for most events. That's how the secrets check in the demo above stopped the commit.
A broken hook lets the action through. If your hook can't start, crashes, or prints JSON that Claude Code can't parse, that counts as a non-blocking error: the action goes ahead and you see a hook error notice. Exit 0 with broken JSON gets the same treatment. That's fine for a formatter and bad for a secrets check. On a hook that guards something, set "onFailure": "block" (v2.1.295 or later) next to "type" and "command", so a hook that fails to start, times out, or exits with an unexpected code blocks the action instead.
Decide before you go to the background. If a hook hands its decision to a background process and exits, a deny that process prints later doesn't block anything, because Claude Code has already stopped reading. Make the policy check in the foreground, keep its exit code, and only then start background work with both output streams redirected (> /dev/null 2>&1 &). A hook that prints an allow and leaves a background process attached to its output gets the tool call blocked, with refusing to honor the suspect capture in the error. To test it, feed the hook a call it should deny and confirm the call is blocked.
Rewritten input gets checked. A PreToolUse or PermissionRequest hook can return updatedInput to change a tool call before it runs. Since v2.1.300, Claude Code validates it like Claude's own input: unknown keys are dropped, and input the tool rejects denies the call.
Handle failures gracefully. Add 2>/dev/null || true to non-critical hooks like formatters, so a file type Prettier can't handle doesn't throw an error after every edit. Test commands on their own before adding them to config.
New guides, when they ship
One email, roughly weekly. CLAUDE.md templates, workflows I actually use, and the cut-for-length stuff that does not make the public guides. One-click unsubscribe.
Or read Product Field Notes, the Substack
