Your repository carries a CLAUDE.md, three Cursor rules, and a Copilot instructions file that
disagree with each other. The test command in CLAUDE.md changed two releases ago, but the copy
in .cursor/rules still names the one before. A contributor who opens the repository in Codex
gets no instructions at all, because none of those files is one Codex reads.
What you get
You will end up with one AGENTS.md holding the shared rules, a pointer file per tool holding what only it understands, and a table of what each tool loads before and after. This is for you if more than one coding agent reads your repository.
Short answer
Move every rule that applies to all tools into AGENTS.md, which Claude Code, Cursor, Copilot,
and Codex read at the repository root. Leave each tool’s file holding only what that tool
alone understands. Make CLAUDE.md a pointer that starts with @AGENTS.md rather than a
symlink, because a checkout without symlink support turns the link into a nine-byte
file. Then run one task in each tool before and after, and compare the commands it chose.
You will need
Node 22 or later for the sample, and a repository that already carries a CLAUDE.md, a
.cursor/rules directory, or a .github/copilot-instructions.md. Verified 2026-10-21 against
Node 22.22.0. The sample migrates a fixture repository under before/. The target format is
AGENTS.md, plain Markdown with no required fields.1
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
.github/copilot-instructions.md kept beside AGENTS.md | Copilot Chat users who need repository-wide text, which that file carries and AGENTS.md may not reach | A second always-on file with no import syntax, so anything shared it still holds is a copy that drifts | Every Copilot surface your team uses reads AGENTS.md |
AGENTS.md with pointer files | Several tools in one repository, and rules that are the same for all of them | One more file to review, and a pointer per tool that has to carry exactly what that tool needs | One tool, one team, and no second format in sight |
CLAUDE.md as a symlink to AGENTS.md | Claude Code is one tool among several and has nothing to be told that the others should not hear | Claude-only text leaks into every tool, and a checkout without symlink support turns the link into a nine-byte file | A contributor clones on Windows with symlinks off, or Claude needs private instructions |
Cursor .mdc rules kept per tool | Rules scoped by globs or loaded on a description, an activation AGENTS.md cannot express | Every copy of a shared rule is maintained by hand, and the copies disagree within a quarter | The rule applies everywhere, where AGENTS.md carries it once |
The pointer files and the symlink differ on one question: whether Claude Code may be told something the other tools are not. A symlink hands Claude exactly the shared file, so the Claude-only text has to move into AGENTS.md, where Cursor and Codex read it too. The measurement below puts that at 253 bytes for the fixture. Hand-maintained copies cost nothing on the first day and a divergence every time a rule changes.
Sort every rule into shared or tool-specific
The unit of migration is the rule: a list item, a paragraph, or an @ import line. A rule
moves to AGENTS.md once, however many files carried it. A rule stays behind on three grounds.
It names a tool, or sits under a heading that names one, as “Use the /commit command” does.
It is an @path import, which is Claude Code syntax
and reads as text everywhere else. Or it sits in a Cursor rule with a globs pattern or an
agent-matched description, rule types that a plain Markdown
file has no field for.
// Two files carry the same rule when the words match after case, spacing, and a final period.
export const key = (text) => text.toLowerCase().replace(/\s+/g, ' ').trim().replace(/\.$/, '')
export const toolSpecific = (it) => it.kind === 'import' || TOOL_WORDS.test(it.text) || TOOL_WORDS.test(it.section)
The plan walks the sources in order, CLAUDE.md first, so a duplicate is always reported against the file that already had the rule:
for (const it of s.items) {
if (toolSpecific(it)) {
r.kept++
keep.set(s.file, [...(keep.get(s.file) ?? []), it])
continue
}
const k = key(it.text)
if (seen.has(k)) { r.duplicates.push(seen.get(k)); continue }
seen.set(k, s.file)
shared.push({ ...it, from: s.file })
r.moved++
}
Run it over the fixture, which has the shape the problem paragraph describes:
node migrate.mjs before migrated
CLAUDE.md 13 rules 9 moved, 4 kept as tool-specific, 0 duplicates
.cursor/rules/api-routes.mdc 2 rules kept whole: a globs rule, an activation AGENTS.md cannot express
.cursor/rules/project.mdc 5 rules 1 moved, 0 kept as tool-specific, 4 duplicates
4 already in CLAUDE.md
.cursor/rules/release.mdc 2 rules kept whole: an agent-requested rule, loaded when its description matches
.github/copilot-instructions.md 4 rules 1 moved, 1 kept as tool-specific, 2 duplicates
2 already in CLAUDE.md
AGENTS.md 1094 bytes, 11 rules
CLAUDE.md 265 bytes, starts with @AGENTS.md
.github/copilot-instructions.md 190 bytes
.cursor/rules/ api-routes.mdc, release.mdc
Four of the five rules in the always-on Cursor file were copies of CLAUDE.md and the fifth moved, so the file is deleted, because Cursor reads AGENTS.md on its own. The two scoped rules are untouched. Copilot’s file keeps the one line that names Copilot code review. The pointer that replaces CLAUDE.md is short:
cat migrated/CLAUDE.md
@AGENTS.md
@docs/architecture.md
Instructions for Claude Code. Read this before changing anything.
## Claude Code
- Use the `/commit` command for commits, so the message format is the house one.
- When a task touches `src/sdk/`, stop and ask instead of editing.
The import comes first on purpose. Claude Code’s documentation says the imported file is read before the rest. It also warns that a CLAUDE.md which tells Claude in words to read AGENTS.md works only when Claude decides to open the file.
Model what each tool loads before you move anything
loads.mjs turns each tool’s documented loading rules into a byte count per layout, so the
end states can be compared before anything moves. Claude Code reads CLAUDE.md when one
exists in the working directory or a parent of it, and
AGENTS.md only when none does. Cursor reads
AGENTS.md plus every .mdc rule with alwaysApply: true, and ignores a plain .md in
.cursor/rules. Copilot’s cloud agent reads .github/copilot-instructions.md and the nearest
AGENTS.md. Codex reads AGENTS.override.md, then AGENTS.md, from the project root down to the
working directory, and
stops at 32 KiB by default.2
Gemini CLI reads GEMINI.md and the files it imports, and reads AGENTS.md directly only when
context.fileName in
its settings names it.
{
const file = read(claude) !== null ? claude : read(agents) !== null ? agents : null
if (!file) out.claude = { bytes: 0, files: [] }
else {
const text = read(file)
const exp = claudeImports(file, text)
const name = lstatSync(file).isSymbolicLink() ? `${rel(file)} -> ${readlinkSync(file)}` : rel(file)
out.claude = { bytes: exp.total, files: [name, ...exp.files.map(rel)] }
}
}
The script builds the three end states from the fixture, plus a fourth that the pitfall section is about, and prints the matrix:
node loads.mjs
bytes of instructions each tool loads at the repository root
end state claude cursor copilot codex gemini
hand-maintained 1230 423 368 nothing nothing
pointer-files 1557 1094 1284 1094 nothing
symlink 1545 1347 1537 1347 nothing
flattened-symlink 9 1347 1537 1347 nothing
gemini reads GEMINI.md unless .gemini/settings.json sets context.fileName; none of these layouts has either
Read the first row as the before state. Codex loads nothing, and Cursor loads the one
always-on rule against the 1230 bytes Claude gets.
The second row is the migration: four tools read the same 1094 bytes, and Claude adds its
pointer on top. The third row costs Cursor, Copilot, and Codex 253 bytes more than the second,
the Claude-only section and the import line sitting in AGENTS.md as text. Gemini CLI loads
nothing in every row until its settings carry
"context": { "fileName": ["AGENTS.md", "GEMINI.md"] }.
Run one task in each tool, before and after
The matrix is a model of the documentation, so finish with the tools themselves. Pick a task
whose correct answer depends on a rule that moved, such as adding a test for one source file
and running the suite. The fixture’s rules say npm ci, node:test, and a .test.mjs name.
Run it headless in each tool from the before tree, then from the migrated tree, and compare
the commands the agent ran rather than its prose. Claude Code takes the task with
-p and --output-format json, and Codex
has codex exec for the same job:
TASK="Add a test for src/api/meters.mjs and run the suite."
claude -p "$TASK" --output-format json > claude-before.json
Repeat in the migrated tree, and diff the commands. A migrated run that reaches for
npm install has lost a rule, and the report from the first step says which file held it.
These runs need a model and an account, so this page did not run them.
Check it worked
Twelve tests cover the sorting, the model, and five repository shapes the fixture lacks.
node --test migrate.test.mjs
1..12
# tests 12
# suites 0
# pass 12
# fail 0
# cancelled 0
# skipped 0
# todo 0
The first test is the one to keep. It reads every rule back out of the migrated tree and
asserts that each rule from the three sources appears exactly once, and that none appeared
from nowhere. The last asserts that a symlinked CLAUDE.md loads AGENTS.md and that the same
file flattened to its link text loads 9 bytes.
When it goes wrong
Claude Code starts a session with no instructions although the symlink end state was
committed and every test passed. The clone was made where git could not create a symlink.
The git config manual says that with core.symlinks
false, symbolic links are checked out as small plain files that contain the link text, and
that git clone probes the filesystem and sets the value itself.3 Git for Windows ships
with symlink support off by default. Claude then
finds a CLAUDE.md, which wins over AGENTS.md by its own loading rule, and reads nine bytes.
node pitfall.mjs
checkout with core.symlinks true
CLAUDE.md -> AGENTS.md, a symlink
Claude Code loads 1545 bytes: CLAUDE.md -> AGENTS.md, then the import docs/architecture.md
checkout with core.symlinks false
CLAUDE.md is a plain file of 9 bytes holding "AGENTS.md"
Claude Code loads 9 bytes from CLAUDE.md and never opens AGENTS.md
Codex, Copilot, and Cursor still load AGENTS.md (1347 bytes), so only Claude Code goes blind
flattened-link check
CLAUDE.md holds only the path AGENTS.md: a symlink flattened by a checkout without symlink support
Three tools keep working, so the fault reads as a model problem. Use the pointer file instead
of the link. Where a link stays, run flattenedLinks from loads.mjs in the test suite each
contributor runs, because a CI runner on Linux checks out a real link.
Claude Code reads CLAUDE.md and never sees AGENTS.md on a good checkout. A CLAUDE.md in the
working directory or a parent of it, including a CLAUDE.local.md, stops Claude reading
AGENTS.md directly. Put the @AGENTS.md import in the pointer, or delete the file when it
holds nothing else.
Two wordings of one rule both survive the move. The duplicate check matches words, so “Run
npm test first” and “Run the suite before you finish” are two rules to it. Read the moved
sections of AGENTS.md once, as a reviewer, and merge them by hand.
When not to do this
Do not migrate a repository that one tool reads. A CLAUDE.md on its own is already the one file, and AGENTS.md beside it adds a pointer with nothing to point at.
Do not choose the symlink when anyone on the team clones on Windows, or when Claude Code needs an instruction the other tools should not get, such as a Claude slash command. The pointer file costs one import line and has neither problem.
Do not move a scoped rule into AGENTS.md to make the migration complete. A rule that applies
to src/api/** applies everywhere the moment it loses its globs field, and Claude Code and
Copilot each have a scoped format of their own for the same reason. Scoped rules stay per tool
because the scoping is the point.
Do not cut .github/copilot-instructions.md down to a pointer, as the sample does, on the
strength of the matrix alone. GitHub documents AGENTS.md for its cloud agent and code review,
and only the repository-wide file for Copilot Chat on GitHub.com, so check which surfaces your
team uses first.
Related how-tos
Last verified
Verified 2026-10-21 against Node 22.22.0. Every output block is what the command preceding it
printed. The loading rules in loads.mjs are modelled from each tool’s documentation, linked
where each rule is stated, and were not observed by running the tools. The before-and-after
runs in the third step were not run, because they need a model and an account.
Footnotes
-
The agents.md site says the format is used by over 60k open source projects and is stewarded by the Agentic AI Foundation under the Linux Foundation. Its FAQ answers the question of required fields with a flat no: any headings will do. The site’s own migration advice is a one-line rename followed by a symbolic link under the old name, and the file it renames is
AGENT.md, singular. A format with sixty thousand adopters and no required fields has already renamed itself once. ↩︎ Back to text -
Codex concatenates the files from the root down, joined by blank lines. Its documentation explains that a file closer to the working directory overrides earlier guidance because it appears later in the combined prompt. Precedence, in other words, is a matter of coming last. The same page lists
project_doc_fallback_filenames, the other names Codex can be told to accept, and its examples areTEAM_GUIDE.mdand.agents.md. ↩︎ Back to text -
The sentence is in git-config, under
core.symlinks. If false, symbolic links are checked out as small plain files that contain the link text. The default is true, except thatgit cloneandgit initprobe and set it false when the filesystem cannot do better. Git for Windows leaves support off by default and still reads links it meets. Creating one on Windows needs a privilege that Developer Mode grants to ordinary users, so support is on by default only where Developer Mode is detected. A nine-byte CLAUDE.md is therefore not a corrupted file. It is git doing exactly what its manual says. ↩︎ Back to text