The AI assistant handed me a component. Clean, typed, tests included. It also used an interface instead of a type, pulled a helper in through a relative ../../../utils path, and hardcoded a user-facing string in English. Three of our conventions, broken in one file, and not a trace of doubt about any of it.
It wasn’t the tool’s fault. It had never read our rules. We had rules, plenty of them, but they lived in a wiki, and the tool had no way to reach them.
You can’t expect a machine to follow conventions you keep somewhere it never looks.
Three tools, three files
So we started writing the rules down where the AI would actually see them.
Claude Code reads a CLAUDE.md at the root of the repo. So we wrote one. It worked. The generated code started respecting our patterns because, for the first time, the patterns were sitting in the assistant’s context instead of in a wiki.
Then people started using Cursor. Cursor doesn’t read CLAUDE.md. It reads its own thing under .cursor/. So we wrote those rules too. Then Copilot arrived, and it reads .github/copilot-instructions.md. So we wrote a third copy.
Three files. The same conventions. Written out three separate times.
You already know how this ends. Someone tightens the testing convention in CLAUDE.md. The Cursor rules still describe the old way. The Copilot file has quietly become a fossil. Now the three tools disagree with each other, and all three disagree with the actual code. We had taken one documentation problem and turned it into three.
One file, symlinked
The fix was almost embarrassing.
One file, and only one. We didn’t even have to name it ourselves. There’s an open format for exactly this called AGENTS.md, a plain-Markdown file that coding agents look for the way a person looks for a README. The big AI coding tools already agreed on it, so it wasn’t something we had to invent. Using their shared standard beats making up our own.
So AGENTS.md became the one real file. For the tools that still expect their own filename, we pointed those names at it:
CLAUDE.md -> AGENTS.md
.github/copilot-instructions.md -> ../AGENTS.md
Symlinks. Claude Code opens CLAUDE.md and gets AGENTS.md. Copilot opens its file and gets AGENTS.md. One file to edit. No sync script, no CI job copying paragraphs from one place to another, no drift, because there is only one file and everything else is a pointer to it. The filesystem does the work for free.
The Cursor wrinkle
Cursor did not fall in line quite so neatly.
It doesn’t load a single instructions file the way the others do. It reads small rule files under .cursor/rules/, each with a bit of frontmatter that can mark it as always applied. Fine. We can write those.
Here’s the part that caught us out. Cursor injects the text of a rule into the model’s context. It does not follow a link inside that rule and go read the file it points to. So a rule that helpfully said “for testing conventions, see .claude/skills/unit-test/SKILL.md” was worse than writing nothing. Cursor pasted that one sentence and stopped. The conventions, one link away, never loaded. The agent went off and did its own thing, and we assumed it had the rules, because we had, technically, written a rule.
A link is not a load. That sounds obvious written down. It is not obvious at all when you’re staring at a rule file that clearly mentions the thing the agent is clearly ignoring.
So the .mdc files stopped trying to be the rules. They became triggers. Each one is short. A handful of the load-bearing bullets, and then an instruction aimed at the agent itself: before you write anything for this kind of task, go read this skill file. Not a summary pretending to be the whole thing. An order to go get the whole thing.
Here’s what one of them actually looks like, the rule for writing tests:
---
description: Vitest + RTL test generation (integration-first; always applied)
alwaysApply: true
---
# Tests (Vitest + RTL)
- Co-locate `__tests__/{name}.test.jsx` (components/hooks) or `.test.js` (pure functions); camelCase filenames.
- Render via `@test-utils/render`; Vitest globals without imports; no direct `@testing-library/react`.
- Mock with `vi.mock()`; test behavior and a11y, not implementation details.
**Full detail:** [`.claude/skills/unit-test/SKILL.md`](../../.claude/skills/unit-test/SKILL.md) — also load `references/*` when generating tests.
The bullets are a nudge for the common case. The last line is the real instruction: the rest lives in the skill, go open it. The rule is deliberately too small to be the whole story, so it can’t quietly rot into a stale half-copy of one.
The file that ate itself
While all this was happening, AGENTS.md developed a problem of its own. It was succeeding.
Every time we caught the AI getting a convention wrong, we added a section to fix it. Naming. Then styling. Then the API layer. Then testing, with examples. Then a list of anti-patterns. Then examples of the anti-patterns, so nobody could claim ambiguity.
It grew into something enormous. Hundreds of lines. And something predictable happens to a document that long. The agents started skimming it, the exact way you skim a terms-of-service page you have no intention of reading. The genuinely important rule about facades was sitting somewhere around paragraph forty, and by paragraph forty nobody was reading closely anymore. Not the humans, and not the agents we’d written the whole thing for. It slowed every session down, too. Each one began by loading a small book before it got to do any actual work.
We had recreated the original problem in a new location. A pile of rules nobody reads. Same pile, better address.
So we turned it inside out. AGENTS.md stopped being the rules and became the index to the rules. It’s about a hundred lines now, and most of that is a routing table. A topic on the left, the one file that owns it on the right.
A slice of it reads like this:
| Topic | Canonical file |
| ---------------------------- | ------------------------------------------------------ |
| React / TS / API / styling | docs/guidelines-conventions/engineering-conventions.md |
| Tests (Vitest + RTL) | .claude/skills/unit-test/SKILL.md |
| Naming | docs/guidelines-conventions/naming-conventions.md |
| Folder structure | docs/ADR/00001-folder-structure.md |
| PR / commits | docs/processes/CREATE_PULL_REQUEST.md |
Nothing in that file explains how to name a test or lay out a folder. It just knows which door to open. The discipline we hold ourselves to is a single line at the top: one topic, one file. Every convention has exactly one canonical home, and AGENTS.md only ever needs to know the address.
Now an agent reads a short index, finds the row that matches its task, and loads that one file. Not the book. The one chapter it needs for the thing it’s about to do.
The wiki was the real problem
There was still the matter of the wiki. All those original pages, the ones that kicked this off. For a while we treated them as the source and copied fragments into the repo, which, if you’ve been paying attention, is the drift problem again. Just slower and more polite about it.
Documentation in a wiki rots for a plain reason. It sits nowhere near the code, and nobody reviews it. You change how testing works inside a pull request, where two people look at the diff and argue about it. The wiki page describing how testing works just sits there and ages, until a new hire follows it to the letter and ends up confused in a channel asking why nothing matches.
So the pages are moving into the repo. Each convention becomes a file that lives next to the code it describes. Changing one means opening a pull request, which means someone reviews it, the same as any other change we make. The old Confluence pages now carry a banner across the top telling you they’re being migrated and to make your edits in the repo, not there.
And the AI reads all of it at no extra effort, because it’s finally sitting in the place the AI was already looking.
What we took from it
- One canonical file, symlinked to whatever names your tools insist on. Let the filesystem prevent drift instead of trusting a script and a good mood.
- Learn how each tool actually loads context before you write for it. Cursor pastes a rule and stops. A link is not a load. Write triggers that send the agent to read the source, not summaries that quietly pretend to be it.
- A rules file long enough to be complete is long enough to be ignored. Index, don’t inline. One topic, one file, one home.
- Put the docs where the work is. Conventions in a wiki are conventions on their way out. In the repo they get versioned, reviewed, and actually read.
The thing that caught me off guard was who ended up benefiting. We did all of this for the robots. But a convention that’s short, findable, kept in the repo, and reviewed in a pull request turns out to be a better convention for the people too. The new engineer and the new AI assistant now start from the same page, literally.
The AI never needed special treatment. It needed what everyone always needed and rarely got. The rules, written down once, kept current, and left somewhere you’d actually think to look.
