DEV Community

Takashi Matsuyama
Takashi Matsuyama

Posted on • Originally published at blog.tak3.jp

Ending the CLAUDE.md / AGENTS.md / copilot-instructions.md Sprawl — Declare One Source, Verify the Wiring

Open a repository these days and you'll find several nearly identical agent-instruction files sitting side by side — CLAUDE.md, AGENTS.md, .github/copilot-instructions.md. This post is about ending that sprawl not by hand-syncing it forever, but with declaration and verification. Last time I introduced the harness itself, basou. This one is about a smaller, duller problem that sits just upstream of it — and that far more people are quietly stepping on.

"Yet another instruction file"

Over the past year or two, every AI coding tool has brought its own "agent-instruction file." The project's rules, coding conventions, the things not to do. A file meant to be read by the agent, separate from the human-facing README.

The trouble is that each tool uses a different filename. Claude Code reads CLAUDE.md, Codex reads AGENTS.md, GitHub Copilot reads .github/copilot-instructions.md. Use more than one of these tools on the same team and you end up with three copies of the same content in the repo.

And they rot in silence. You fix one and leave the other a version behind. You think you've told the agent "here are the rules," while a different tool is reading the previous generation's rules. Worse, you can't tell by eye which copy is current and which has gone stale.

The scope of this post is a single repository, for an individual or a small team. Not company-wide policy distribution — just the everyday problem of "instruction files keep multiplying in my repo," dealt with in a realistic number of moves.

Why you can't just merge them into one

The naive thought is "just standardize on one AGENTS.md." And in fact, AGENTS.md is becoming the de facto common format. Designed as a "README for agents," it's an open spec that, as of mid-2026, is stewarded by the Agentic AI Foundation (under the Linux Foundation), read by more than twenty tools, and adopted in over 60,000 open-source repositories. Codex CLI, Cursor, Windsurf, Aider, Zed — most of the field has converged on AGENTS.md, and even GitHub Copilot's coding agent now reads it. Google's newer entrants (Jules and friends) do too.

The one that didn't converge is Claude Code. What Claude Code reads is CLAUDE.md, not AGENTS.md. It doesn't even read AGENTS.md as a fallback (the claim that "it reads AGENTS.md if CLAUDE.md is absent" is wrong). Start Claude Code in a repo that has only AGENTS.md and you'll get no error — it just runs, cheerfully, with zero project instructions loaded. That's the quietest place for a "unified" team to break. The request to "please support AGENTS.md" has gathered thousands of reactions on anthropics/claude-code issue #6235, but as of July 2026 the official docs show no sign of it on the roadmap.

Laid out, the matrix looks like this (as of July 2026).

Tool Instruction file it reads
Claude Code CLAUDE.md (does not read AGENTS.md)
Codex CLI AGENTS.md
GitHub Copilot .github/copilot-instructions.md (the coding agent also reads AGENTS.md)
Cursor AGENTS.md (.cursor/rules is a separate, current mechanism; the old .cursorrules is deprecated)

As long as you use Claude Code, you can't delete CLAUDE.md. Standardize on AGENTS.md and you're still left maintaining a second file just for Claude Code. That one asymmetry is what keeps "just merge it into one file" from ever quite working.

Naive solutions, and where they run out

Copy-paste into all of them

The most naive move is to copy the same body into each file. It works the first time. It falls apart from the second time onward: every one-line rule change now obliges you to edit N files, and a human will always forget one of them. And, as above, you can't see which copy has rotted. The cost of syncing, and the inability to detect rot — those two are the intrinsic limits of copy-paste.

Consolidate into one file and reference it via import

Claude Code has an @path import syntax. Write @AGENTS.md as the first line of CLAUDE.md and, at startup, the contents of AGENTS.md are expanded inline and read as-is.

<!-- CLAUDE.md -->
@AGENTS.md
Enter fullscreen mode Exit fullscreen mode

This is a clean instinct. The real body lives in a single place — AGENTS.md — and CLAUDE.md is just a one-line stub. Import also lets you add tool-specific instructions on top of the shared body — if there's a rule you only want Claude Code to follow, put it from line two of the stub onward. It has limits, though. The import syntax isn't common across tools (the same trick doesn't work in Copilot's .github/copilot-instructions.md). And since both files can still carry a body, the moment the "keep the stub to one line" discipline slips, the drift is back.

For what it's worth, running Claude Code's /init in a repo that has an AGENTS.md will read the existing AGENTS.md and incorporate the relevant parts into the generated CLAUDE.md — but that's a copy, not a reference. If consolidation is the goal, writing the one import line by hand is the cleaner move.

Bundle into one real file with a symlink

