The gate I expected to be fake
I built a governance-audit prompt, grounded it in my most heavily-governed project, then pointed it back at that same project. I went in cocky. This was the codebase I was proudest of: honesty rules everywhere, a do-not-act gate, fixtures wired into the test runner. I assumed the audit would mostly pat me on the back.
The first thing it made me check was the do-not-act gate. The system pushes scheduled summaries on its own, and one function decides whether any actionable line is allowed out, or whether the run should fall back to “the data is too stale, here is the one honest sentence instead”. I had quietly assumed it was decorative. A nice-looking function everyone trusts and nobody wired in properly.
So I traced it. It turned out to be called per run, before every scheduled push, and it is pinned down by half a dozen fixtures in the test runner: stale data suppresses, a failed refresh suppresses, a mass scan failure suppresses, fresh data passes, and an explicit “when the gate is shut, push nothing” case. Break any of those and the suite goes red and the commit never lands.
That was the surprise. I had braced for a broken control and found a real one. Which raised the obvious follow-up: if this one was load-bearing, how many of the others just looked like it?
Enforced versus aspirational
Here is the lens the audit runs on. Every rule in a project is one of two kinds.
An enforced rule has a gate. Something mechanical fails when you break it: a hook exits non-zero, or a commit gets rejected before it lands. You cannot proceed.
An aspirational rule is a sentence. It lives in a doc or an instructions file. It describes what should happen and hopes someone, or some model, reads it and complies.
Both are useful. The trouble is they look identical on the page. “Never run destructive commands without confirmation” reads exactly as solid whether it is backed by a deny-list the shell actually consults or by nothing at all. You cannot tell the real rule from the wish by reading it. You tell them apart by asking what fails when you break it.
This matters more in 2026 than it used to, because a lot of what reads our rules now is agents, and agent instructions are advisory by construction. Anthropic’s own Claude Code docs are refreshingly blunt about it. The guidance on CLAUDE.md says you can “tune instructions by adding emphasis (e.g., ‘IMPORTANT’ or ‘YOU MUST’) to improve adherence” — improve, not guarantee. And in the same document, on hooks: “Unlike CLAUDE.md instructions which are advisory, hooks are deterministic and guarantee the action happens.” That is the vendor itself drawing the line between a wish and a gate. A rule the model follows most of the time is fine for a style preference. It is not fine for the command that wipes your working tree.
The gap is industry-wide, not personal sloppiness. A 2026 survey of 225 security and risk leaders found that 60% of organisations could not quickly terminate a misbehaving agent and 63% could not enforce limits on what their agents were allowed to do, even though every organisation surveyed had agentic systems on the roadmap and 51% already ran them in production. The policies are getting written. The gates are not getting built at the same rate.
The deny-list with holes
This was the finding I would least have gone looking for on my own, because it lived in config I had personally set up and felt good about.
The project relied on a deny-list of catastrophic commands. It blocked rm -rf of home and of root and their wildcard variants, plus sudo rm. Looks responsible. Then I read it next to the threat instead of next to my own sense of having been careful, and the holes showed up fast.
It did not block rm -rf of the repo path itself. It did not block relative paths. And it did not block the shell-wrapped form:
bash -c 'rm -rf .'
That last one sails straight past a deny-list that pattern-matches on the literal command, because the dangerous string is now an argument to bash, not the command being matched. The agent ran in auto-approve. One missed variant and the working tree is gone, silently, with the deny-list reporting all-clear. Nobody had ever sat down to hunt for the gaps, including me, and I wrote the thing.
The wolf the audit cried
Then the audit overreached, and the overreach taught me more than any of the real finds.
It flagged my permission setup as a gap: auto-approve on, no “ask before running” tier, a pre-approved allow-list of read-only commands. On paper that reads reckless. Except I had built it that way on purpose, written it down, and dated the decision. The whole point was to kill the constant approval prompts during walk-away runs, roughly seven in ten of them noise, while a catastrophic-only deny-net stayed underneath. The audit never read that note. It saw a missing control and cried wolf on a deliberate trade-off.
Which is its own kind of failure, and a worse one. An audit that hunts gaps with no respect for intent just nags you back into the friction you removed on purpose. So the rule it taught me is now the audit’s first duty, ahead of finding anything: before you call something a gap, search the docs and git history for a decision. If you find one, cite it. If you do not, cite “no decision found” and keep the gap. A control that was present and then removed, with a reason in a decision note, a doc, or a commit message, is a choice, not an oversight.
And when there is a genuine fix, watch which direction it pushes. Hardening can be silent: a deny-list rule, a hook, a scheduled job, a check in the build. None of those prompt anyone. Re-adding an “ask first” tier nags on every run. The two are not interchangeable. You can close a real hole in a deny-net and ship zero new prompts, which is a strict win. Reintroducing confirmation friction someone deleted deliberately is a regression dressed up as diligence. Deny is silent. Ask nags. Know which one your “fix” is before you call it one.
Three more shapes this takes
The do-not-act gate, the deny-list, and the false alarm above were all real episodes in my own config. The next three are patterns I have hit before and that the audit prompt is built to surface. I am generalising them here so you can spot them in your own repo, not presenting them as fresh discoveries.
The control that is pure ceremony. Picture a “quarterly self-demotion” job, the thing whose entire purpose is to retire review rules once they have quietly stopped catching anything. Good idea. It exists as a script. It is documented as the mechanism for keeping the rule set honest. It is wired to no schedule and has never run once. The one mechanism meant to stop the rule set rotting was itself the most rotted thing in it, dressed up as enforced.
The map that stopped matching the territory. A canonical settings doc, the one your onboarding tells people to read first, describes a safety tier of commands that prompt before running. The live config dropped that tier in some refactor months ago. So the document everyone trusts as the source of truth describes a safety net that is no longer there. This one is sneaky because the doc is more confident and more readable than the config, so when they disagree people side with the doc.
The budget nobody can meet. A health check flags one file as over its size budget on every single run, because that file is structurally always going to be over it. A gate that is permanently red stops being a gate. People learn the alert is noise and filter it out, which is the rational thing to do. Then the day a real size drift shows up, the alert fires exactly as it always has, and nobody looks.
The negative control
So you patch the deny-list and the build goes green. Done? Not yet. Green only proves the gate passes good input, which tells you nothing about whether it would catch bad input. A guardrail you have only ever watched succeed has not been tested as a guardrail at all.
This is the part I keep relearning, and I am stealing it from lab work: you do not trust an assay until you have run a known-positive and a known-negative through it and watched both come out right. Same idea here. A gate is worth nothing until a known-bad input proves it fails, and fails for the right reason.
For the deny-list, that means feeding it the wrapped rm in a sandbox and confirming it gets blocked, with the block naming the rule that caught it:
# negative control: the gate must FAIL on known-bad input
if echo "bash -c 'rm -rf .'" | ./scripts/deny-check.sh; then
echo "FAIL: deny-list let the wrapped rm through" >&2
exit 1
else
echo "OK: deny-list blocked it"
fi
If that script exits clean, your deny-list is real. If it does not, you found out in a test instead of in a postmortem. (I, ah, found out in a test. This time.)
The one test
You do not need my prompt to start. You need one question, asked of every rule in your project.
What gate fails if I violate this?
If the answer names a hook or a rejected commit, then it is a rule. If the answer is “well, it is in the docs” or “the agent is told not to”, then it is a wish. Wishes are fine, as long as you know which ones you are holding. The danger is mistaking a wish for a rule and sleeping well because of it.
Then the harder follow-up. Have I ever watched this gate fail on purpose? If not, you have a control you are hoping works, which is a different thing from a control that works.
And one the audit had to teach me, because I aimed it the wrong way first. Before I patch a gap, did I check it was not a choice? A control that is missing because someone decided it should be missing, and wrote down why, is not an accidental omission; it is accepted risk. You can still challenge the trade-off, but do not pretend the decision never existed. Read the decision record before you reach for the fix.
I wrote the audit up as a reusable prompt: governance-audit-and-level-up. Run it on your most-governed project, not your least. The least-governed one tells you nothing you do not already suspect. The most-governed one is where you have stopped looking, and where you stop looking is exactly where the wishes hide. I went in expecting applause and came out with a deny-list to patch. That is the good outcome.
Comments & Reactions
Got a thought, a war story, or a “well, actually”? Sign in with GitHub and jump in.
Loading comments…