Every harness has the same well-meaning file. CLAUDE.md, .cursorrules, AGENTS.md, a system
prompt someone has been growing for months. It holds the things you want the agent to always do,
and it works — for a while.
Then you give the agent more room. Longer tasks, more autonomy, a plan it executes over an hour instead of a turn. And somewhere in there it does the thing you explicitly told it not to do.
The instinct is to write the rule again, louder. We did that. We had rules in three places, in bold, with warning emoji. It doesn’t work, and the reason it doesn’t work is structural rather than a failure of obedience.
Distance, not defiance
A rule in a system prompt is read once, at the start, and then has to survive everything that comes after — twenty tool calls, a file that didn’t parse, a redirect from the user, a subtask that went somewhere unexpected. By the time the moment arrives where the rule actually applies, it’s a hundred thousand tokens behind you.
The rule didn’t fail because the model stopped caring. It failed because it was never present at the moment it mattered.
And here’s the part that surprised us: this gets worse as models get better. A model that needs step-by-step direction re-reads its instructions constantly, because it’s checking in constantly. A model capable of running an hour of work unattended goes an hour without looking back. The more independent the agent, the further it travels from the last time it read your rules. Capability and drift move together.
So the fix isn’t a better-written rule. It’s a rule that’s somewhere else.
Let the rule wait where it applies
In this release, a protocol can carry its own enforcement in frontmatter:
gates:
- paths: ["src/migrations/"]
mode: remind
rule: "Migrations are append-only — never edit an applied one. Add a new migration instead."
- command: "npm publish"
mode: block
rule: "Run `npm run verify` first — publish is irreversible."
- after: "git commit"
mode: remind
rule: "Update CHANGELOG [Unreleased] before the next task."
Nothing there loads into the prompt. The rule sits dormant and costs nothing until the agent
actually reaches for a migration file, or types npm publish, or finishes a commit. Then it
arrives — in the tool result, at the exact instant it’s relevant, with no distance to survive.
Three triggers, and the third is the interesting one. paths fires before a write. command fires
before a bash command. after fires after a command succeeds — for when the action was correct
but created an obligation. A commit isn’t wrong; a commit without a changelog entry is.
Three strengths, and why you want the weakest one
The mode decides how hard the rule pushes:
remind blocks once, shows the rule, and lets the identical retry straight through. It costs
you one round trip and buys the agent a fact it didn’t have.
block stays blocked until a named document has actually been read this session. Not “please
consult the docs” — the gate stays shut until the read happens. This is the one that genuinely locks
an agent into a rule before it can proceed, and it’s the one to use sparingly: for the irreversible
thing that must not happen unread.
warn surfaces a notice to the human and never touches the agent at all.
We reach for remind almost every time. A rule that merely needed to be present doesn’t need to
be enforced, and most rules turn out to be in that category. The agent wasn’t being stubborn — it
just didn’t have the fact in hand.
The part we’d tell anyone building this
Two things we’d have liked to know before writing our first gate.
Anchor the pattern to the invocation, not the word. Our first command gate matched
\b(timeout|gtimeout|setsid)\b — which is exactly right, and also matches grep 'timeout' notes.md
and curl --timeout 5. Five false positives in fourteen realistic cases. Anchoring to command
position ((^|[;|&(]\s*)\s*timeout\s) took that to zero without losing a single real catch. Write
down what must fire and what must not, and check both — a gate is only as good as the workflow it
doesn’t interrupt.
Re-read a gate’s rule text when the code beneath it changes. A gate outlives the thing it was written about, and nobody re-reads a rule they already agree with. We found gates enforcing a claim about our own caching that had stopped being true months earlier — while the body of the very same file already explained why. A stale gate is worse than no gate: it’s confidently wrong at exactly the moment someone is trying to work.
Both of those are cheap to avoid once you know to look.
Where this fits
We already had two ways to write something down. A muscle is a learned pattern — a reflex, and reflexes fire only when you happen to recall them. A protocol is a rule that’s meant to be mandatory. But until gates, promoting a muscle to a protocol changed the label and not much else: both were text injected into a prompt, and text can’t stop you. Real enforcement meant writing an automation, which is a big jump most patterns never justify.
Gates moved enforcement down a rung. That makes the promotion real, and it gives a clean test for when to make it:
Does this rule keep getting broken precisely because nothing catches it at the moment it’s broken?
If yes, the enforceable part becomes a gate. The reasoning stays where it was. You’re not moving the note — you’re moving the one line that needs to arrive at the violation, and leaving the rest behind. The habit becomes automatic exactly by giving up the part it could never enforce on its own.
If no — if the rule needs noticing that you’re in a situation — it isn’t gateable, and saying so is the right answer. We keep a list of rules we deliberately did not gate for that reason. It’s longer than the list we gated.
Try it
Add a gates: block to any protocol in .soma/amps/protocols/, or declare the same thing in
settings.json under guard.pathGates if you’d rather configure than author. Start with one
remind on the path you keep having to correct. Then start a fresh session — gates are read at
startup, so the session that writes one can’t see it.
The first time a rule you wrote weeks ago shows up at exactly the right moment, without having occupied a single token in between, the shape of it clicks.
Full reference: Protocols → Gates.