Last updated: April 2026
Claude Code Hooks8 recipes that save you from yourself
Hooks are shell commands Claude Code runs automatically at set points in a session — before a tool call, after an edit, when a turn ends. They do not negotiate. They run. That makes them the right place to enforce rules you do not want Claude (or a sleepy version of you) to decide about: blocking destructive commands, formatting on save, running targeted tests, protecting generated files.
Every recipe below is copy-paste ready. Drop it into ~/.claude/settings.json or your project's .claude/settings.json and restart Claude Code.
How hooks actually work
A hook is an entry in settings.json that binds a shell command to a Claude Code event. When the event fires, Claude Code executes your command with the event's payload piped to stdin as JSON. Your command can inspect that payload, do work, and exit.
Exit codes determine what happens next. Exit 0 means "continue as normal." Exit 2 on a PreToolUse hook blocks the tool call and feeds your stderr back to Claude as guidance. Any other non-zero exit is surfaced to you but does not block the run.
The events you will use most:
PreToolUse— before Claude runs a tool (Bash, Edit, Write). This is where guardrails live.PostToolUse— after a tool succeeds. This is where formatters, linters, and targeted tests live.UserPromptSubmit— when you send a new prompt. Useful for logging and for rewriting your input.Notification— when Claude needs your attention (permissions, long task done). Good for desktop notifications.
You can scope most hooks with a matcher regex so they only fire for specific tools (for example, Write|Edit to skip Bash calls). All the recipes below use that.
The recipes
Every one of these is running on at least one of my projects. Start with one or two that solve an actual pain point — adding them all at once is the fastest way to slow Claude Code to a crawl.
Auto-format files after Claude writes them
Claude occasionally ignores your formatter rules mid-flow. This closes the loop automatically — every saved file gets formatted before Claude (or you) sees the next state. No drift, no lint PRs.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -I{} npx prettier --write {}"
}
]
}
]
}
}Block destructive bash commands
Catch rm -rf, force-pushes to main, and DROP TABLE before Claude runs them. Exit 2 blocks the call and tells Claude why, so it can try a safer approach on the next turn.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "cmd=$(jq -r '.tool_input.command'); if echo \"$cmd\" | grep -qE 'rm -rf /|git push --force.*main|DROP TABLE'; then echo 'Blocked destructive command' >&2; exit 2; fi"
}
]
}
]
}
}Run tests for the file Claude just edited
Instead of waiting until the end of a session to find out a change broke something, run the targeted test file the moment Claude saves. Fast feedback, no full-suite cost.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "file=$(jq -r '.tool_input.file_path'); test=\"${file%.js}.test.js\"; if [ -f \"$test\" ]; then npx jest \"$test\" --silent || exit 2; fi"
}
]
}
]
}
}Protect generated and read-only paths
Generated code (protobuf output, prisma client, build artefacts) should never be edited by hand. This hook refuses writes to those paths and tells Claude to regenerate instead.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "path=$(jq -r '.tool_input.file_path'); if echo \"$path\" | grep -qE '/(dist|build|generated|\\.next)/'; then echo 'Do not hand-edit generated code. Regenerate via the appropriate build command instead.' >&2; exit 2; fi"
}
]
}
]
}
}Log every prompt you send
Keep a running log of what you asked Claude to do, timestamped. Useful for post-mortems, for writing up what you shipped at the end of the day, or for spotting prompts you could turn into slash commands.
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "mkdir -p ~/.claude/logs && jq -r '\"[\\(.timestamp // now | todate)] \\(.prompt)\"' >> ~/.claude/logs/prompts.log"
}
]
}
]
}
}Desktop notification when Claude needs you
When Claude is waiting on a permission prompt or a long task is done, you want to know without staring at the terminal. This triggers a macOS notification; swap for notify-send on Linux.
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "msg=$(jq -r '.message // \"Claude Code needs attention\"'); osascript -e \"display notification \\\"$msg\\\" with title \\\"Claude Code\\\"\""
}
]
}
]
}
}Type-check only the files Claude touched
Full tsc --noEmit on a big repo takes 30+ seconds. Scoping it to the file Claude just changed keeps the feedback loop tight while still catching type errors before you ask Claude to commit.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "file=$(jq -r '.tool_input.file_path'); case \"$file\" in *.ts|*.tsx) npx tsc --noEmit --skipLibCheck \"$file\" 2>&1 || exit 2 ;; esac"
}
]
}
]
}
}Block commits to protected branches
Never let Claude (or a distracted you) commit straight to main. This hook checks the current branch before any git commit and refuses if it is on a protected branch.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "cmd=$(jq -r '.tool_input.command'); if echo \"$cmd\" | grep -qE '^git commit'; then branch=$(git rev-parse --abbrev-ref HEAD); case \"$branch\" in main|master|production) echo \"Refusing to commit directly to $branch. Create a branch first.\" >&2; exit 2 ;; esac; fi"
}
]
}
]
}
}A few hard-won lessons
Scope your slow hooks
A hook that runs the whole test suite on every save will kill your flow. Target the specific file Claude just changed, or move the slow check to a git pre-push hook instead.
Write stderr messages Claude can act on
When you exit 2 to block something, the stderr goes back to Claude. "Blocked" is useless. "Do not edit generated code in /dist; regenerate via npm run build" tells Claude what to do next.
Prefer tools that are already on PATH
Hooks that depend on a specific developer's setup (a custom alias, a homebrew-only tool, a personal script) will break for teammates. Stick to tools checked into the repo or installed via the project's package manager.
Commit project hooks. Keep personal hooks personal.
Team guardrails belong in .claude/settings.json so everyone gets them. Personal niceties (desktop notifications, prompt logging) belong in ~/.claude/settings.json so you do not inflict them on teammates.
Frequently Asked Questions
Questions that keep coming up when people first set up hooks.
What are hooks in Claude Code?+
Hooks are shell commands that Claude Code runs automatically at specific points in a session — for example, every time Claude writes to a file, before it runs a bash command, or when you submit a prompt. They are configured in your settings.json under the hooks key. Claude does not choose whether to run them; the runtime does. That makes them useful for enforcing rules that you never want Claude to 'decide' about.
Where do I configure hooks?+
Hooks live in settings.json. User-level hooks at ~/.claude/settings.json apply to every project. Project-level hooks at .claude/settings.json apply only to that repo and can be checked into git so the whole team gets them. Claude Code merges both; project hooks run in addition to user hooks.
What events can I hook into?+
The main events are PreToolUse (before Claude runs a tool like Bash or Edit), PostToolUse (after a tool succeeds), UserPromptSubmit (when you send a new message), Notification (when Claude needs your attention), and Stop/SubagentStop (when a turn ends). Each event gets the relevant context on stdin as JSON.
Can a hook block Claude from doing something?+
Yes. A PreToolUse hook that exits with code 2 blocks the tool call and feeds its stderr back to Claude as guidance. That is how you stop Claude from running rm -rf, committing to main, or writing to files that should be generated. Exit 0 allows the call. Any other exit code is shown to the user but does not block.
Do hooks slow Claude down?+
A little. Every PreToolUse hook runs before Claude's tool call and every PostToolUse hook runs after. For fast commands (formatters, linters on a single file) the overhead is negligible. For slow commands (full test suites, type-checking the whole repo) it adds up, so scope the work — for example, only lint the file that was just edited.
Can hooks read what Claude is about to do?+
Yes. Claude Code passes the full tool call payload on stdin as JSON, so your hook can read the file path, the bash command, or the edit diff before deciding to allow or block. Most of the recipes below are just jq one-liners against that payload.
Where do hooks fit compared to slash commands and MCP?+
Hooks run automatically without Claude's input and can block actions. Slash commands are prompts you invoke manually — they run when you type /name. MCP servers expose new tools Claude can call. Use hooks for guardrails and automation. Use slash commands for reusable workflows. Use MCP to give Claude capabilities it does not have built-in.
Are hooks safe to share in a team?+
Yes — if you keep them simple and read-only where possible. Commit project-level hooks to .claude/settings.json so every teammate gets the same guardrails. Avoid hooks that depend on a specific developer's shell setup or absolute paths; use tools that are already on the project's PATH (eslint, pytest, gofmt, ruff) so they work the same on everyone's machine.
Hooks are the middle layer. What connects to them?
Hooks, slash commands, MCP servers, CLAUDE.md — it is easy to confuse what each one does and when to reach for it. The decision-tree guide maps them against real tasks so you know which layer to extend next.