The more direct option is to make "there is only one real file" true at the filesystem level. Make CLAUDE.md a symlink to AGENTS.md, and when Claude Code opens CLAUDE.md, the OS transparently returns the contents of AGENTS.md. The tool never has to know about the symlink. The worry that "it won't follow the symlink" is, at least for reads, unfounded: the tool does the opening, but the OS does the following. In fact, Claude Code's own official documentation lists ln -s AGENTS.md CLAUDE.md as an option.

For a single repository, this is the sturdiest choice. Here's the actual procedure.

In practice — bundling to one canonical with symlinks

Topology

Make AGENTS.md the canonical (the hub) and let the other files be spokes pointing at it.

myrepo/AGENTS.md                          # canonical (the real file — edit only this)
myrepo/CLAUDE.md                          -> AGENTS.md
myrepo/.github/copilot-instructions.md    -> ../AGENTS.md
Enter fullscreen mode Exit fullscreen mode

The reason to make AGENTS.md the canonical is that it's the format the most tools read out of the box. If Claude Code ever supports AGENTS.md, you just drop one spoke.

Commands

# If the body currently lives in CLAUDE.md, move the real file to AGENTS.md first
git mv CLAUDE.md AGENTS.md

# Make CLAUDE.md a symlink to AGENTS.md
ln -s AGENTS.md CLAUDE.md

# For Copilot (mind the relative path as seen from inside .github/)
mkdir -p .github
ln -s ../AGENTS.md .github/copilot-instructions.md
Enter fullscreen mode Exit fullscreen mode

Making the paths relative is the trick. Clone the repo anywhere and the links resolve entirely within the repo, so they don't break.

How Git handles it

Git records a symlink plainly, as a "blob of mode 120000 whose content is the link target path." So symlinks commit as-is and are restored on clone.

$ git ls-files -s AGENTS.md CLAUDE.md .github/copilot-instructions.md
120000 …  .github/copilot-instructions.md  # symlink
100644 …  AGENTS.md                        # real file
120000 …  CLAUDE.md                        # symlink
Enter fullscreen mode Exit fullscreen mode

The caveat is Windows. Checking symlinks out correctly requires core.symlinks=true plus developer mode or the right privileges. Without that, a symlink is expanded as a plain text file whose content is the link target path. A team with Windows in the mix should just confirm this point up front. (As a general note — I run macOS myself.)

Where this approach runs out

Symlinks are enough within a single repo, but three problems surface as the scope widens.

  1. Public repos and private contents. If the instruction file contains non-public planning information, putting the real file in a public repo's history exposes that content directly. A symlink keeps the contents out, but the link target path — the directory layout on the private side — still lands in the history.
  2. Wanting one canonical across several repos. The more repos you have, the more hand-linking breaks down as a practice.
  3. The wiring rots in silence. A link goes missing, breaks, or points somewhere else — and you can't track that by eye. You've reintroduced instruction-file rot, this time at the symlink layer.

In short, symlinks solve "bundle to one real file," but not "declare" and "verify." You're still linking by hand and checking by hand, forever.

Going further — declare the canonical, verify the wiring

Everything so far stayed inside one repository. From here we step just past the scope I drew in the first section — to when repos multiply and public and private start to mix. The idea is this:

Declare one canonical, generate the wiring to each tool, and verify that wiring.

That three-beat rhythm needs no dedicated tool. Write the canonical's location once (declare), lay the links with a few lines of script (generate), and drop a few lines in CI that diff the expected output of git ls-files -s (verify) — and the shape holds. What separates it from linking by hand is that "the wiring as it should be" exists as code rather than prose, and a machine tells you when it breaks.

What I actually use is this same idea realized in basou. basou is a harness for steering AI coding agents, and one of its features treats the wiring of agent-instruction files declaratively. What follows describes v0.32.0.

Declare

You declare each repository and its attributes (visibility — public or private, language, and so on) in a manifest. The canonical body lives in exactly one place, on a private "anchor" — the repo that aggregates the canonicals (agents/<repo>/AGENTS.md). Each repo's instruction file is treated as wiring pointing there.

The topology is the same hub-and-spoke as before, except the hub points at the anchor's canonical.

<repo>/AGENTS.md                          -> <anchor>/agents/<repo>/AGENTS.md   # hub -> canonical
<repo>/CLAUDE.md                          -> AGENTS.md                          # spoke -> hub
<repo>/.github/copilot-instructions.md    -> ../AGENTS.md                       # spoke -> hub
Enter fullscreen mode Exit fullscreen mode

The canonical is the single file on the anchor side. Each repo's AGENTS.md becomes a gitignored symlink pointing to it. So the canonical (which may contain non-public planning information) is edited exactly once, every tool reads it through the symlink, and it never enters a public repo's history.

Generate

Don't lay the wiring by hand — generate it.

