Writing and scoping a rules file

A method for the file an agent reads every session: collecting the conventions a codebase really follows, deciding what loads always and what only for one area, and checking what actually loaded.

Updated
Verified

A rules file is the plain text an agent reads at the start of every session: the project's conventions, its commands and its boundaries. AI IDEs covers what one costs when it is wrong. This guide is the method for writing one that is right: what to put in it, how to split it so each rule loads where it applies, and how to check what the agent actually received.

Tools name these files differently and read them from different places. The examples below use a rules/ directory and a small header of their own, so take the file locations and the header syntax from your tool's documentation, and the decisions from this page. The products themselves are in the tool index.

Collect the conventions the code actually follows

Write the file from the codebase, not from your preferences. A rules file describing the conventions you wish you had is followed as faithfully as one describing those you have, and the result is new code that disagrees with every file around it. Four sources give you the real ones.

  1. Recent changes that passed review without comment. Read three or four. They show the conventions as the team practises them, including the ones nobody would think to write down.
  2. Corrections you have made twice. Every time you tell an agent the same thing in two sessions, you have found a rule. Keep a list as you work, and move each item into the file.
  3. What must not change. Public interfaces, URLs, the schema, merged migrations, and any module written differently on purpose. Name each one, with the reason it is different.
  4. The commands. One each for build, test, lint and type check. These are how the agent finds out whether it is right, so they should be exact.

Where the code follows a convention you would not choose today, write the one it follows. If you want a different convention, change the code first and the rule in the same commit. A rule that leads the code produces a codebase with two styles in it.

What earns a line

Every line is read on every request it loads into, so each one has to pay for that. A line earns its place when an agent would otherwise get something wrong, and the mistake would be expensive or easy to miss in review.

Lines that earn it:

  • Choices particular to this project: how errors are returned, where money is computed, which of two similar helpers is the current one.
  • Boundaries: what not to touch, and what needs a person's approval first.
  • The unusual module, and why it is unusual.
  • The commands.

Lines that do not:

  • Anything a linter, formatter or type checker already enforces. A check that fails the build is stronger than a sentence, and it costs nothing in context.
  • General good practice the agent does without being told. "Write clean, readable code" changes nothing except the size of the file.
  • Instructions for a single task. Those belong in the task's brief. A task written into the rules file is loaded into every unrelated task after it. Writing a brief for an unattended agent covers the brief.

Give each rule its reason, and an example where the pattern is not obvious. The reason is what lets a reader, the agent included, tell whether a rule covers a case it does not name, and it tells whoever maintains the file whether the rule still holds. One example of the pattern you want does more than a paragraph describing it, the point Context engineering for AI coding agents makes about context generally.

Always, by area, or on request

Each rule needs a decision about when it loads. There are three answers.

LoadForExample
AlwaysConventions that apply to every change, the commands, the boundaries, and any narrow rule whose violation cannot be undonerules/project.md, rules/money.md
When a task touches an areaConventions that hold in one part of the codebaserules/admin.md, for changes under src/admin/
When asked for by nameProcedures used now and thenrules/migrations.md, named in a brief that writes one

Most rules are easy to place. The one that is not is the narrow rule with a large consequence, and money is the clearest case. Rules about money apply to a small share of the code, which argues for loading them by area. They load always anyway, because a floating-point currency bug cannot be fixed by a later change once orders exist, only reconciled by hand, and the change that introduces one does not always touch a file anyone thought of as money code.

rules/money.md
---
load: always
---
 
# Money
 
Store and calculate money in integer minor units (cents, kobo), never
as a floating-point number. Round once, at the order total.
Why: a rounding error in an orders table is not fixed by a later
change, only reconciled by hand.
 
- Amounts are whole numbers named with a `Minor` suffix:
  `subtotalMinor`, `refundMinor`.
- Prices, totals and discounts are calculated on the server. The client
  shows amounts it was sent, and never sends one back to be trusted.
- Display formatting is `formatMoney` in `src/lib/money.js`. Do not
  write a second formatter.

An area rule says where it applies, and should be short enough that loading it by mistake would be cheap:

rules/admin.md
---
load: when the task changes files under src/admin/
---
 
# Admin area
 
The admin screens predate the rest of the app and use their own form
components in `src/admin/forms/`. Use those here, not `src/components/`.
Why: they carry the audit logging every admin change needs.
 
Every admin action that changes data writes an audit entry through
`recordAdminAction`. A change that skips it is incomplete.

The headers here are illustrative. Every tool spells the header differently, in its field names and in how it writes a path pattern, and that difference is exactly where the silent failure below comes from.

Check what actually loaded

The failure nobody notices is a header that does not say what you think it says. A malformed header can load a narrow rule into every conversation, so the admin conventions ride along on a change to the marketing pages, crowding out the files that matter. Or it can stop a rule loading at all, so the money rules quietly go missing. Nothing warns you in either case. Check what loaded, not what you meant to load, after every change to a header.

  • Where the tool shows what went into a request, look. Run one task inside the area and one outside it, and read the list of rules each received.
  • Where it does not, use a marker. Add a temporary line to the scoped file asking the agent to begin its first reply with a phrase such as ADMIN RULES LOADED. Ask about something outside the area, then about something inside it. The phrase should appear only the second time. It is a rough check, since an agent may not follow the line, but a marker that appears where it should not is proof that the scope is wrong. Remove the line afterwards.
  • Keep the always-loaded set small. It is paid for on every request. A long always-on file usually holds a rule that belongs to one area.

A rough measure of the always-loaded set is one command:

Size of each rules file, largest first
wc -w rules/*.md | sort -rn

Keep it like code

  • Review changes to it as you review code. A rule changes every change that follows it, so an edit to the rules file deserves at least the care of an edit to a shared module.
  • Change the rule in the commit that changes the convention. A rule that no longer matches the code is worse than no rule, because the agent will follow it.
  • Add a rule when you correct the same thing twice. That list from the start of this guide never closes. It is how the file keeps up with the codebase.
  • Delete what no longer applies. A rule about a module that was removed, or a library that was replaced, costs context and misleads.
  • Keep it in plain files in the repository. Conventions held in one product's settings leave with that product. Plain files can be read by the next tool, by a reviewer, and by a person joining the team.

A terminal agent reads the same kind of file, and the same method applies. So does a long session in any tool, where instructions given at the start are the first to slip: Sizing tasks an AI coding agent can finish explains why a convention belongs in a file the agent reads every time rather than in the first message.