There's a category error hiding in most agent configs: treating a rules file as an enforcement mechanism. It isn't one. CLAUDE.md, AGENTS.md, .cursor/rules — these are advisory context. The model reads them, weighs them against everything else in its window, and usually complies. Usually.
The failure mode isn't dramatic. It's a 40-turn session where the instruction "never commit directly to main" is now 30,000 tokens behind the current task, and the model — trying to be helpful — commits directly to main. Nobody disobeyed you. Your reminder just lost an attention contest.
Claude Code has a mechanism for the rules that must not lose that contest: hooks. A hook is a shell command that runs at a fixed point in the agent's lifecycle. It doesn't ask the model to remember anything, because it doesn't run inside the model at all.
This post covers the mechanics that matter, five hooks that earn their keep, and the pitfalls I hit shipping a set of them.
A rule in CLAUDE.md is a reminder. A hook is a gate.
If violating a rule would make you revert a commit or rotate a credential, it should be a hook, not a sentence.
Hooks are configured in your settings file (.claude/settings.json
for the project, or ~/.claude/settings.json
globally) under a hooks
key. Each entry names a lifecycle event and a matcher for which tools it applies to.
The events you'll use most:
Three mechanical details do most of the work:
Blocked: direct commit to main. Create a branch first.
course-corrects the very next action. Exit 0 lets the call proceed through the normal permission flow.One more property worth internalizing: hooks run with your user permissions, outside any model-mediated safety layer. That cuts both ways — it's why they're reliable, and why you should read any hook you didn't write before installing it.
These are deliberately small. A hook should be readable in one screen — it's a security control, and unreadable security controls don't get reviewed.
1. The main-branch guard (PreToolUse on Bash). Parse the incoming command; if it's a git commit
and git branch --show-current
says main
or master
, exit 2 with "create a branch first" on stderr. This single hook has stopped more real accidents for me than the other four combined.
2. The secret scan (PreToolUse on Bash, matching git push and git commit). Grep the staged diff for key-shaped strings (
AKIA…
, -----BEGIN
, api_key=
, base64 blobs above a length threshold) before letting the command run. False positives are annoying for a day; a pushed credential is annoying for a week.3. The test gate (PreToolUse on Bash). If the command is a git commit
, run your test suite first; block with the failure summary on stderr if it's red. Two warnings: mind the timeout (command hooks default to a generous 10 minutes, but a very slow suite can still hit it — set the hook's timeout
to match your suite), and make the gate skippable by a human (SKIP_TEST_GATE=1
) so an emergency fix is never held hostage by an unrelated flaky test.
4. Format-on-write (PostToolUse on Edit/Write). After any file edit, run your formatter on the touched file. This is enforcement of a different kind: it removes an entire category of "fix the formatting" turns from your sessions. Cheap, silent, no false positives.
5. The out-of-repo write fence (PreToolUse on Bash). Block rm
, mv
, and redirects that resolve outside the repository root. Agents rarely try this — but "rarely" multiplied by "hundreds of sessions" is why fences exist.
BSD vs. GNU tooling. If your hook uses grep -P
, it works on Linux and fails on a stock Mac (BSD grep has no -P
). We shipped a hook set with exactly this bug; the fix is to write POSIX-compatible patterns or detect the platform. Test hooks on the OS your team actually uses.
Overblocking teaches routing-around. If your gate is too aggressive, the model starts composing commands that evade the matcher — not maliciously, just optimizing for progress. When a block fires, the stderr message should always name a legitimate next step, so the path of least resistance stays inside your rules.
Log before you loosen. Run a new hook in log-only mode (or just review its blocks) for a week before weakening it. In my logs, about half the "annoying" blocks turned out to be the hook doing exactly its job on a command I didn't mean to run.
Don't move judgment into hooks. A hook that tries to evaluate "is this diff good?" will be wrong constantly and erode trust in the gates that matter. Hooks enforce invariants; the rules file and review handle judgment. Keep the boundary clean.
Start with the main-branch guard — it's ten lines, it fires rarely, and the first time it fires you'll be glad it exists. Add the secret scan the same day. Everything after that can wait until a real incident tells you which invariant you were relying on politely.
If you'd rather fork than write: the Claude Code Hooks Pack ships 13 production-ready hooks — the secret scan, test gate, and main-branch guard above included, each with a Reason line and platform notes.