All posts

Rules agents follow: checks over prose

Dennis Zaiser, technical founder

Every team that works with coding agents ends up with a rules file. It starts short: use this test runner, put components here, never log secrets. It grows with every mistake an agent makes, until it is long enough that the agent follows some of it some of the time. The rules that hold reliably are the ones a tool checks. So the real question is not how to word a rule, but where each rule should live.

Why prose rules slip

A rule in prose is advice. The model weighs it against everything else in its context: the task, the code it just read, the last error message. In a long session, instructions read at the start carry less weight than the work in front of it, and after a compaction a rule may survive only as a summary of itself.

Prose also conflicts quietly. Two rules written months apart by different people point in slightly different directions, and the agent picks one. You won't know which until review.

And every rule costs context on every load, whether or not the task needs it. The more rules you add to hold the line, the less attention each one gets. The telling sign is the correction you keep making in review: that rule is written down, and it isn't holding.

A check is deterministic and talks back

A lint rule, a type, a boundary check, a test or a pre-commit hook runs every time and gives the same answer. It doesn't need the agent to remember anything. The agent meets the rule at the moment it breaks it, in the output of a command it was going to run anyway, and fixes it without a person in the loop.

Take a rule like “a package never imports another package's internals”. In prose, it holds until the agent finds a function it needs three folders over. As a lint rule that fails on the import, with the path and the allowed entry point, it holds every time, and it costs nothing until it fires.

That last point matters as much as reliability. A check sits outside the context until the moment it is needed, so a hundred checks cost an agent less attention than ten paragraphs of rules.

Write the message for the agent

When a check fails, its message is the instruction. A bare “import not allowed” leaves the agent to guess, and a guessing agent often reaches for the quickest way to make the error disappear. A good message says what is wrong, why the constraint exists and how to fix it, with the correct form shown.

For the import above: the file and line, one sentence on why packages expose a public entry point, and the import to use instead. An agent reading that fixes the cause. One reading only “not allowed” is as likely to copy the function into its own file.

The same applies to reminders. A hook that fires when an agent runs an unbounded shell command, and names the rule and the bounded form, does two jobs: it corrects the command, and it shows that the rule has slipped out of the agent's working context.

Keep the prose for what a tool can't see

Not every rule can be checked. Why a boundary exists, the edge cases where it bends, judgement calls such as preferring the smaller option, and what the product is meant to do: these stay in prose. But prose beside a check should hold only the reason and the boundary cases. Restating what the check already enforces adds context without adding a rule.

A useful habit in planning and review: whenever someone states a constraint a tool could verify, name the check that holds it, or add one. A constraint held only by prose is a finding, not a rule.

There is a middle ground worth using: a check that can't judge but can point. A hook that notices a migration file in a change and reminds the agent of the migration rule decides nothing, but it brings the right prose into context at the right moment, instead of at the start of every session.

Never let the agent silence the check

An agent under pressure to finish has easy ways out: a disable comment, a wider allow list, a relaxed threshold, a skipped test. Each one turns a check back into prose, and worse, into prose that says the opposite of the rule. Forbid them outright. A failing check is fixed at its cause. If the check itself is wrong, the agent says so, and the check is changed where it is owned, by someone who can weigh it.

The same holds for tests. A failing test is never deleted or skipped to get past it. Where a change alters behaviour a test pins, the test is updated alongside it, in the open.

Watch the pattern as well as the instance. If one check fails again and again, the check may be wrong, the rule behind it may be unclear, or agents may be missing the context that would let them meet it. Each of those has a fix, and none of them is a disable comment.

Where ctxflows fits

In ctxflows, checks are part of the workflow, not a step the agent may skip. On the Workflows page, a check that must pass holds its node until it does, every other check keeps its result as evidence, and a node can't finish until its outputs exist. A loop repeats its work until the checks pass. The checks run on the engine, in the run's working copy on your machine, as the Engine page describes, so your code never enters ctxflows.

The prose that remains lives in the Library: one set of rules for every agent instead of one per vendor, written once, with a context step that hands each task only the rules that apply. Terms brings the same discipline to words: an agent checks its text against the project's vocabulary before it writes, and unknown or misused words are found before review, not argued about in it.

Give your coding agents the context and the workflows

ctxflows is the context layer and the workflows for agent-built software.