# How to migrate CLAUDE.md and .cursor/rules to AGENTS.md

> Move the rules every agent needs into one AGENTS.md, keep each tool's file for what it alone understands, and measure what each tool loads before and after.

Source: https://voxgig.com/howto/migrate-cursor-rules-to-agents-md

- Audience: platform-team
- Level: intermediate
- Verified: 2026-10-21
- Published: 2026-10-19

## 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](https://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](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/add-custom-instructions/add-repository-instructions) | 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](https://agents.md/) | 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](https://code.claude.com/docs/en/memory) | 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](https://cursor.com/docs/rules) | 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](https://code.claude.com/docs/en/memory)
and reads as text everywhere else. Or it sits in a Cursor rule with a `globs` pattern or an
agent-matched description, [rule types](https://cursor.com/docs/rules) that a plain Markdown
file has no field for.

```js title="migrate.mjs"
// 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:

```js title="migrate.mjs"
    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:

```bash
node migrate.mjs before migrated
```

```text output
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:

```bash
cat migrated/CLAUDE.md
```

```text output
@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](https://code.claude.com/docs/en/memory). 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](https://learn.chatgpt.com/docs/agent-configuration/agents-md) by default.[^2]
Gemini CLI reads GEMINI.md and the files it imports, and reads AGENTS.md directly only when
[`context.fileName`](https://google-gemini.github.io/gemini-cli/docs/cli/gemini-md.html) in
its settings names it.

```js title="loads.mjs"
  {
    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:

```bash
node loads.mjs
```

```text output
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`](https://code.claude.com/docs/en/cli-reference), and Codex
has `codex exec` for the same job:

```bash
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.

```bash
node --test migrate.test.mjs
```

```text output
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](https://git-scm.com/docs/git-config) 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](https://gitforwindows.org/symbolic-links). Claude then
finds a CLAUDE.md, which wins over AGENTS.md by its own loading rule, and reads nine bytes.

```bash
node pitfall.mjs
```

```text output
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

- [Organize agent skills across many related repositories](/howto/organise-skills-across-a-multi-package-ecosystem)

- [Install an API skill and check a coding agent uses it](/howto/install-an-api-skill-for-a-coding-agent)

- [Scope agent instructions to one package in a monorepo](/howto/scope-agent-instructions-per-package)

## 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.

[^1]: The [agents.md](https://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.

[^2]: Codex concatenates the files from the root down, joined by blank lines. Its
[documentation](https://learn.chatgpt.com/docs/agent-configuration/agents-md) 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
are `TEAM_GUIDE.md` and `.agents.md`.

[^3]: The sentence is in [git-config](https://git-scm.com/docs/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 that `git clone` and `git init` probe and set it false
when the filesystem cannot do better. [Git for Windows](https://gitforwindows.org/symbolic-links)
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.