DEV Community

Rulestack
Rulestack

Posted on

Your CLAUDE.md says "always run tests" — hooks are how you actually mean it

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.

The mental model: reminders vs. gates

A rule in CLAUDE.md is a reminder. A hook is a gate.

  • A reminder's reliability decays with context length. A gate's reliability is constant — it's just a process exit code.
  • A reminder shapes what the model intends. A gate constrains what the model does.
  • You want both: rules for judgment calls ("prefer small diffs"), hooks for invariants ("no secrets in commits").

If violating a rule would make you revert a commit or rotate a credential, it should be a hook, not a sentence.

The mechanics that matter

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:

  • PreToolUse — fires before a tool call executes. This is where you block a tool call before it happens.
  • PostToolUse — fires after the tool call completes. You can't undo the action, but you can report on it and feed the result back to Claude.
  • UserPromptSubmit, SessionStart, Stop and friends cover the rest of the lifecycle. Stop and UserPromptSubmit can block too ("don't stop until tests pass" is a real pattern) — but PreToolUse is where most enforcement lives.

Three mechanical details do most of the work:

  1. Your hook receives JSON on stdin. For a PreToolUse hook on Bash, that JSON includes the exact command about to run. You parse it (jq is fine), decide, and exit.
  2. Exit code 2 blocks the call — and your stderr becomes feedback to the model. This is the detail that makes hooks better than an external CI check: the agent doesn't just fail, it reads why. A stderr message like 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.
  3. For finer control, emit JSON on stdout — the structured output can explicitly allow, deny, or escalate to an "ask the human" decision, and carries a reason. Plain exit codes cover 90% of cases; reach for JSON when you need the third option.

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.

Five hooks that earn their keep

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.

The pitfalls (learned the slow way)

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.

Where to start

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.

Top comments (0)