AGENTS.md and CLAUDE.md import drift: when @AGENTS.md silently doubles in size
If your CLAUDE.md is one line that reads @AGENTS.md, you are not looking at what Claude sees. The repo this analyzer ships from uses exactly that pattern. So does Anthropic's own recommended snippet. The trouble is that the pointer stays one line while the target file grows, recurses, and gets edited by teammates who never open CLAUDE.md.
When CLAUDE.md contains @AGENTS.md, Claude Code inlines the entire AGENTS.md at session start, recursive up to five hops, per Anthropic's memory docs. Import drift = the target grows independently while the one-line pointer stays the same, so the ruleset Claude follows diverges from the file you re-read. The damage compounds from three places: nested imports you did not author, a first-time approval dialog teammates can decline silently, and the analyzer blind-spot where pasting CLAUDE.md alone scores the pointer instead of the resolved payload.
The two files this site ships with
Before we walk the drift, look at the actual files. The CLAUDE.md and AGENTS.md for this repo are short enough to fit on screen. The CLAUDE.md is exactly one import.
The HTML comment fences are not decoration. Per Anthropic's docs, block-level HTML comments are stripped before the content is injected into Claude's context. That makes them free bookmarks for humans: a future teammate can hand-edit only the lines between BEGIN and END without breaking anything Claude reads. Comments inside code blocks are preserved, so if you need the fences visible to the agent, wrap them in a code block instead.
Anthropic's own recommended pattern is an import
This is not a private trick. The memory docs ship the snippet below as the canonical setup for any repo that already uses AGENTS.md for Codex or another agent. The first line is the import; the rest of CLAUDE.md is Claude-specific overrides that run after the imported content.
A symlink also works on macOS and Linux. On Windows it requires Administrator or Developer Mode, so the docs recommend the @-import. The reason this matters for drift: a symlink is one file with two names, so growth is visible from either side. An import is two files with a one-way reference, so growth in the target is invisible from the source.
Five moments where the drift accumulates
Drift is not one event. It is a chain of small, individually reasonable edits across a quarter. Here is the shape we see most often, with day numbers chosen to make the cadence concrete.
Day 0: you write a clean pointer
You add CLAUDE.md with one line: @AGENTS.md. Anthropic's docs literally ship this snippet as the recommended pattern for repos that already use AGENTS.md for Codex or other agents. At this point your CLAUDE.md is 10 characters and the AGENTS.md it points at is 400 characters. You re-read the pointer and feel like you are in control.
Day 7: a teammate edits AGENTS.md, never opens CLAUDE.md
Someone on the Codex side of the team adds a Stack section and a Workflow section to AGENTS.md. They never touch CLAUDE.md because CLAUDE.md says @AGENTS.md and they assume the pointer takes care of it. The pointer is still one line. The target is now 2,200 characters. Claude is reading 2,200 characters on every turn and you do not know yet.
Day 21: a nested import doubles it again
Someone adds a single line to AGENTS.md: @docs/style-guide.md. Anthropic's loader recurses up to 5 hops. style-guide.md is 4,000 characters and was written for a human onboarding doc, not for an agent. The first 20 lines have an ISO date in the header, which ccmd's analyzer would flag as cache_bust (severity high) if it ever saw it. But it does not see it. You pasted the pointer.
Day 35: Claude follows a rule you cannot find
Claude refuses to use one of your test fixtures. You grep CLAUDE.md. Nothing. You re-read AGENTS.md. Nothing obvious. The rule is in docs/style-guide.md at line 187, transitively imported through AGENTS.md. The pointer you wrote on Day 0 is now the entry to a five-hop chain you did not author. This is the drift moment most people feel before they have a name for it.
Day 50: someone declines the import approval prompt
A new contributor clones the repo. Claude Code shows the first-time external-imports approval dialog (Anthropic's docs: it lists the imported files; if you decline, the imports stay disabled and the dialog does not appear again). They click decline because they did not know what it was. Claude now sees a 3-token CLAUDE.md with no imported content. Their sessions diverge from yours by 4,400 characters of system prompt. Nothing surfaces this difference at the chat level.
The analyzer blind-spot
ccmd's free analyzer runs entirely in the browser. No filesystem access, no upload, no network calls. That is the contract that makes it safe to paste a config without thinking. The trade-off: pasting only CLAUDE.md when CLAUDE.md is @AGENTS.md scores three tokens, finds nothing, and tells you almost nothing useful. You have to paste every file in the chain.
The fix when you want a single number for the resolved payload: paste each file in the import chain into the analyzer one at a time, write down the token count for each, sum them. That sum is what fires on every turn, not the size of CLAUDE.md on disk.
What you write vs what Claude reads
The import resolver runs once, at session start, before the first user message. It inlines every target it finds along the chain, drops the HTML comments, leaves the code blocks alone, and hands the concatenated result to the model. The block below shows the source side and the loaded side together so the gap is visible.
The same repo, three months apart
CLAUDE.md is one line that says @AGENTS.md. AGENTS.md is eight lines about not using stale Next.js APIs. Total resolved payload: 401 chars, ~101 tokens. You wrote it. You remember every rule.
- CLAUDE.md: 10 chars, 1 line
- AGENTS.md: 401 chars, 8 lines
- Resolved payload: ~101 tokens
- Rules you can name: all of them
Pointer vs resolved payload
The pointer is a constant. The resolved payload is the variable. Treat them as separate audit units.
| Feature | What Claude reads at session start | What you wrote (CLAUDE.md) |
|---|---|---|
| What you re-read | 10 chars / 3 tokens | @AGENTS.md (one line, stable forever) |
| What loads at session start | 401 chars / ~101 tokens on Day 0 | Grows to 6,200+ tokens by Day 35 across the 5-hop import chain |
| What pasting CLAUDE.md into a single-file analyzer scores | 1 / 12 rubric (the pointer matches nothing) | Meaningless. The score belongs to the resolved payload, not the pointer. |
| What HTML comments cost | 0 tokens for the agent | Stripped from Claude's view per Anthropic's docs; preserved on disk. Inside code blocks they are kept. |
| Survives /compact | Yes, root CLAUDE.md is re-read from disk | Yes. Imports re-resolve. If the target grew during the session, the post-compact context is larger than the pre-compact context. |
| What happens on import-approval-dialog decline | Imports stay disabled silently. No future prompt. | CLAUDE.md becomes the literal pointer line and nothing else. A teammate's session and yours now load different rulesets. |
The audit pass that actually catches drift
- Run
/memoryinside Claude Code. It prints every CLAUDE.md, CLAUDE.local.md, and rules file actually loaded for the current session, including imports. If a file you expected is missing, the import declined at the approval prompt or the path resolved wrong. - Paste each file from that list into the homepage analyzer one at a time. Write down the token count and rubric pass count for each. Sum the token counts. That is the per-turn byte cost.
- Look at the file with the highest token count first. That is usually the one nobody has audited in two months. If the first twenty lines contain an ISO date or the words
todayorthis session, move them out: the analyzer's cache_bust check flags this because volatile text near the top defeats prompt caching on every cold start. - For team repos, wrap each managed block in HTML comment fences like the ones in this repo's AGENTS.md (
<!-- BEGIN:name -->and<!-- END:name -->). The fences are stripped from Claude's view but stay on disk, so a future teammate has free bookmarks for which lines belong to which owner. - Document the first-time import approval prompt in your README. New contributors who decline it once will silently miss every imported rule until you change a setting. Adding two lines to the README is cheaper than discovering this in a code review.
Want a second pair of eyes on your import chain?
If your CLAUDE.md is a thin pointer into a stack of nested AGENTS.md and rules files, book a 20-minute review. We will run the full chain through the analyzer with you and tell you which target is growing fastest.
Frequently asked questions
What is import drift between CLAUDE.md and AGENTS.md?
Claude Code lets CLAUDE.md import other files with @path/to/import syntax. Anthropic's memory docs describe the mechanism directly: 'Imported files are expanded and loaded into context at launch alongside the CLAUDE.md that references them.' Import drift is when the pointer (the @-line you wrote in CLAUDE.md) stays the same while the target file grows, gets nested imports of its own, or gets edited by teammates working on a different agent. The rules Claude follows diverge from the file you re-read. You feel it as Claude obeying a rule you cannot find by ctrl-F-ing CLAUDE.md.
Is @AGENTS.md actually the recommended pattern?
Yes, this is not folklore. Anthropic's memory docs show this exact snippet as the recommended pattern for repos that already use AGENTS.md: a CLAUDE.md containing @AGENTS.md on the first line, then a '## Claude Code' section below for Claude-specific instructions. The docs note that a symlink works too on macOS and Linux, but on Windows you need Administrator or Developer Mode for a symlink, so the @-import is the portable choice. The repo this analyzer ships from uses the import form: its CLAUDE.md is literally one line, @AGENTS.md.
How deep does the recursion go?
Five hops. From Anthropic's docs: 'Imported files can recursively import other files, with a maximum depth of five hops.' Relative paths resolve relative to the file containing the import, not the working directory. Absolute paths also work. So a CLAUDE.md that points at AGENTS.md that points at docs/style-guide.md that points at docs/lints.md is fine. Six hops fails. The 5-hop limit is the ceiling on how deep one bad import can drag your context.
Do imports reduce token cost?
No. Same docs, exact quote: 'Splitting into @path imports helps organization but does not reduce context, since imported files load at launch.' Imports are a source-code organization tool, not a cost optimization. The wire payload is identical whether you have one 6,000-token CLAUDE.md or a 3-token CLAUDE.md that imports a 6,000-token AGENTS.md. If anything the imported version is harder to audit because you cannot see the size in one screen.
Why does ccmd's analyzer not follow the import?
Today, deliberately. The analyzer at src/lib/analyzer.ts is a pure function: text in, score out, no filesystem access, no network. That is the contract: pasting a config into the homepage textarea must run entirely in the browser with no upload. Following @-imports would require resolving paths against a repo we do not have. The honest workflow is to paste each file in the chain, score it, sum the token counts, and treat the lines under <!-- BEGIN -->/<!-- END --> fences in AGENTS.md as the audit unit. The paid tier handles this for you by ingesting the repo, but the free analyzer is text in, score out.
What about the first-time import approval dialog?
Anthropic's docs are explicit: 'The first time Claude Code encounters external imports in a project, it shows an approval dialog listing the files. If you decline, the imports stay disabled and the dialog does not appear again.' This is a real footgun for teams. A new contributor clones the repo, opens Claude Code, sees a dialog they do not recognize, clicks decline because they are cautious. From that point their Claude sees a 3-token CLAUDE.md and yours sees a 6,200-token resolved payload. Nothing in the chat surfaces this. The fix is to document the approval step in your README and to verify with /memory which files are loaded.
How do HTML comments interact with imports?
Anthropic strips block-level HTML comments from the version Claude sees: 'Block-level HTML comments in CLAUDE.md files are stripped before the content is injected into Claude's context. Use them to leave notes for human maintainers without spending context tokens on them. Comments inside code blocks are preserved.' This means the <!-- BEGIN:nextjs-agent-rules --> and <!-- END:nextjs-agent-rules --> fences used in many AGENTS.md files cost zero tokens but help humans see where a managed block lives. If you wrap section markers in <!-- --> rather than markdown headers, you keep human-readable bookmarks without paying for them on every turn.
Does /compact help with import drift?
Compaction does not shrink imports. Per Anthropic: 'Project-root CLAUDE.md survives compaction: after /compact, Claude re-reads it from disk and re-injects it into the session.' That re-injection runs the import resolver again. If AGENTS.md grew during the session because a teammate pushed to main and you pulled, the post-compact context is now larger than the pre-compact context. /compact is a conversation-history compactor, not a system-prompt compactor. Nested CLAUDE.md files in subdirectories are not re-injected automatically.
How do I audit my full resolved payload?
Two ways. First, run /memory inside a Claude Code session: it lists every CLAUDE.md, CLAUDE.local.md, and rules file actually loaded for that session, including imports. Second, set the InstructionsLoaded hook (Anthropic documents this) to log which instruction files load and when. Once you have the resolved list, paste each file into ccmd one at a time, write down the token count, sum them. That sum is the byte cost per turn, not the size of CLAUDE.md on disk.
Comments (••)
Leave a comment to see what others are saying.Public and anonymous. No signup.