A prompt is not an invariant
A rule you write into an agent prompt or a CLAUDE.md is advisory: the model reads it on every path but only weighs it, and weighing is not refusing, so it holds most of the time, and most of the time is not what a load-bearing rule is for. To make one actually hold it has to move to where compliance is not optional, a check the harness runs and enforces on its own, regardless of what the model decided. With a limit: a gate binds only over the paths it covers, and some rules cannot be settled at any gate at all, because their violation shows up in the world and not in the action it would inspect.
Every agent I run is defined mostly by rules written in prose, and those rules are not all the same kind of thing, though they sit in the same file in the same voice. Some are taste: prefer the smaller dependency, keep a function short enough to read at once, name things so nobody has to ask. Some are load-bearing: never run a destructive delete outside the sandbox, never push straight to main, never print a secret to a log. The first kind can slip and cost a little polish. The second kind slipping is the incident. And the day I started moving the second kind out of the files and into something the harness runs no matter what the model decides, I saw what the files had been doing all along, which was less than I had assumed. They had been stating the rules. They had never been enforcing them.
The mistake is easy to make, because writing "always" or "never" into a prompt feels like setting a constraint. It reads like one and it is shaped like one. But the file states an intent to a reader that complies by disposition, and disposition is a probability, not a promise. The model grants the rule most of the time, and most of the time is a different property from every time. For taste that is fine. For a rule whose entire job is to hold on the one occasion it is tested, most of the time is not a weaker guarantee. It is the absence of one.
What makes an enforcer an enforcer
Borrow the idea from the part of engineering that already has it. A real constraint does two things a sentence cannot. It binds: reaching the forbidden state is not an available move, not a discouraged one. And it covers: it sits on every path to that state, so nothing arrives there around it. A type binds and covers, the compiler applies it to every assignment and rejects the wrong value. A database constraint does both as well, since every write in the system passes through it. An assertion binds but hardly covers, it refuses at the line where you put it and sees nothing on the paths that never reach that line.
A rule in a prompt has the opposite profile, and it is the worse one. It covers everything, because the model reads it on every path and there is no route where the rule is simply absent. What it does not do is bind. It is read and weighed against everything else in the window, and weighing is not refusing. A rule that enough surrounding context can argue out of the way was never binding; it was a strong preference with good statistics. The coverage was never the problem. The problem is that under the coverage there is nothing that says no.
The same rule as a sentence and as a gate
Take never push straight to main and put it in two places. In a CLAUDE.md it is a line the model reads and usually keeps. Give it a plausible story, this push is the exception, the fix is urgent, the rule is the only thing in the way, and it runs the command, not from any wish to defy you but because you handed it a sentence and it weighed the sentence.
Now put the rule where the harness runs it. Claude Code's hooks are the near example, and the word that carries the weight is in the definition:
Hooks are user-defined shell commands, HTTP endpoints, MCP tool
calls, LLM prompts, or subagents that execute automatically at
specific points in Claude Code's lifecycle.
Automatically. The model does not choose to consult the hook and cannot choose to skip it; the harness runs it because the event fired. That is the binding the file was missing, and it arrives from outside the model rather than from inside its reading. The refusal, once the hook makes it, is also not a vote that a louder one can outnumber:
Exit 2 means a blocking error. On events that can block, exit 2
blocks whether or not you print JSON: even a JSON permissionDecision
of "allow" can't override it.
Read narrowly, that clause settles a precedence inside the hook, its exit code against its own printed verdict, and the exit code wins. Narrow is enough. The block is the outcome, not a preference with a number attached, and the thing that produced it ran whether or not the model wanted it to. Same rule as the line in the CLAUDE.md, a different kind of object: one is read and weighed by the thing that then acts, the other runs beside that thing and has already decided.
Not every rule wants to be a gate
None of this is an argument for gating everything. Most rules should stay sentences. Taste has no gate: nothing mechanical decides whether a name is good or an abstraction earns its keep, and although the definition's own list of hook types includes an LLM prompt, so you could hang a model in the gate to judge it, that only moves the weighing indoors and calls it enforcement. The rule worth promoting is the narrow one, found by a single question: which of your instructions is such that one silent violation is an incident and not a rough edge? Those are the ones to move. The rest are guidance, and guidance belongs in prose, read and weighed, which is the thing prose is good at.
The failure I watch for is the other direction. It is a load-bearing rule sitting in a file, wearing the calm authority of a constraint while behaving like a suggestion, and the authority misleads no one so much as the person who wrote it and reads it back as handled.
What a gate cannot reach
Even the rules you do move hold less tightly than the block makes it look, because a hook covers only the path it is placed on, and one path is not every path. Block the push on the shell tool and an agent under the same pressure does not argue with the block; it reaches main by another road, a different tool, an API call, a script that runs the push for it. The matcher that picks the road is best-effort by the documentation's own admission, so even the road it watches it watches imperfectly. A check binds only over the paths it covers, which is the earlier point again at the level of the action: to hold a rule you have to sit on every route to the effect, and past a point the way to do that is to stop inspecting commands and take the reach away, so there is no road left to guard.
Underneath that is a harder limit. Some rules cannot be settled at any checkpoint on any road, because the violation is not in the call, it is in the consequences the call does not carry. A migration that is valid SQL and drops a column a downstream job still reads clears every check at the gate. A retry change that parses cleanly and quietly drops the backoff clears them too, and shows the damage only later, under load, in production. Nothing at the gate sees the wrong in either, because the wrong is not written in the action. Those rules cannot be moved at all, and a sentence in a file pretending to hold them is the first mistake again, in the form that has no repair. What holds them is a person reading the change, which is the reason the earlier cut matters: keep gating narrow, promote only the load-bearing few, and the queue a person actually has to read stays short enough to mean something, instead of decaying into the reflex approval a gate of a thousand items always becomes.
Two verbs the one sentence hides
This is not the rule drifting out of sync, the problem of a declared state and a running state pulling apart that reconciliation exists to close; a rule can be perfectly reconciled, running exactly as written, and still only advisory. It is not the question of which layer the rule belongs to or who is allowed to write it; addressing settles whose word a rule is, and the operator's word still only outranks the repository's as a weighting until something stands behind it. And it sits upstream of the two essays that work out what standing behind it looks like: that a decidable seam lets you refuse a whole class while a semantic one forces you to gate the capability instead, and that an unattended action belongs on the reach the harness granted, not a guess about what the command will do. Those are about the action. This is about the file, where the guarantee is assumed to already exist because the sentence is written in the imperative.
The sentence and the constraint read alike, an instruction either way, and the likeness is what lets the first stand in for the second until something tests it. The rule I keep for myself now is smaller than the ones I used to write: before I put a load-bearing never into a file, I ask whether I have stated it or backed it, and when I cannot back it I say so in the same breath instead of letting the imperative pretend. The rules I could never back, the ones whose damage lives in the world and not in the call, turned out to be exactly the ones I most wanted the file to hold, and it never could. A file is a place to say what should be true. It was never the place that makes it so.