Writing an AGENTS.md That Actually Reduces Bugs

Most agent instruction files are a list of vibes. Here is what belongs in one, based on which rules measurably changed the output.

Share
Writing an AGENTS.md That Actually Reduces Bugs. Abstract ai tooling illustration in orange and dark grey on debugly.dev

Every coding agent now reads a project instruction file. Claude Code reads CLAUDE.md, Cursor reads .cursor/rules, others read AGENTS.md, and several read whichever they find.

Most of the ones I have seen are close to useless. They contain things like "write clean code", "follow best practices", and "use meaningful variable names". A model that needed to be told that would not be able to comply, and one that can comply did not need telling.

The file is genuinely valuable, but only for a specific category of content: facts about your system that a competent engineer could not deduce from reading the repository.

The test for whether a rule belongs

Ask: could a strong engineer, given read access to this codebase and a week, work this out on their own?

If yes, leave it out. The model can infer it too, and every line you add dilutes the ones that matter.

If no, it belongs. These are the constraints that live in somebody's head, in a Slack thread from 2024, or in the memory of an incident.

This maps directly onto the failure modes I see most in agent written code. Those bugs are not caused by the model being weak at code. They are caused by context that is invisible in the text.

What actually belongs

Commands, exactly

The single highest value section, and the most boring.

## Commands
- Install: `pnpm install --frozen-lockfile`
- Dev: `pnpm dev` (port 3000, needs Postgres on 5432)
- Test: `pnpm test` (Vitest)
- Single test: `pnpm test -- path/to/file.test.ts`
- Typecheck: `pnpm typecheck` (run before every commit)
- Lint: `pnpm lint --fix`
- DB reset: `pnpm db:reset` (destructive, local only)

Without this the agent guesses, runs npm test in a pnpm repository, gets an error, and spends three turns recovering. With it, verification loops close immediately.

The "single test" line is disproportionately useful, because agents default to running the entire suite and a slow suite makes every iteration expensive.

Invariants that are not visible in the code

This is the part that reduces bugs.

## Invariants
- Money is always integer paise. Never float. `amount_paise`, not `amount`.
- Every request handler is concurrent. Assume two run simultaneously
  with the same input. Use upserts, not read-then-write.
- `users.email` is unique but NOT case-normalised at the DB level.
  Always `.toLowerCase()` before comparison or insert.
- Soft deletes: `deleted_at IS NULL` is required on every user-facing
  query. There is a `activeUsers` helper. Use it.
- Tenant scoping: every query touching tenant data must filter by
  `tenant_id`. There is no RLS. Missing it is a data leak, not a bug.
- All times stored UTC. Display conversion happens only in the view layer.

Every one of those is a real constraint you cannot see by reading a file. The tenant scoping one in particular is the kind of rule where a violation is not a failing test, it is an incident.

Things that look wrong and are correct

Agents "fix" code they do not understand. Pre-empt it.

## Do not "fix" these
- `apps/api/src/legacy/pricing.ts` looks over-complicated. It encodes
  three years of tax rules. Do not refactor. Add cases, do not restructure.
- The double JSON.parse in `webhooks/stripe.ts` is intentional. The
  provider double-encodes.
- `setTimeout(fn, 0)` in `cart-drawer.ts` is load-bearing. It defers
  past a third-party script that binds on DOMContentLoaded.

That last kind of comment saves real time. Without it the agent removes the timeout, the tests pass, and the bug reappears in production where two scripts race.

Error handling policy

## Errors
- Never `catch (e) {}` or `except Exception: pass`.
- Never catch broadly and return null. Let it propagate or wrap with context.
- Wrapping is fine, but preserve the cause: `raise X(...) from e`.
- Config errors must fail at startup, not at first use.
- No default values for required config. `os.environ["X"]`, not `.get()`.

Swallowed exceptions are the single most common pattern in agent output and it is the one most likely to turn a loud failure into silent corruption. This section pays for itself.

Testing policy

## Tests
- Do NOT derive expected values by running the implementation.
  If you cannot determine the expected output from the spec, ask.
- Do not mock the module under test.
- Integration tests use a real Postgres via testcontainers. Do not mock the DB.
- Every new endpoint needs a query-count assertion:
  `expect(queryCount).toBeLessThan(5)`

The first rule addresses tests generated from the implementation, which is the most dangerous pattern because it produces green CI around a bug.

Performance constraints

## Performance
- `orders` has 40M rows. Never `SELECT *`. Never query without an index
  on the filter column. Check with EXPLAIN before adding a query.
- No I/O inside loops. Batch it.
- The `/api/feed` endpoint has a 200ms p99 budget.

The row count line is exactly the sort of thing that is nowhere in the repository and determines whether generated code works.

What to leave out

Style. Prettier, ESLint, Black, and gofmt do this deterministically. A rule saying "use 2 space indentation" is a formatter's job, and the agent will match surrounding code anyway.

Generic advice. "Write readable code", "add comments where helpful", "consider edge cases". No behavioural effect.

Anything the types already say. If the function signature says it returns Result<User, DbError>, do not also write a paragraph about it.

Long architecture essays. Two paragraphs of context is useful. Two pages gets skimmed and pushes out the specific rules.

Keep it short

This is the part people resist. A 400 line instruction file performs worse than a 60 line one.

Everything in the file competes for attention with the actual task, and with the code the agent has read. Past a certain length, adding rules makes the existing rules less likely to be followed.

My working target is under 100 lines. Every line should be something that has actually caused a problem. When I add a rule, I usually try to remove one.

A good forcing function: when an agent produces a bug, ask whether a rule would have prevented it. If yes, add the rule. If no, the file is not the fix and you need a lint rule, a type, or a test instead.

Prefer mechanical enforcement

The most important point in this post.

A rule in AGENTS.md is a request. A lint rule is a wall. Where you can convert one to the other, do it:

Instead of a rule saying Use
Never catch bare exceptions ESLint no-empty, Ruff BLE001
Always filter by tenant_id A repository layer that requires it in the type
Money is integer paise A Paise branded type
No I/O in loops A query count assertion in tests
Config must fail fast A schema validated at startup with Zod or Pydantic
Do not use this deprecated helper @deprecated plus an ESLint rule

The instruction file should hold what you cannot enforce mechanically. Everything else belongs in the toolchain, where it also protects you from humans, and where the feedback arrives in the agent's own verification loop rather than depending on it having read a file.

That last part is the real mechanism. An agent that runs pnpm lint and sees an error will fix it. An agent that read a rule sixty turns ago may not recall it. Put the constraint where the loop can see it.

A skeleton

# AGENTS.md

## What this is
One paragraph. What the product does, the main services, where they live.

## Commands
Install, dev, test, single test, typecheck, lint, db reset.

## Invariants
The 5 to 10 rules that cause incidents when broken.

## Errors
Your error handling policy.

## Tests
How to write them, what not to mock, required assertions.

## Do not touch
Files and patterns that look wrong and are correct, with reasons.

## Scale
Row counts, traffic, latency budgets.

Then delete anything you cannot point at a real incident for.

The honest caveat

None of this makes agent output reliably correct. It moves the error rate, sometimes noticeably, on the categories you write rules for. It does nothing for the categories you have not thought of yet.

The file is a way of transferring context, and the reason it works at all is the same reason onboarding documentation works: most mistakes made by capable newcomers come from not knowing things that were never written down. Agents are permanent newcomers. They will be new again tomorrow.