diff --git a/_posts/2026-07-10-claude-code-wasnt-reading-this-sites-agents-md.md b/_posts/2026-07-10-claude-code-wasnt-reading-this-sites-agents-md.md new file mode 100644 index 000000000..5550c7e31 --- /dev/null +++ b/_posts/2026-07-10-claude-code-wasnt-reading-this-sites-agents-md.md @@ -0,0 +1,38 @@ +--- +title: Claude Code wasn't reading this site's AGENTS.md +slug: claude-code-wasnt-reading-this-sites-agents-md +date: 2026-07-10 14:30:00 -0400 +excerpt: The repo behind this site carries about 1,200 lines of agent guidance + in AGENTS.md. Claude Code had never read a word of it, because my CLAUDE.md + pointed there with a markdown link instead of an import. +published: false +--- +The repository behind this site carries about 1,200 lines of guidance for coding agents in [`AGENTS.md`](https://github.com/Adam-S-Daniel/adamdaniel.ai/blob/main/AGENTS.md) — test-driven-development rules, e2e invariants that took real debugging pain to learn ("never bypass the UI in a UI test"), architecture notes, a warning not to re-vendor the gem-delivered admin machinery. As far as I can tell, Claude Code never read a word of it. + +The mechanism is mundane. Claude Code loads `CLAUDE.md`, not `AGENTS.md` — [its docs say so flatly](https://code.claude.com/docs/en/memory). The supported way to bridge the two is an import line, `@AGENTS.md`, which the memory loader expands at session launch. My `CLAUDE.md` instead said: + +```markdown +See [AGENTS.md](./AGENTS.md) for instructions, and make any updates/additions there. +``` + +That's a markdown link. It reads perfectly to a human, and the memory loader treats it as inert text. The agent got one sentence of context telling it where the instructions live, and — in my testing, at least — essentially never spent a tool call to go read them. Everything below that link was invisible. + +## How I noticed + +I'd read somewhere that Claude doesn't pick up files referenced from CLAUDE.md in some contexts, and since every repo I own uses the referenced-file pattern, I went to verify it empirically — planting unique magic tokens in AGENTS.md files across a matrix of layouts, then asking headless Claude Code sessions (with all file-reading tools disabled) to quote the token. If the model can say the word, the loader injected the file; if it can't, nothing did. The full matrix — imports, symlinks, subagents, the Agent SDK, and one embarrassing false positive where the model cheated by reading the file with its own tools — is in [the companion post](/testing-claude-code-agents-md-bridge/). + +The `@AGENTS.md` import itself passed everywhere I could test it. The failure wasn't the mechanism I was worried about — it was sitting in this repo the whole time, one layer up: a bridge file that never bridged. + +## Why nothing caught it + +I run a small guidance layer, [`_agent-guidance`](https://github.com/Adam-S-Daniel/_agent-guidance), that syncs a managed `AGENTS.md` into every repo I own and adds the `CLAUDE.md` bridge where it's missing. It has a deliberate safety rule: never modify an existing `CLAUDE.md` — clobbering someone's hand-written file would be worse than warning. So for this repo it printed `WARN: CLAUDE.md exists but does not import @AGENTS.md` into a CI log nobody reads, every sync, and moved on. The nightly drift dashboard checks whether each repo's `AGENTS.md` matches the managed content — but never looks at `CLAUDE.md` at all. + +Both behaviors are individually defensible, and together they add up to a silent failure that persisted for months: the guidance was perfectly up to date, and perfectly unread. + +The fixes are filed: [a one-line bridge for this repo](https://github.com/Adam-S-Daniel/adamdaniel.ai/issues/2545), and [dashboard/monitoring changes in `_agent-guidance`](https://github.com/Adam-S-Daniel/_agent-guidance/issues/17) so a present-but-not-importing `CLAUDE.md` shows up as a red cell rather than a log line, backed by [a behavioral canary eval](https://github.com/Adam-S-Daniel/skills-evals/issues/5) so an upstream regression can't quietly undo the bridge either. + +## The takeaway + +Agent guidance is plumbing, and plumbing needs a pressure test. It's easy to reason "the file is there, the sync is green, therefore the agent sees it" — I did, for months. The only check that actually settles the question is behavioral: put a token in the file that the model could only know by having it in context, and ask. It costs one headless API call per repo, and I'm guessing I'm not the only person whose `CLAUDE.md` says something friendly and does nothing. + +Not urgent for anyone else's stack, obviously — but if your CLAUDE.md "references" your AGENTS.md, it's worth thirty seconds to check whether that reference starts with `@`. diff --git a/_posts/2026-07-10-testing-claude-code-agents-md-bridge.md b/_posts/2026-07-10-testing-claude-code-agents-md-bridge.md new file mode 100644 index 000000000..fae437240 --- /dev/null +++ b/_posts/2026-07-10-testing-claude-code-agents-md-bridge.md @@ -0,0 +1,64 @@ +--- +title: "Symlink or @import? Testing how Claude Code actually loads AGENTS.md" +slug: testing-claude-code-agents-md-bridge +date: 2026-07-10 15:00:00 -0400 +excerpt: I planted magic tokens in AGENTS.md files across sixteen repo layouts + and asked headless Claude Code sessions to quote them. Results — including + the round-one false positive where the model cheated by reading the file + with its own tools. +published: false +--- +`AGENTS.md` has quietly become the cross-tool standard for agent instructions — [agents.md](https://agents.md) lists twenty-three tools, Codex, Jules, Devin, Cursor, Copilot's coding agent, Aider, goose, opencode, and Zed among them. Claude Code is conspicuously not on that list. It reads `CLAUDE.md`, [says so in its docs](https://code.claude.com/docs/en/memory), and the feature request for native AGENTS.md support ([anthropics/claude-code#6235](https://github.com/anthropics/claude-code/issues/6235), opened August 2025, thousands of 👍) is still open — the changelog through v2.1.206 has zero AGENTS.md entries. I checked byte-by-byte, and then grepped the shipped binary for good measure: the string appears exactly twice, both inside the `/init` prompt. + +So if you keep guidance in `AGENTS.md` (I sync a managed one into every repo I own), you need a bridge. There are two candidates: + +1. a `CLAUDE.md` containing the import line `@AGENTS.md`, or +2. a symlink: `ln -s AGENTS.md CLAUDE.md`. + +I'd read that Claude doesn't pick up referenced files in some contexts, which — given that all my repos use option 1 — seemed worth actually testing rather than worrying about. Sixteen layouts later, here's what holds up. + +## Method, and the trap + +Each test directory plants a unique token ("The magic word is FLUMMOX-7291") somewhere in the layout, and a headless session gets asked: *what's the magic word?* If the loader injected the file, the model can answer; if not, it can't. + +Except that's not quite true, and my first run produced a beautiful false positive. In a directory with **only** an `AGENTS.md` — no CLAUDE.md, no bridge — the model answered correctly. Native AGENTS.md support, undocumented? No: headless Claude Code still has its Read and Glob tools, and the model, asked about project instructions it didn't have, simply went and read the file like a sensible agent. The control — rerunning with every file-reading tool disallowed — flipped the answer to NONE. + +Agents make lousy lab rats; they cheat. Every result below is from the tools-disabled runs (Claude Code v2.1.206, Agent SDK v0.3.206, Linux). + +## Results + +| Layout | Guidance visible? | +|---|---| +| `CLAUDE.md` = `@AGENTS.md` | ✅ | +| `CLAUDE.md` → symlink to AGENTS.md | ✅ | +| Only `AGENTS.md`, no CLAUDE.md | ❌ no native support, no fallback | +| `CLAUDE.md` without an import, AGENTS.md alongside | ❌ AGENTS.md invisible | +| `@AGENTS.md` inside a fenced code block | ❌ (documented: imports skip code blocks) | +| Import chains (CLAUDE.md → AGENTS.md → deeper file) | ✅ up to 4 hops | +| Subdirectory CLAUDE.md + import, loaded lazily on file access | ✅ | +| Subdirectory AGENTS.md alone | ❌ | +| `CLAUDE.local.md` with an import | ✅ | +| `--add-dir` directory's CLAUDE.md (headless, even with the documented env flag) | ❌ | +| Agent SDK, default options | ✅ imports expanded | +| Agent SDK, `settingSources: []` | ❌ nothing loads | +| Subagent, `general-purpose` type | ✅ full memory incl. imports | +| Subagent, `Explore` type | ❌ by design | +| `@file` reference in a slash command | ✅ expanded | +| `@file` reference in a SKILL.md body | ❌ not an import — the agent is expected to Read it | + +Two things I'd half-remembered turned out to be true *once*, and stale now. The Agent SDK really did ship a breaking change (v0.1.0) that stopped loading CLAUDE.md by default — it was reverted; current SDKs load it again unless you opt out. And through this spring there were open issues reporting that subagents didn't receive CLAUDE.md at all (v2.1.62–v2.1.152 era); on current builds the [documented behavior](https://code.claude.com/docs/en/sub-agents) — everything except Explore and Plan gets the full memory hierarchy — is what I measured. I'm guessing one of those two is the "Claude ignores referenced files" claim I ran into. In fairness, it was hard to check without running the experiment: this stuff has changed three times in a year. + +## So: symlink or import? + +The symlink works — on my Linux box. But every place the two approaches differ, the import wins: + +- **Windows.** Git only materializes real symlinks with Developer Mode or admin rights plus `core.symlinks=true`; otherwise the checkout contains a plain text file whose content is the string `AGENTS.md`. Which is not an import, so the bridge silently degrades to exactly the broken state the symlink was meant to prevent. Anthropic's own memory doc recommends the import over the symlink on Windows. +- **Anything that reads files over the GitHub API** gets a symlink's target *path*, not its content. +- **Track record.** The Claude Code changelog is a small museum of symlink fixes — sandbox startup failures when `.claude/skills` is a symlink, rules not loading via symlinked paths, symlinked-settings hot-reload bugs. And there's an open bug filed against this exact pattern: with `ln -s AGENTS.md CLAUDE.md`, current versions read the file fine but [refuse to Edit or Write through the symlink](https://github.com/anthropics/claude-code/issues/66559) — so `/init` and any agent-driven memory update break. Imports have no comparable history. +- And the contexts where imports genuinely don't help — Explore subagents, `settingSources: []`, claude.ai chat — don't read `CLAUDE.md` **at all**. A symlink is equally invisible there. There is, as far as I can tell, no surface that reads CLAUDE.md but refuses to expand its imports. + +So the import bridge stays. What actually needed fixing was operational, not mechanical: my sync tool refuses (correctly) to edit a hand-written CLAUDE.md and only warns in CI logs, my drift dashboard never checked the bridge file at all — and one of my repos, [including the one that builds this site](/claude-code-wasnt-reading-this-sites-agents-md/), had a CLAUDE.md that *linked* to AGENTS.md instead of importing it. Months of well-maintained guidance, never loaded. + +The implementation issues that came out of this — bridge checks in the drift report, a magic-token canary eval so a CLI regression can't quietly undo the bridge, and prompt-inlining guidance for Explore-type subagents that never see memory — are in [`_agent-guidance#17`](https://github.com/Adam-S-Daniel/_agent-guidance/issues/17) and [`skills-evals#5`](https://github.com/Adam-S-Daniel/skills-evals/issues/5). + +If you take one thing from this: don't audit your agent plumbing by reading it. Plant a token, disable the tools, and ask.