Repository navigation
Releases: alifurkangokce/driftlint
Release list
v0.20.0 — which file actually loads
Which file actually loads. The agent ecosystem moved this month, and some of what it moved made this tool's own README wrong — so this release starts there.
Claude Code reads AGENTS.md now — conditionally. Since v2.1.277 it reads AGENTS.md, but by default only when there is no CLAUDE.md, .claude/CLAUDE.md or CLAUDE.local.md at or above it. That turns a missing feature into a silent one. Four new silent-config cases, each from the documented behaviour:
- An AGENTS.md Claude never reads, because a CLAUDE.md sits at or above it and nothing imports it. Reported only when AGENTS.md holds instructions CLAUDE.md doesn't — two identical copies lose Claude nothing. Several hidden files under one CLAUDE.md are reported once, on that CLAUDE.md.
- The
CLAUDE.local.mdtrap. A gitignored personal file counts as a CLAUDE.md, so creating one switches AGENTS.md off on your machine alone. Your teammates' Claude reads it; yours doesn't. - "Read AGENTS.md" written in words. Claude only sees the file if it decides to open it.
@AGENTS.mdloads it at launch, every session. - The SessionStart workaround now double-loads (
load-budget, info). The hook that printed AGENTS.md was the standard fix for years; with native support, it adds a second copy — reported only when Claude would read AGENTS.md itself.
Templates, examples, skill bundles (.agents/ — which Claude documents it never reads), container payloads, and nested AGENTS.md files a CLAUDE.md deliberately names by path are left alone. Each of those was a false positive on a real repository first.
dead-import — @path imports, which were being skipped entirely. A missing target is an error (with the single real candidate as a --fix); an import nested past the four hops Claude Code follows is a warning, because that file never loads. Parsed as documented: relative to the importing file, backslash-escaped spaces, nothing inside code spans, fences or comments. @alice, @types/node, @1.2.3 and email addresses are not path claims.
dead-glob — path-scoped rules whose globs match no file, so they never load: Claude paths: (rules and skills), Cursor globs: (unless alwaysApply), Copilot applyTo:, Kiro fileMatchPattern (with inclusion: fileMatch), Windsurf/Devin globs: (with trigger: glob). The matcher abstains on anything it can't read with confidence — negation, extglobs, globs into unwalked build output — and collapses into one note when most of a repo's rules are dead, which means it ships rules for projects it generates. Plus a sharper case under silent-config: a Claude rule scoped with globs: or applyTo: is unscoped — Claude reads only paths:, ignores the rest without an error, and loads the rule for every file.
New surfaces. .devin/rules/ and .windsurf/rules/ (Windsurf is Devin Desktop now) with the documented 12,000-character per-file limit; .kiro/steering/ and its #[[file:…]] live references; .cursorrules; .cursor/commands/; .github/prompts/*.prompt.md; .junie/guidelines.md; .continue/rules/; .aiassistant/rules/.
CI. --format github (workflow-command annotations — inline on a PR, on private repos, without GitHub Advanced Security), --format gitlab (Code Quality report), --format azure (##vso[task.logissue]). The Action now annotates PRs by default; sarif-file still routes to code scanning instead.
Validation. Run over 61 real repositories from the August corpus, against 0.19: no existing finding changed. On the first 29, the new rules produced 23 findings; hand-labelling found 7 false positives in four classes (templates and examples, skill bundles, a container payload, an on-demand pointer), each now excluded and pinned by a test. On 32 repositories not looked at while tuning, 11 findings: 10 clearly real, 1 borderline. The clearest: a six-line CLAUDE.md saying documentation had moved, above a 127-line AGENTS.md titled Engineering Protocol — scope: entire repository, which Claude Code never read.
Plugin, fixed for Anthropic's directory. Review rejected it for running npx @alifurkangokce/driftlint without an exact version: code that runs by name can change after it was reviewed. The plugin's commands now run the exact version they ship with, the unpinned github: fallback is gone, and a packaging test fails the build when any pin drifts from package.json — the same treatment the MCP engine pin and the plugin manifest version got earlier. The plugin also moved to plugins/driftlint/: with the whole repository as the plugin, claude plugin validate --strict now rejects this repo's own contributor CLAUDE.md as plugin context. Install commands are unchanged.
The Action runs the version it is tagged at. It used to run @latest from npm whatever ref you pinned, so uses: alifurkangokce/driftlint@v0.17.0 quietly ran the newest scanner. It now runs exactly the version in that ref.
This README was wrong, and is fixed. It said Claude Code reads only CLAUDE.md, that the request was "not planned", that nobody else verifies references against the tree (Claude Code's /doctor prompt-audit now does, interactively — see the comparison for the honest split), that the next milestone was an LLM pass shipped in August, and it pinned pre-commit to v0.7.0.
- 162-test suite.
v0.19.0 — nested projects
Nested projects, reported in #27 by @kims6305-bjk with a file:line diagnosis, a minimal repro and a count: 39 of their 44 findings were this one shape.
A context file inside a nested project writes paths from that project's root. sub/.agents/skills/myskill/SKILL.md says eval/comparison/test-sim/, meaning sub/eval/comparison/test-sim/. References were resolved against the scan root and the file's own directory, so it came back dead — while the did-you-mean hint cheerfully printed the correct location it had just refused to check.
- The owning project is now a resolution base. It is derived from the path itself: the parent of the agent-config directory the file sits under (
sub/.agents/…→sub). The report suggested walking up to the nearest.git, but their own stated case — vendored mirrors under.research/— has no.gitto find, so the marker had to be something every one of these files already carries. - Consequence worth naming. Resolving more references pushed some files below the
foreign-contextratio, so references belonging to the described repo started arriving as individual errors instead of one collapsed warning. A file that resolves anything against its own nested project is a nested project's document; what it still can't resolve is that project's surroundings, not this repo's drift. Those findings are now warnings, and say so in the hint. - Verified on a real 14-file workspace: error count unchanged at 7, two expert packages went from a vague "7 of 8 don't resolve" to completely clean, and the rest became specific instead of collapsed.
- 132-test suite.
v0.18.0 — Windows & write-path correctness
Windows support and a correctness pass over the code that writes to your files. Almost all of it is @saferbayram's work in #25 — twenty files, twenty-one new tests, and a CI matrix that finally covers the platform I can't test on (closes #10).
The one worth spelling out, because it affected the only feature that edits your files. Given a test:unit script and this line:
For faster tests, run `npm run test` before pushing.
driftlint --fix produced:
For faster test:units, run `npm run test` before pushing.
It rewrote an English word in the prose and left the broken command alone — indexOf("test") matched inside "tests" four characters earlier. Fixes now carry the source column from extraction through to the edit, resolve every range against the original line before anything is written, apply right-to-left so one replacement can't shift another's offsets, and abstain entirely when there's no column and the text is ambiguous. A skipped fix is cheap; a corrupted instruction file is not.
- Windows: memory entry paths consistently use
/; auto-memory discovery recognizes drive letters and backslashes as well as older directory encodings. Tests run on Node 20 without shell glob expansion, with Ubuntu/Windows CI coverage on Node 20 and 22. - Twins no longer report drift caused only by LF/CRLF conversion. Sync preserves the target's line endings and leaves an already-current mirror untouched.
- Reviewed Memory keeps both approved facts when proposal filenames collide, including collisions introduced between proposing and approving.
- Command fixes and reviewdog suggestions target the script's source column. Multiple fixes on one line keep their original positions; ambiguous or stale replacements are skipped.
- Explicit
cdinstructions no longer pass merely because a script or make target exists at the repository root. Separate inline examples keep separate working directories. ignorepatterns also suppress silent-config and dead-config-ref checks on excluded files, while those files remain available as path evidence.
v0.17.0 — false positives
A false-positive pass, from an unusually thorough review by a user who ran driftlint over a personal agent workspace of 139 findings and then checked, by hand, whether any of them were real. None were. Every item below is one of the reasons.
- Paths written with the repo's own directory name now resolve. A
CLAUDE.mdinsideAjanlarim/that points atAjanlarim/hafiza/was reported dead, because the reference was resolved against the root a second time. Humans write the project name into the path all the time; the checker now strips a leading self-prefix before giving up. The same shortcut applies to markdown link targets. - Hooks in a nested project resolve against that project's root.
$CLAUDE_PROJECT_DIRinsub/project/.claude/settings.jsonpoints atsub/project, not the scanned root, so every script reference in a monorepo's inner project read as adead-config-ref. - Small plugin skills collapse instead of flooding. The "describes another repo" heuristic needed five references before it would fire; installed marketplace skills are small, so a workspace with thirty of them produced ~120 individual
dead-patherrors about a project the user never had. A skill, sub-agent or command where nothing resolves and at least three references were tried now collapses to a single warning, the same as a large foreign file. - A skill directory named
buildis no longer invisible.build,out,bin,dist,targetandobjare skipped as build output everywhere in a repo — including, until now, inside.claude/, where they are ordinary skill and command names. Build output stays ignored; agent config trees are walked in full. --fixrefuses to write outside the scanned root. The path came from a finding, and every finding came from our own walk, so this was not reachable in practice — but a fixer that resolves a relative path and writes without checking containment is a fixer one crafted context file away from being a problem. It now verifies the resolved target is inside the root and reports anything it skipped.
Also new: SECURITY.md, which states plainly what this tool reads, what it writes, and the one flag that sends anything off your machine.
- 105-test suite; six regression tests, one per item above.
v0.16.0 — instruction surfaces
Prompted by a critique on r/codex that turned out to be right: the set of files this tool opened was narrower than the set agents actually load.
- Nested directories are no longer invisible. The patterns were single-segment, so
.claude/agents/backend/api.mdwas never opened while.claude/agents/api.mdwas. Rule, skill, sub-agent and command directories now match at any depth. - New surfaces:
.claude/rules/**,.codex/rules/**/*.rules,.github/instructions/**/*.instructions.md,.github/agents/*.agent.md,.cursor/agents/**,.gemini/agents/**,AGENTS.override.md, andskills/**/SKILL.mdunder.codex,.gemini,.githuband.agents— the Agent Skills path several agents read from each other. Every existing check applies to them unchanged (#23). --user-scope(#24): Codex and Claude Code load an instruction file from your home directory that counts toward the same 32 KB concatenation budget, so a repo-only total understates it. Opt-in, off by default, size only — the content of~/.codex/AGENTS.mdnever enters a finding.
Validating this on real checkouts surfaced a genuine case: four AGENTS.md files totalling 65.6 KB with 33.6 KB truncated away, and no single file anywhere near the limit.
98 tests.
v0.15.1 — the 32KB limit applies to the concatenated set
Reported on r/ClaudeCode, and the reporter was right.
load-budgetmeasured the wrong thing. Codex truncates the concatenated instruction set at 32 KB, not each file separately — so a rootAGENTS.mdand two nested ones at 12 KB apiece all pass individually and still lose the tail together. The check now sums the AGENTS.md files it can see, reports one combined finding naming the largest contributors, and skips it when a single file is already oversized (that one has its own finding).- The hint says what the scan can't see: only the files on the path to the working directory are concatenated, and a user-level
~/.codex/AGENTS.mdcounts toward the same budget (--user-scopein 0.16.0 folds that in). - Verified while answering the same thread: a symlinked
CLAUDE.md → AGENTS.md— the long-established fix for the two-file problem — produces zero findings.
93 tests.
v0.15.0 — precision & crash pass (community source review)
Everything in this release came from @cemililik, who read the source and filed eleven issues with file:line diagnoses — one with the fix attached (#12).
Crashes and bad writes
- An unreadable
Makefiletook the whole scan down with an unhandled exception. Thepackage.jsonbranch four lines up was already guarded (#14). --fixwrote replacements through a string pattern, so$$,$&,$`` and$'` in a path spliced surrounding text into your context file instead of the text we showed you (#13)..github/was never walked, leavingcopilot-instructions.mdunreachable since the day it shipped — documented, typed, wired up, and dead (#11).
False positives
yarn audit,yarn outdated,yarn workspace …reported as missing scripts, failing a yarn repo on its first run (#18)LICENSE,CHANGELOG,CODEOWNERSpermanently exempt from link and path checks (#16)- bullets inside fenced code blocks counted as instructions (#19)
.cursor/rules/README.mdflagged as a rule that never loads — it's documentation, and the error madenpx driftlintexit 1 on a clean repo (#20)- reasons written as a markdown loose list never counted (#21)
docs/x.md?plain=1reported as missing (#22)
Correctness
twin-driftnamedtestwhen the script istest:unit(#15)--llmapplied theignoreglobs to the linted files but not to the evidence pool, so an excluded path was still grepped and pasted into the prompt (#17)
91 tests. The two highest-severity regressions were verified to fail without their fix before being committed.
v0.14.1 — first-run polish
Installed it globally on a clean machine and used it like a first-time user. Three things were rough:
- The freshness score published a percentage from a handful of references. A repo with three checked references could swing from 100% to 0% because one file moved — and
--badge-jsonwould write a red0%badge off that. Below five references the report now omits the line and the badge saysn/a. untracked-contextfired in a repo with no commits yet. Every file is untracked in somebody's first five minutes; that's not drift, it's a new project. The rule now waits for history.- The personal-file hint suggested
CLAUDE.local.mdfor files that have no such variant —.opencode/knowledge/*.md,.windsurfrules. Those now point at theignoreconfig instead.
77-test suite.
v0.14.0 — reference-style links (first community contribution)
The first outside contribution landed — thank you @LunaMeerkats (#9, closing #8).
- Reference-style markdown links are now verified:
[text][label]and collapsed[label][]usages resolve against their definitions, and the finding is reported at the definition line — the one place an author can fix it. Definitions are collected in a first pass so a usage may precede its definition, the first definition wins (CommonMark), inline links are masked before reference scanning, and future-artifact suppression spans every usage line. Shortcut references ([label]) stay out of scope on purpose: without a full markdown parser they collide with ordinary bracketed prose. - Precision fix from dogfooding:
ALL_CAPSlink targets —[ticket](TFS_LINK)— are fill-in markers, not claims that a file exists.
74-test suite, zero runtime dependencies, no telemetry.
npx @alifurkangokce/driftlintv0.13.0 — silent config: valid JSON, missing file
Schema validators tell you the JSON is well-formed. They can't tell you the file it points at is gone, or that the tool never reads that file at all.
dead-config-ref— hook commands, MCP servercommand/args, plugin and marketplace manifest paths, and skillallowed-toolsscripts are resolved against the actual tree.${CLAUDE_PROJECT_DIR},${CLAUDE_PLUGIN_ROOT}and${CLAUDE_SKILL_DIR}are expanded; plugin manifests resolve against the plugin root; did-you-mean hints come from the repo index. Remote servers (npx,uvx,docker), machine paths,~, unresolvable variables and globs are skipped, andpermissions.allowentries warn whiledeny/askrules are left alone.silent-config— a plain.mdunder.cursor/rulesnever loads (Cursor requires.mdc); a bare.mdwhere<name>/SKILL.mdbelongs is never picked up.skill-budgetgained the official hard cut:description+when_to_usepast 1,536 characters is truncated in the skill listing, so the tail never reaches the model.- Skills are now discovered under
.cursor/skills/as well as.claude/skills/— Agent Skills is a cross-tool standard.
Validated on six real local repos plus this one: zero false positives from the new rules. 70-test suite, zero runtime dependencies.
npx @alifurkangokce/driftlint