I almost pushed an API key to a public repo last Tuesday. A Claude Code hook caught it half a second before the commit. That's the moment I went from "hooks are a nice-to-have" to "I will not run an agent without them."
Claude Code hooks are the most underrated feature in the whole tool, and almost nobody on my timeline uses them. Let me fix that. I'll show you the exact hooks I run shipping solo, the config that powers them, and the one that genuinely saved my repo.
What a hook actually is
A hook is a shell command Claude Code runs automatically when something happens. Before a tool runs. After a file gets edited. When the agent finishes. You're not asking Claude to remember to do something — the harness does it, deterministically, every time. That distinction matters. An agent that "usually remembers" to run the formatter is an agent that will forget at the worst possible moment.
Hooks live in your settings.json. Here's the shape:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "prettier --write \"$CLAUDE_FILE_PATHS\"" }
]
}
]
}
}
That one runs Prettier on any file Claude edits, the instant it edits it. I never think about formatting anymore. The diff is always clean. No more "oops, 400 whitespace changes" polluting my commits.
The hook that saved my repo
This is the API-key one. A PreToolUse hook on Bash that scans for a git commit or git push and greps the staged diff for anything that looks like a secret.
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/scripts/secret-scan.sh" }
]
}
]
}
The script greps staged changes for patterns like sk-, AKIA, and a few of my own provider prefixes. If it hits, the script exits non-zero, and a non-zero exit from a PreToolUse hook blocks the action. Claude literally cannot run the command. It gets told why and routes around it.
Last Tuesday it hit. I'd let an agent refactor a config and it inlined a key from an env file it shouldn't have read. Without the hook, that key is on GitHub, I'm rotating credentials at midnight, and maybe somebody's mining crypto on my dime. With the hook, I got a one-line block message and moved on. Twelve lines of bash. Cheapest insurance I own.
My everyday hooks
Beyond the two above, here's what actually earns its place.
A PostToolUse hook that runs my type checker after edits to .ts files and feeds errors straight back to the agent. Claude sees its own type errors without me pasting them. It self-corrects in the same turn. That feedback loop alone is worth the setup. The agent fixes the mistake before I even read it.
A Stop hook that fires a desktop notification when Claude finishes a long task. I run agents on big refactors and go make lunch. The notification means I'm not babysitting a terminal, glancing at it every ninety seconds like a tamagotchi.
{
"Stop": [
{ "hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Claude is done\" with title \"CC\"'" }] }
]
}
Tiny. Changes how I work entirely. I kick off a task and walk away with actual confidence I'll get pinged.
The one I removed
I tried a hook that auto-ran my full test suite after every edit. Sounded responsible. It was miserable. The suite takes 40 seconds, so every tiny edit stalled for 40 seconds, and the agent sat there idle burning my afternoon. Lesson: hooks should be fast or async. A slow synchronous hook turns your snappy agent into molasses.
Now tests run only on a Stop hook, once, at the end. Fast checks (lint, types, secret scan) run inline. Slow checks run at the boundary. Match the hook's cost to how often it fires. If it fires on every edit, it had better return in under a second.
Where to put them
Project-specific hooks go in .claude/settings.json in the repo so they ship with the code and my collaborators get them too. Personal ones, like the desktop notification, go in ~/.claude/settings.json so they follow me across every project. Keep the secret scanner global. You want that everywhere, no exceptions, no "I'll add it later."
One gotcha that cost me an hour: hook commands run in a non-interactive shell. If your command relies on something from your .zshrc, it won't be there. I learned this when prettier worked in my terminal and silently no-oped in the hook because the PATH was different. Use absolute paths or npx.
Start with one
Don't build all of these today. Add the secret scanner. Just that one. It's twelve lines and it's the difference between a calm Tuesday and a credential-rotation fire drill.
Then, once you trust it, add the formatter. Then the notification. Within a week you'll have an agent that formats clean, blocks secrets, fixes its own type errors, and taps you on the shoulder when it's done — and you'll have stopped doing all four of those things by hand.
That's the hour a day. It was never one big task. It was forty small ones, and now a shell script does them while I'm getting coffee.
