TL;DR
A rule written in CLAUDE.md is a request. A hook is a rule.
One survey of 2,500 public Claude Code repos found hooks in 13.3% of them, so most projects run on requests.
The order that worked in mine: write the promise, add a check that fails, then wire the gate.
Someone opened an issue on my repo two weeks ago that I could not argue with.
The bootstrap message printed a line saying Claude reads your _CLAUDE.md automatically on every session. A contributor named motkoning pointed out that this is true only when three separate pieces of wiring are all in place: a SessionStart hook, an environment variable pointing at the vault, and a session actually started inside that vault. Miss one and the file sits there doing nothing while the message says otherwise.
He was right. That sentence had been in the repo for months. It read like enforcement. It enforced nothing.
That gap is what this post is about. It is not a bug in Claude Code. It is a bug in how most of us write rules for coding agents, mine included, and after four rounds of it I can finally describe the shape.
The failure class nobody names
Open any repo built for Claude Code and you find a CLAUDE.md full of rules. Never commit secrets. Always run the tests before you claim done. Use sentence case in headers.
None of those are rules. They are requests, written in prose, read by a model that decides fresh each time whether to follow them. Most sessions it follows them. The sessions it does not are the ones that teach you the difference, usually in public.
I wrote about what belongs in a CLAUDE.md back in July, and I stand by the file. What I got wrong was the assumption underneath it: that a well-written instruction and an enforced constraint are the same category of thing. They are not even close.
The clean test is one question. If the model ignores this line, does anything stop?
For every rule in my CLAUDE.md, the honest answer was no.
Why CLAUDE.md cannot enforce anything
The reason is structural, not a matter of prompt quality.
CLAUDE.md is context. It gets loaded into the window, competes with everything else in there, survives or does not survive compaction, and then gets interpreted. A long file makes this worse, because the instruction that matters is now sharing space with forty that do not.
Hooks sit somewhere else entirely. A hook is a shell command the runtime fires on a lifecycle event: before a tool call, after a tool call, on session start, on stop, on a file change. It is not read by the model. It runs whether the model wanted it to or not, and its exit code can block the action outright.
That is the whole distinction. Instructions live inside the model’s judgment. Hooks live outside it.
The consensus that formed in the docs and the community over the last few months puts it in three lines: a skill teaches the how, a hook enforces the rule, a subagent isolates the work. Useful, and it is where I would start if you are choosing between them today.
What almost nobody publishes is the cost of learning it the other way round.
The bug three people found four times
Here is the expensive version of the lesson.
My repo has a tokenizer that splits a search query into terms. The original version split on whitespace and dropped anything shorter than three characters. That works in English and destroys Chinese and Japanese, where a two-character word is a normal word.
SylvesterTee filed it first, as issue 159. I fixed it in vault_ops.py and closed the issue.
Then P1uxozor filed it again on a different command. A copy of the same tokenizer lived there too. Fixed, closed.
Then hamidasiblog filed it again, on the abstention gate. Same logic, third copy. Fixed, closed.
Then hamidasiblog filed it a fourth time, as issue 212, on the NotebookLM command. And in that report they suggested a grep, which found a fourth copy I did not know about.
Three different people. Four issues. One root cause, copied by hand into four files over months, each copy quietly aging past the fix.
My CLAUDE.md already said not to duplicate logic. It said it clearly. It said it in a file the model reads every session. Four copies grew anyway, because a sentence in a document has no way to fail.
The fence that made the fifth copy impossible
The fix for issue 212 was not another patch.
I moved the tokenizer into one shared module, pointed every caller at it, and then added a test whose only job is to fail if a copy ever regrows in that directory. Not a warning. Not a note in the contributing guide. A test in the suite that turns red.
I call it a fence. It does not explain the rule to anyone. It makes the rule impossible to break without the build telling you.
The suite went from 591 tests to 607 across that window, and most of those new tests are this shape: not proving a feature works, but making a specific past mistake unrepeatable.
A week later the same principle caught me. I pushed a commit that was green on my machine and red in CI, because a new test imported a module whose top-level import needed an optional package that CI does not install. Local green was not CI green. The gate held and the instruction in my head did not.
When a hook is the wrong answer
The obvious next move is to convert every rule into a hook. I tried a version of that and stopped.
My repo builds for seven agent surfaces, not just Claude Code. One of them is the Codex CLI, and the write-time validation hook that guards note quality only works where the host actually fires hooks. Claude Code does. Codex, at the time I looked, does not.
The tempting fix was to ship the hooks directory into the Codex build anyway so the layout matched. I decided against it, and the reason is the entire point of this post: a hook directory with no host wiring reads as enforcement and enforces nothing. That is the exact bug I had spent two weeks closing. Shipping it as a fix would have been the same lie with better file structure.
So instead the docs got scoped. The guide now says which platform the hook ships on, and the Codex install page has a section titled with what is not included. Honest documentation is worse than a working gate and much better than a fake one.
Same logic applies to smaller cases. Hooks that run on every tool call add latency to every tool call. Hooks holding flexible business logic get brittle, and hidden constraints confuse the next person. A rule that is genuinely a preference belongs in prose. Only the non-negotiable ones earn a gate.
The order that works
If I were setting this up again on a fresh repo, this is the sequence.
Write the rule in prose first. You need to know what you mean before you can enforce it, and most rules die at this step because they turn out to be preferences.
Then write the check that catches the violation. A test, a lint, a script that exits non-zero. Run it against a real past failure and confirm it actually goes red, because a check that cannot fail is another promise.
Then wire it to an event so nobody has to remember to run it. PreToolUse to block, PostToolUse to validate, Stop to gate the finish, a file-change event for the fast stuff.
Prose, check, gate. In that order, and only for the rules where a miss actually costs something.
The measured picture says most projects have not done step three. That survey of 2,500 public repos found hooks in 13.3% of them, and the GitHub topic for Claude Code hooks lists a couple hundred repos total. Interest is loud and adoption is thin, which is a strange place for a feature this cheap to install.
My honest read after four rounds of the same bug: I did not have a discipline problem. I had a wiring problem, and I kept trying to solve it by writing better sentences. The second-brain setup I run on Claude Code works now for one boring reason. The parts that must not break are gates, and everything else is allowed to be advice. All of it is in the open, at github.com/eugeniughelbur/obsidian-second-brain, issues included.
Frequently asked questions
What are Claude Code hooks?
Hooks are shell commands the Claude Code runtime fires on lifecycle events such as before a tool call, after a tool call, session start, stop, or a file change. They run outside the model, so they execute whether or not the model decided to follow an instruction.
What is the difference between a hook and a CLAUDE.md rule?
A CLAUDE.md rule is context the model reads and may or may not follow. A hook is code the runtime executes, and its exit code can block the action. Instructions rely on judgment, hooks do not.
When should I use a hook instead of a skill or a subagent?
Use a hook when something must always happen, a skill when you want a reusable procedure the agent pulls in when relevant, and a subagent when the work needs its own isolated context.
Do Claude Code hooks slow things down?
A hook wired to a frequent event runs on every one of those events, so a slow hook on PreToolUse is felt on every tool call. Keep frequent hooks cheap and push heavier checks onto Stop or a file-change event.
Can I just put everything in CLAUDE.md instead?
You can, and most repos do. It works until the session where the model deprioritizes your line, and you find out that nothing was ever checking. Keep preferences in prose, and move the non-negotiables to gates.
Why did the same bug get reported four times in one repo?
Because the fix landed in one file while three hand-made copies of the same logic lived elsewhere, and a written no-duplication rule cannot fail. A shared module plus a test that goes red on a new copy fixed it structurally.
Key takeaways
A rule in
CLAUDE.mdis a request read by a model. A hook is code the runtime runs regardless.The test for any agent rule you have written: if the model ignores this line, does anything stop?
One survey of 2,500 public Claude Code repos found hooks in 13.3% of them, so most projects are running on requests.
The same root bug was reported four times by three different people because a written no-duplication rule has no way to fail.
A test that goes red when a past mistake reappears is worth more than the paragraph explaining the mistake.
A hook directory with no host wiring reads as enforcement and enforces nothing, which is worse than honest documentation.
Further reading
Claude Code hooks reference - the full event surface, exit-code semantics, and hook types.
Automate actions with hooks - the official guide to deterministic control over formatting, validation, and project rules.
Issue 212 in obsidian-second-brain - the fourth report of the same tokenizer bug, and the grep that found the copy I missed.
The CLAUDE.md behind a tool 3,000 developers use - what belongs in the file, and what does not.
Claude Code as a second brain - the setup these gates protect.
About the author
Eugeniu Ghelbur builds production AI agent systems and the tooling around them. He maintains obsidian-second-brain, an open-source Claude Code skill that runs an Obsidian vault as a living knowledge system, starred by over 4,000 developers on GitHub and MIT licensed. He writes The AI Operator, where this post first appeared.








