Hooks
Status: 🟩 COMPLETE (🟦 LIVING) Last updated: 2026-06-19 Plain-English tagline: Shell commands that run automatically when Claude Code does specific things — before tool use, after a session ends, when a task completes. The way you enforce non-negotiable behavior.
In plain English
Most of Claude’s behavior is soft — the model decides what to do based on its prompt and your message. That’s powerful but probabilistic. Sometimes you want behavior that’s hard — must happen, every time, regardless of what the model chooses.
Hooks are how you do that. A hook is a shell command (or shell script) that Claude Code runs automatically on a specific event:
- Before a tool runs (
PreToolUse) - After a tool runs (
PostToolUse) - When Claude finishes a turn (
Stop) - When you submit a message (
UserPromptSubmit) - When the session starts (
SessionStart) - And several others
The hook command can do anything a shell command can: write to a log file, send a desktop notification, format a file, run a linter, validate an action and block it. Because hooks are shell scripts, they’re deterministic — they can’t hallucinate, they always run, they always produce a definite outcome.
Why it matters
Three reasons:
-
Hard guarantees. “Never let Claude run
rm -rfoutside this folder” is a rule you can’t enforce with prompts — Claude might forget or be tricked. A hook enforces it absolutely. -
Automation glue. “After Claude edits a file, run
prettieron it” — a hook makes this automatic, so you don’t have to remember. -
Observability. Hooks can log every action Claude takes. Useful for review, audit, replay.
For solo dev work, a few well-chosen hooks raise the floor of your Claude Code experience considerably. For team or production use, hooks are how you bake in compliance and safety.
The hook events
The most useful events you can hook into:
| Event | Fires when | Common use |
|---|---|---|
PreToolUse | Just before any tool call runs | Validate or block the action |
PostToolUse | After a tool call completes | Logging, formatting, follow-up |
Stop | When Claude finishes responding | Desktop notification when long task done |
UserPromptSubmit | You hit enter on a message | Pre-processing the prompt |
SessionStart | At the beginning of a session | Setup, environment checks |
SessionEnd | When you close the session | Cleanup, save state |
Notification | When Claude needs your attention | Trigger sounds, push to phone |
A hook can be filtered by matcher — e.g. only fire on Write/Edit tools, only on specific commands, only on certain matchers. This keeps hooks scoped.
How hooks are configured
Hooks live in .claude/settings.json (either global at ~/.claude/settings.json or project-local at <project>/.claude/settings.json).
The shape:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "echo 'about to write a file' >> ~/claude-log.txt" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "notify-send 'Claude is done'" }
]
}
]
}
}Each event maps to an array of matcher-grouped hook lists. The matcher is a regex (for events that have a “tool name” to match against). If omitted, the hook fires for every occurrence of that event.
Blocking actions with PreToolUse
A PreToolUse hook can block the tool call by exiting with a non-zero status code. The tool call is canceled and Claude sees the hook’s stderr output as the reason.
Example: block rm -rf from being run anywhere outside /tmp:
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -E 'rm -rf' | grep -v '/tmp'; then echo 'rm -rf outside /tmp is forbidden' >&2; exit 1; fi"
}
]
}
]
}The hook receives the planned tool call via environment variables (CLAUDE_TOOL_INPUT contains the arguments). If it exits 1, Claude doesn’t run the command, sees the error message, and can react (usually by trying a different approach or asking you).
This is how you build hard guardrails that the model literally cannot bypass.
A handful of useful hook recipes
1. Desktop notification when Claude finishes
Useful for long-running tasks so you can walk away:
{
"Stop": [
{
"hooks": [
{ "type": "command", "command": "powershell -Command \"[System.Console]::Beep(800, 200)\"" }
]
}
]
}On Windows. For macOS, osascript -e 'display notification \"Claude done\" with title \"Claude Code\"'. On Linux, notify-send.
2. Auto-format after edits
{
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true" }
]
}
]
}Runs prettier on any file Claude edits. The || true ensures the hook doesn’t fail if prettier isn’t installed.
3. Log every tool call to a file
{
"PreToolUse": [
{
"hooks": [
{ "type": "command", "command": "echo \"$(date -Iseconds) $CLAUDE_TOOL_NAME\" >> ~/.claude/tool-use.log" }
]
}
]
}Useful for retroactive review of what Claude has done.
4. Block secrets from being written
{
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'sk-[a-zA-Z0-9]{40}|eyJ[A-Za-z0-9_-]{100,}'; then echo 'looks like a secret — blocked' >&2; exit 1; fi" }
]
}
]
}Crude but catches obvious leakage of API keys / tokens into committed files.
5. Run a smoke check before commits
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -q 'git commit'; then npm run typecheck && npm run lint || { echo 'pre-commit checks failed'; exit 1; }; fi" }
]
}
]
}Blocks commits if typecheck or lint fails.
Hooks vs git hooks
Both run shell commands on events; the events are different.
- Git hooks (
.git/hooks/pre-commit, etc.) run when git operations happen — independent of Claude Code. - Claude Code hooks run when Claude operates — independent of git.
A git commit from Claude Code triggers both: a PreToolUse Claude hook on the Bash call, then a pre-commit git hook inside git. They compose; use whichever scope fits.
Git hooks are usually a better fit for “things that must happen regardless of who commits” (CI-style checks). Claude Code hooks are better for “things specific to AI-driven work” (logging Claude’s tool calls, blocking patterns the model is prone to).
How hook output reaches you
- stdout of a hook is appended to Claude’s view of what just happened — Claude sees it as context.
- stderr of a hook is shown to you and (if non-zero exit) to Claude as the blocking reason.
- Exit code 0 means “all good, proceed.”
- Non-zero exit in
PreToolUsemeans “block this action.”
You can also produce structured output (JSON to stdout) that Claude can parse — useful for richer hook behavior.
Performance considerations
Hooks run synchronously. A slow hook slows down Claude. Two implications:
- Heavy hooks (running tests, calling APIs) on
PreToolUsemake every tool call slow. Restrict thematcherso they only run on relevant tools. - For long-running work, fire-and-forget into the background (
&on Unix) — but then you lose the ability to block.
Most hooks should run in under 50ms. If yours takes longer, consider whether PostToolUse is the right event (so it doesn’t block the chain).
Common gotchas
-
Hook scripts are shell scripts. Quoting, escaping, env-var expansion — all the usual shell traps apply. Test the script outside Claude before wiring it as a hook.
-
Cross-platform pain. A hook that uses
notify-sendworks on Linux but not Windows. A hook withpowershellworks on Windows but not Linux. For portable hooks, write a small script that detects the OS and dispatches. -
A non-zero exit in
PreToolUseblocks the action — but only that action. Claude can try something else. The hook doesn’t end the session. -
Matchers are regex. If your matcher accidentally matches everything (e.g.
.*or an unanchored common word), the hook fires on every tool call. Anchor matchers carefully. -
Hooks see what Claude planned to do, not what happened.
PreToolUseshows the intended call.PostToolUseshows the result. Don’t mix them up. -
Don’t put credentials in hook commands. They get logged. Use env vars or files.
-
Loops are possible. A
PostToolUsehook that triggers another tool call can recurse. Be careful with hooks that themselves invoke Claude. -
Hooks run with your shell environment. They see your PATH, your env vars, your aliases (or not, depending on the shell). Test them in the actual environment Claude runs them in.
-
Debugging hooks is hard. Write logs to a file (
echo X >> ~/hook.log) so you can inspect what happened after a session. -
Some hook events are still experimental. Behavior may change. Pin Claude Code versions for production hook reliance.
See also
- Claude Code deep dive 🟩 🟦 — hooks are one of six primitives
- settings.json 🟩 — where hooks are configured
- Slash commands 🟩 — different mechanism, complementary use
- The memory system 🟩 — hooks can be used to auto-write memory
- Plugins 🟩 — plugins can ship hooks