Skip to content

The Rule That Waits

The better an agent gets at working independently, the more of your instructions it drifts past — not from defiance, but because a rule read at startup is competing with everything that happened since. So we stopped putting rules in the prompt and started letting them wait at the place they apply.

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.