Working with agents·9 min read

Write a rules file your agent respects

Every coding agent — Claude Code, Cursor, Windsurf, Copilot — reads a rules file before it touches your code. Most people write theirs like a wishlist and then wonder why the agent ignores it. A rules file is not documentation; it's a control surface. Here's how to write one the agent actually obeys.

Know which file you're actually writing

The filename is not cosmetic — each tool loads a specific path, and putting rules in the wrong place means they're silently never read. Claude Code reads CLAUDE.md from the repo root (and merges nested CLAUDE.md files as you descend into subdirectories). Cursor reads .cursor/rules/*.mdc files with frontmatter that scopes them by glob. Windsurf uses .windsurfrules; GitHub Copilot uses .github/copilot-instructions.md. If your team uses more than one, don't maintain four divergent copies — write the real rules in CLAUDE.md and make the others one-line pointers, or symlink them. The single worst failure mode is a beautifully written rules file that the tool never loads because it's named AGENTS.md when your agent wanted CLAUDE.md.

text
repo/
  CLAUDE.md                      # Claude Code — root, always loaded
  packages/api/CLAUDE.md         # merged in when working under packages/api
  .cursor/rules/typescript.mdc   # Cursor — scoped by glob in frontmatter
  .github/copilot-instructions.md

Rules are a budget, not a manifesto

Everything in your rules file competes for the same attention the agent needs for your actual task. A 600-line CLAUDE.md doesn't make the agent 600 lines smarter — past a point it makes it worse, because the signal you care about is buried under boilerplate the model has to swim through on every turn. Treat the file like a budget: aim for well under ~200 lines. If a rule is something any competent engineer would do by default ("write clean code", "handle errors"), delete it — it's pure noise. Reserve the space for the non-obvious, project-specific things the agent cannot infer from reading the code: which of two auth patterns is the blessed one, why you use Drizzle instead of the Prisma the model keeps reaching for, the deploy step that isn't in package.json.

Write imperatives, not vibes

'Prefer good test coverage' is unfalsifiable — the agent can claim it complied no matter what it did. 'Every new API route gets a Playwright test in tests/e2e/ that hits the real endpoint' is checkable, and checkable rules get followed. The trick is to write each rule so that you could tell, from the diff alone, whether it was obeyed. Use MUST and NEVER for the hard constraints and reserve softer language for genuine preferences. Vague guidance doesn't just get ignored — it teaches the agent that your rules are decorative, which bleeds over into the rules that actually matter.

markdown
# Bad — unverifiable
- Try to keep components small and reusable
- Use good naming conventions

# Good — checkable from the diff
- NEVER import from `@/lib/legacy/*`; it is being deleted. Use `@/lib/core`.
- Every React component file exports exactly one component.
- DB access goes through `db/queries/`. NEVER write raw SQL in a route handler.

Encode the decisions, not the descriptions

The highest-value content in a rules file is your resolved arguments — the places where there are two reasonable ways to do something and your team picked one. The agent has read a million repos and will default to the statistically common choice, which is often not yours. If you use Drizzle, say so and say NEVER suggest Prisma, or the agent will helpfully scaffold Prisma the first chance it gets. If money is stored as integer cents, write it down or you'll get floats. If dates are always UTC ISO strings at the boundary, write it down. These one-liners prevent the specific, expensive rewrites that happen when an agent guesses your convention wrong and then propagates the guess across fifteen files.

markdown
## Stack decisions (do not deviate)
- ORM: Drizzle. NEVER suggest or scaffold Prisma.
- Auth: Clerk. NEVER hand-roll sessions or JWT verification.
- Email: Resend via `lib/email.ts`. NEVER call the Resend SDK directly elsewhere.
- Money: integer cents, never floats. Format only at the view layer.
- Package manager: pnpm. NEVER run `npm install`.

Give the agent the commands so it can check its own work

An agent that can run your build, tests, typecheck, and linter will self-correct before it ever shows you the code. An agent that can't just hands you plausible-looking work and hopes. So the single highest-leverage block in most rules files is a short, exact list of the commands that verify a change — copy-pasteable, with the real flags. Include the non-obvious ones: the codegen step after a schema change, the command to run one test file instead of the whole suite, the lint autofix. Then add the rule that closes the loop: 'Before saying you're done, run pnpm typecheck and pnpm test and fix what breaks.' This one section moves more quality than the rest of the file combined.

markdown
## Verify before you claim done
- Typecheck: `pnpm typecheck`
- Lint (autofix): `pnpm lint --fix`
- One test file: `pnpm test path/to/file.test.ts`
- Full suite: `pnpm test`
- After ANY change to `db/schema.ts`: run `pnpm db:generate`
Always run typecheck + the relevant tests before reporting completion.

Fence off the danger and the sacred

There are files an agent should never touch and actions it should never take on its own, and the rules file is where you draw those lines explicitly — because the agent's default is to be helpfully aggressive. Name the generated files it must not hand-edit (migrations, lockfiles, anything under a generated/ dir). Name the destructive commands it must ask before running: no git push, no force-push, no dropping tables, no editing .env, no deleting migration files. Agents are eager to 'fix' failing states by nuking them — a red test suite becomes a deleted test, a merge conflict becomes a git reset --hard. An explicit NEVER list is cheap insurance against the expensive mistake.

markdown
## Never touch / never run without asking
- NEVER edit files in `migrations/` — they are immutable once committed.
- NEVER edit `.env` or `.env.local`. Tell me what to add; I'll add it.
- NEVER run `git push`, `git reset --hard`, or force-push.
- NEVER delete a failing test to make CI pass. Fix it or flag it.
- NEVER commit `pnpm-lock.yaml` changes unless the task was a dependency change.

Treat it as living config and prune it like code

A rules file rots the same way a codebase does. You add a rule to stop the agent from doing something dumb, the underlying reason disappears three months later, and now you're paying attention-budget for a rule that constrains nothing. The discipline that keeps a rules file effective: when the agent makes the same mistake twice, add a rule — that's the signal the model can't infer it from context. When you delete a subsystem, delete its rules in the same PR. And when you catch a rule the agent is visibly ignoring, don't add three more begging it to comply — figure out why it's being overridden (usually the rule is vague, or it contradicts something the code itself is telling the agent, and the code always wins). The best rules files are short because their owners edit them ruthlessly, not because their authors had less to say.

Test whether the rules are load-bearing

You don't actually know a rule is being followed until you watch what happens without it, so periodically probe. Ask the agent to do a task that should trip a rule and see if it complies unprompted — if you say 'add a payments table' and it reaches for Prisma despite your Drizzle rule, the rule isn't landing and you need to move it higher, make it blunter, or figure out what's outcompeting it. Position matters: rules near the top and rules phrased as hard constraints get honored more than a soft preference buried on line 140. A rules file you never audit is a file you're trusting on faith, and agents are exactly good enough to make faith feel justified right up until it isn't.

Takeaways

  • Put rules in the exact file your tool loads (CLAUDE.md, .cursor/rules, copilot-instructions.md) — wrong path means silently unread.
  • Keep it under ~200 lines; attention is a budget and boilerplate crowds out the rules that matter.
  • Write checkable imperatives (MUST/NEVER) you could verify from the diff, not unfalsifiable vibes.
  • Encode your resolved decisions — Drizzle not Prisma, cents not floats — because the agent defaults to the common choice, not yours.
  • Give exact verify commands and require the agent to run them before claiming done.
  • Maintain an explicit NEVER list for destructive commands and generated files, and prune the whole file like code.