There is a moment in every Claude Code setup where you discover hooks, get excited, configure six of them in an afternoon, and then quietly turn most of them off two weeks later because they fire too often, fire at the wrong time, or fire perfectly but for a workflow you no longer have.
That has been my exact arc.
I have been running Claude Code for nine months now and I have configured eleven hooks across my projects in that time. Four of them are still active. Seven got deleted, some within a week, some after limping along for a month before I admitted they were not earning their keep. This is the honest accounting of what each one actually did, why I kept or killed it, and what I would tell someone starting from zero.
A quick note on what we are even talking about. Hooks in Claude Code are shell commands or scripts that fire automatically at specific points in the session lifecycle. The configuration lives in settings.json, either at the project level in .claude/settings.json (shared with the team, checked into git) or at the user level in ~/.claude/settings.json (your personal preferences, not in git). As of the 2026 updates there are 21 lifecycle events you can hook into, things like PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, Stop, Notification, plus newer ones like WorktreeCreate and PostCompact. You can attach a hook to any of them with a matcher and a handler.
That is the surface area. The reality is much smaller. Most workflows need two or three hooks. The rest is people configuring features instead of solving problems.
Here is what stuck for me, and what did not.
the four that stuck ..
The first one I kept is a PostToolUse hook that runs my project formatter after Claude edits a TypeScript or Python file. Boring, useful, fires twenty times a day, costs me nothing. Before this I would notice Claude leaving inconsistent indentation or trailing commas in files, run the formatter myself, and feel mildly annoyed. Now I never see it. The hook is six lines in settings.json and it just works. This is the platonic ideal of a hook. It does one boring thing reliably and you forget it exists.
The second is a SessionStart hook that drops a one line context summary into the prompt, basically "you are working in ~/Desktop/HQ/<project>, the dev server runs on port X, do not restart it without asking". Sounds redundant because all of that is already in my CLAUDE.md file but Claude in a fresh session sometimes ignores things in long markdown files when they conflict with what the user just said. Having the most critical facts re injected at the top of every prompt has prevented at least three "wait why did you kill the server" moments. Worth the eight lines.
The third one is a Stop hook that sends me a desktop notification when Claude finishes a long running task. I am usually doing something else when Claude is grinding through a build or a test suite. The notification means I do not have to keep tabbing back to check. Genuinely small quality of life win, but I have noticed it adds up across a day. The actual implementation is one line, it calls terminal-notifier on Mac with the project name.
The fourth, and this is the one I am most attached to, is a PreToolUse hook on Bash that blocks any command matching a list of patterns I have curated over months. Things like rm -rf, git push --force, DROP TABLE, anything touching ~/Downloads/, anything touching the private/ folder. It is a tiny shell script that exits 1 if it sees a banned pattern, and Claude treats that as a tool rejection and asks me before retrying. Twice this has caught a git reset --hard that would have lost half a day of work. Once it caught a find / -name something that would have hung my machine for ten minutes. Worth its weight in gold, costs nothing.
Notice the pattern across all four. Each one solves a friction I actually had. Each one is short. Each one fires often enough that I trust it but not so often that it becomes noise. None of them required understanding all 21 lifecycle events. I only ever touched four.
the seven that died ..
Now the graveyard.
The first to die was a UserPromptSubmit hook that tried to inject the current git status into every prompt. The idea was that Claude would always know whether I had uncommitted changes. In practice this added 200 tokens to every single message I sent, slowed down responses noticeably, and contributed nothing because Claude could just run git status itself when it needed to. Killed it after four days. Lesson: do not inject context Claude can fetch on demand.
The second was an over engineered PostToolUse hook that tried to run my entire test suite after any code edit. Sounded great on paper. In practice my test suite takes ninety seconds, fired between every edit, and turned a five minute back and forth into a thirty minute one. I had built a tool that made my workflow worse. Killed it after a week, replaced it with a manual npm test at the end of a feature.
The third was a Notification hook that messaged me on Telegram whenever Claude needed approval for a tool. This sounds useful and was for the first two days. Then I realized I was getting fifteen telegram pings per work session, which trained me to ignore the bot entirely, which meant when something actually important came through Telegram I missed it. Hooks should not destroy your existing notification habits. Killed.
The fourth was a PreToolUse hook on Write that tried to enforce a coding style by failing if the file did not have a top of file comment. This is the kind of hook that sounds smart and is actually petty. Claude would write a perfectly good file, the hook would reject it, Claude would add a useless comment to pass the hook, and I would have added nothing of value to the codebase. Killed after three days. Lesson: dont use hooks to enforce things your linter or formatter should be doing.
The fifth was a SessionStart hook that read my entire project README and injected it into the prompt. Six thousand tokens, every session, mostly stuff already in the codebase. Killed after a week.
The sixth was a complicated PostCompact hook that tried to write a session summary to a markdown file every time context got compacted. The intent was to have a paper trail. The reality is I never once went back and read those summaries, and they were eating disk space and adding latency. Killed after two weeks.
The seventh was the most embarrassing one. I built a Stop hook that ran a small Python script to grade the quality of what Claude had produced, give it a score from 1 to 10, and log it. I told myself this was for "tracking quality over time". It was actually for ego. Killed after about a month when I admitted to myself I had not once acted on the scores.
The pattern in the dead pile is the inverse of the pattern in the live pile. Each killed hook either fired too often, did something Claude could already do itself, enforced style things a linter should enforce, replaced an existing healthy habit with a broken one, or was secretly built for theater rather than utility.
what to actually do if you are starting ..
Dont configure six hooks on day one. Pick one friction you actually have. Write the hook for it. Use it for a week. If it is still earning its keep at the end of the week, leave it alone. If you are starting to ignore it, kill it. Then do the same for the next friction.
The bias to watch for is the same bias that bites you with skills, MCP servers, and CLAUDE.md sections. You will be tempted to configure things because they are configurable, not because you need them. Hooks especially have this trap because they are easy to add and they make you feel like a power user. Most of the people writing "I have configured 15 hooks" blog posts have configured 15 hooks and used 3 of them seriously. There is no shame in two hooks. There is no virtue in twelve.
If I had to give one specific recommendation it would be: start with the PreToolUse Bash blocker for destructive commands. This one alone earns the entire concept of hooks. Run Claude for long enough and it will eventually try to rm -rf something you care about. A twelve line shell script that checks for patterns and exits 1 is the cheapest insurance policy you can buy. Mine looks roughly like this:
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
banned="rm -rf|git reset --hard|git push --force|DROP TABLE|truncate|~/Downloads|/private/"
if echo "$cmd" | grep -qE "$banned"; then
echo '{"decision":"block","reason":"matched banned pattern"}'
exit 0
fiDrop that into ~/.claude/hooks/bash-guard.sh, make it executable, wire it up in your settings.json under the PreToolUse event with a Bash matcher, and you have just bought yourself peace of mind. Nine months in this is the only hook I genuinely recommend to everyone.
The rest of it depends on what your day actually looks like.
the meta lesson ..
Hooks are not features. They are a way to encode something you have already learned about your own workflow. Which means the people who are best at hooks are not the ones who read the most documentation. They are the ones who have used Claude Code long enough to know what their actual frictions are. If you dont have a friction yet, you dont need a hook yet.
Configure less. Notice more. The right hooks will reveal themselves.