How-to › Expose your API to agents

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.

Audience
Platform team
Level
intermediate
Topic
Write agent skills and instruction files
Verified

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

ApproachWhen it fitsWhat it costs youWhen to pick something else
.github/copilot-instructions.md kept beside AGENTS.mdCopilot Chat users who need repository-wide text, which that file carries and AGENTS.md may not reachA second always-on file with no import syntax, so anything shared it still holds is a copy that driftsEvery Copilot surface your team uses reads AGENTS.md
AGENTS.md with pointer filesSeveral tools in one repository, and rules that are the same for all of themOne more file to review, and a pointer per tool that has to carry exactly what that tool needsOne tool, one team, and no second format in sight
CLAUDE.md as a symlink to AGENTS.mdClaude Code is one tool among several and has nothing to be told that the others should not hearClaude-only text leaks into every tool, and a checkout without symlink support turns the link into a nine-byte fileA contributor clones on Windows with symlinks off, or Claude needs private instructions
Cursor .mdc rules kept per toolRules scoped by globs or loaded on a description, an activation AGENTS.md cannot expressEvery copy of a shared rule is maintained by hand, and the copies disagree within a quarterThe 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.

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

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

  2. 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 are TEAM_GUIDE.md and .agents.md. ↩︎ Back to text

  3. 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 that git clone and git init probe 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

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.