basou project derive    # generate the full wiring from the declaration (dry-run by default; --apply writes)
Enter fullscreen mode Exit fullscreen mode

derive builds, in dependency order, each repo's symlinks, the canonical's boilerplate block, the .gitignore that keeps a public repo clean, and the workspace view that bundles several repos (there are also symlinks / preset / gitignore / workspace to run individually). All of it is non-destructive. It creates only what's missing (preset updates only its own generated block and never touches your hand-written body), it won't overwrite an existing file or repoint a symlink that points elsewhere, and it reports conflicts as conflicts and leaves them to a human. There's also a retrofit for pulling an existing hand-written AGENTS.md into this topology.

Verify

And then, verify the wiring. This is the piece that was missing from the hand-run symlink approach.

$ basou project check     # surface drift between the declaration and the actual wiring (read-only)
✅ Every present repo's and the view's instruction files (AGENTS.md + spokes) are wired as declared.

$ basou project wiring    # inspect the wiring and privacy risk (read-only)
✅ No instruction file is tracked by git in a public-facing repo (no privacy risk).
Enter fullscreen mode Exit fullscreen mode

check reports drift like "the canonical is missing," "a spoke is absent," or "there's a conflict." wiring goes further and surfaces privacy risk — a state where a public repo tracks an instruction file in git and could expose the contents of a private canonical in public history. What the eye couldn't track comes out as a verdict from a single command.

The output above isn't a staged sample. It's the real thing (excerpted), run just now against the repository of the blog you're reading. And this blog's symlinks were laid by hand, following the procedure in the "In practice" section — not through basou's wiring feature. Add the manifest declaration after the fact and run check, and it finds nothing to fix — the hand-laid wiring passes the reconciliation against the declaration as-is. The "In practice" and "Going further" sections aren't an either/or; they converge on the same shape. Start by hand, and nothing is wasted.

The exception — a self-contained private repo

You don't always converge on the anchor with hub-and-spoke. The very blog repo I've been using as the example is private, and it keeps AGENTS.md as a real file and commits it, symlinks and all — the git ls-files output shown in the "In practice" section is, in fact, from this very repository. Its manifest declaration is, in reality, just these three lines.

repos:
  - path: .
    visibility: private
    language: ja
Enter fullscreen mode Exit fullscreen mode

Being private, it has no leakage risk, so rather than aggregating the canonical onto an outside anchor, it's more natural to keep the instruction docs' editing and diffs entirely inside the repo — the single-repo shape, where the repo is its own anchor. In basou you can declare a repo that "commits its instruction files by design" as instructions: self, and a declared repo isn't flagged as a privacy risk even though it tracks instruction files.

Where self really earns its keep is on public repos — an OSS project that deliberately commits AGENTS.md for contributors to read. A private repo doesn't need it: it passes on the strength of its visibility alone (that's what the wiring output above shows). So a public repo hides its canonical and wires to it, while a repo that's fine being self-contained commits the canonical outright — being able to declare even that distinction is the difference from hand-running.

An honest positioning

Let me put the exit up front. If you're on a single repository and don't need per-tool differences, stopping at the symlink from the "In practice" section is fine. This blog itself is still in exactly that shape. Conversely, once you want to add agent-specific instructions, that's the domain of the import from the earlier section — full symlink unification (and basou's wiring too) has no answer for it.

And, to be clear: basou is my own harness, still at a single-author dogfooding stage. What I want to recommend here isn't a product but the shift in framing — from "hand-sync N files" to "declare one canonical, and have the wiring generated and verified." Laying symlinks by hand already gets you half of it today, and verification can start from a few lines in CI. basou is just one example of putting that three-beat rhythm onto a single manifest. What basou itself is, I'll leave to the previous post.

In summary

  • Each AI coding tool uses a different instruction filename (CLAUDE.md / AGENTS.md / copilot-instructions.md). AGENTS.md is converging toward a de facto standard, but because Claude Code reads only the CLAUDE.md family, never AGENTS.md, you can't collapse everything into one file.
  • The first hands-on move is the symlink. Make AGENTS.md the canonical and turn CLAUDE.md and friends into symlinks to it: the real file becomes one, and tools read it transparently. Git carries it plainly as a 120000 blob (only Windows needs care).
  • Symlinks solve "bundle to one real file," but not the mix of public and private, multiple repos, or wiring rot. From there, move onto the three-beat rhythm of declare (one canonical), generate (the wiring), and verify (drift and privacy risk) — and you can start from a few lines of CI, no dedicated tool required.
  • Take the management of instruction files themselves from hand-syncing to human declaration plus tool generation and verification. The wiring of instruction files is one more rein a human should hold. The scarce resource, again, is control.

The Japanese version of this post — its "paired" article — is already live.

Top comments (0)