You write the rule down where the agent will read it: AGENTS.md, CLAUDE.md, a standing instruction near the start of the session. Most days it holds. Then one day the agent does the forbidden thing anyway, and when you go looking for what broke, the rule is intact. It never had the power to stop anything.
Three surfaces landed on the same conclusion this past month. Claude Code’s permissions doc states the split as policy: permission rules “are enforced by Claude Code, not by the model,” while instructions in the prompt or CLAUDE.md “shape what Claude tries to do” without changing what Claude Code allows. The illustration arrived this week as #101241: a session grant (“restart doesn’t need to ask me”) got stretched to docker rm on a crashed vLLM container that held the only copy of the crash log, and the user’s remedy afterward was to convert the grant into deny rules and auto-mode hard_deny rules, the layer that actually refuses. Geoffrey Huntley, quoted by Simon Willison today, made the same move from the other side: AGENTS.md guidance drifts against code, so the rule went into a pre-commit hook that reads the diff for calls bypassing his diagnostics module, fails the commit, and injects a prompt pointing back at the library. “All of a sudden it’s ten out of ten.” deslop packages the idea as lint rules: nested type assertions “fabricate evidence,” and ad-hoc typeof narrowing fails the check instead of waiting for a human to notice. I haven’t built such a hook or run deslop; each instance is its owner’s report, read against its live source this week.
What the three share is who reads the rule, and when. Prose is read by a model with the whole task competing for its attention, at the start, before it knows what it will do. A gate runs at the moment of the act, with the act in hand, and can return nonzero. That is why prose drifts and a gate that actually runs doesn’t: the hook reads the exact diff it is judging.
The question I ask of any rule I write for an agent now: what happens if the model ignores it? If the answer is “I’ll catch it in review,” it was advice. If something refuses, fails, or blocks, it was a rule. Huntley’s hook does both jobs at once, failing the commit and then injecting a prompt so the model learns the reason instead of routing around it. The constraint belongs in the layer that can say no. The inverse failure is live too: nanoMuse’s taint gate two weeks ago existed and never ran on the decision path auto mode used, so it blocked nothing until 1.0.2 ran it on every decision.