AGENTS.md and CLAUDE.md rot silently. You write "setup is npm run build", "the entry point is src/index.ts", someone renames a script, and the instruction quietly becomes a lie. The agent believes it and breaks things.
So I built reflint: a linter that ignores wording and style entirely and checks one thing — do the references still resolve? Back-quoted paths against the disk, markdown link targets against the repo, npm run <script> against package.json. Zero dependencies, language-agnostic, exits 1 in CI.
That is the launch paragraph. The rest of this post is what happened after I published it.
I ran reflint against 118 public repositories that have an AGENTS.md or CLAUDE.md, in both the before and after versions. What it found was mostly my own breakage.
1. A flag I had advertised in a blog post had never done anything
reflint has --code-blocks. The help text says it makes the tool "also check paths inside fenced code blocks", which implies the default skips them. Months earlier I had written a post saying, in so many words: turn this on and it checks bare paths inside code blocks.
Across 118 repositories, the number where the flag changed the result was zero.
The cause was wiring. Only the fourth of four scanners consulted the in-a-fence state. The npm run scanner, the back-quoted reference scanner and the markdown link scanner never looked at it — they read fenced content by default, so turning the flag on could not add anything. The flag was dead.
I moved all three inside the fence check. On the same 118 repositories, 10 now change their result depending on the flag.
2. pnpm -r build was reported as "no script named -r"
AGENTS.md:3 `pnpm -r` — no script "-r" in package.json
AGENTS.md:4 `pnpm --filter` — no script "--filter" in package.json
AGENTS.md:5 `pnpm -w` — no script "-w" in package.json
The character class in the script-name regex included -, so the flag right after pnpm was read as the script name. 16 of the 39 script-kind findings were this. In any repo using pnpm workspaces, correct documentation turned red.
Those forms run per-workspace-package, so the root package.json is not the right thing to check them against. reflint now skips the whole invocation.
3. One misplaced fence and it checked nothing, then said everything was fine
Fence state was tracked with the obvious inFence = !inFence toggle. That breaks on any document that demonstrates a fence by wrapping it in a longer fence: the marker count goes odd and everything below sticks to "inside a fence" forever.
One of the 118 — a 3,461-line AGENTS.md — did exactly this. Headings and prose were being treated as fenced content.
Combined with fix #1, this is the dangerous one. Fenced content is skipped by default now, so from the misplaced marker down, reflint inspects nothing, exits 0, and prints reflint: all references resolve.
For a linter that is the worst possible failure, and I had already been here once. In 0.9.2, the CLI installed via npm i -g or npx exited 0 without running at all, because the entry-point check compared process.argv[1] against import.meta.url and those two never match through a symlink. "Found no problems" and "never ran" are indistinguishable — to the user and to CI.
Closing is now CommonMark's rule: same character, at least as long as the opener, no info string.
If you go quiet, say why you went quiet
Every one of those fixes creates a region that is not checked by default. If the count drops, the user cannot tell whether things got fixed or whether the tool stopped looking. That is the same failure again, one level up.
So reflint now always reports what it held back:
reflint: 2 broken references (3 inside code blocks, not checked — run with --code-blocks)
reflint: all references resolve (1 inside code blocks, not checked — run with --code-blocks)
The JSON output gained a skipped field. HTML comments are not counted — they are disabled text, not content that --code-blocks would bring back.
The numbers
Across the 118 repositories: 208 findings became 185. All 23 that disappeared were noise, and zero new findings appeared.
For honesty's sake, two other fixes in the same release — HTML comments and indented code blocks — had zero occurrences in this corpus. They are reproducible and I closed them, but they moved no numbers. The three above did.
A different shape, same lesson: only the people who wrote it correctly got warned
The release after the audit fixed something else. Agent instruction files are full of prohibitions:
Never run npm run release; releases are performed by a human.
→ `npm run release` — no script "release" in package.json
Never read or execute `scripts/deprecated.sh`; it was removed after the migration.
→ reference `scripts/deprecated.sh` does not exist
Both of those are correctly written documents. If you explicitly tell the agent not to use something that was removed, then of course it does not exist — that is the entire point of the sentence. And yet the person who documented it was the only one getting flagged.
Prohibition lines are no longer scanned as references, and that test runs outside fences only, so a # never edit this comment inside a block does not silently disqualify the real references under it.
I have now hit this exact shape in three of my linters. Text rules see mention, not execution. The more dangerous a thing is, the more likely a careful author is to name it in order to declare, forbid, or scope it — so false positives concentrate on the people writing the best documentation.
The part I actually want to leave here
Through all of it, the test suite was green before any of these fixes.
There were tests for --code-blocks. They asserted that enabling it made the fourth scanner check fenced paths. They did not assert that the other three were skipping them. There were tests for fence tracking. None of them fed in a document with an odd marker count.
The tests I write only cover the ways I imagined it breaking. 118 repositories of other people's AGENTS.md cover the ways I did not. Until I ran it against them, I believed my tool worked.
Try it
npx @hyuga/reflint # auto-detects AGENTS.md / llms.txt / CLAUDE.md
npx @hyuga/reflint --code-blocks # also check inside fenced blocks (this works now)
npx @hyuga/reflint --since main # only files changed against a git ref
npx @hyuga/reflint --format json
In CI, which is the point:
name: reflint
on: [push, pull_request]
jobs:
reflint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hyuga611/reflint@v1
Findings show up as inline PR annotations. One correction I had to make to my own README: GitHub does not block a merge on a failing check until you mark that check required. Until then it is a red X that anyone can click past.
Repo: https://github.com/hyuga611/reflint — the changelog is written up per release, including the ones where I was wrong.
Top comments (0)