Sheet 1 of 1 Revision 0.5.0 Node 24+ No config, no API key, no network MIT

driftwatch

Find the parts of your CLAUDE.md, AGENTS.md and skills that are no longer true.

knip finds dead code. driftwatch finds dead context.

npx @abr4xas/driftwatch

Field booklive, not a recording

~/your-repo driftwatch 0.5.0

							

58 of 66 real repositories produce zero false positives.

See the survey

Your document is a map. The repository is the ground.

An out-of-date README confuses a person, who notices and asks. An out-of-date CLAUDE.md hands the agent a false premise that it executes with confidence: it runs a script that no longer exists, edits a file that moved, follows an abandoned convention.

The cost is not confusion, it is incorrect work with no error signal.

Linters check your code. Nothing checks the document you wrote about your code.

Unlike code, these files have no compiler, no tests, no linter. They are the one part of the repo where lying has no mechanical consequence.

SpecimenAGENTS.md, with revision overprint
1# Contributing
2
3 The entry point is src/util/date.ts. src/helpers/date.ts
4
5 See src/cli.ts for the flag table. no single candidate, not rewritten
6
7Run pnpm test before pushing.

Struck in overprint: a claim the ground contradicts. Written beneath it: the correction, only when exactly one candidate clears 0.8 confidence.


Five checks. Each answers a question the document already claimed to know.

Turning one off

  • --skip link/broken turns that check off everywhere
  • --only path keeps only that family, and a prefix matches, so --only path and --only path/missing both work
  • checks: in the config file makes either permanent, or demotes a check to a warning instead of silencing it
  • a directive in the document silences one line rather than a whole check, and the syntax is with the first run

A selection that leaves no check enabled is refused.

What it never does

  • judge whether the content is good. It checks whether a claim is true, and those are different questions with different answers
  • lint your Markdown. Style, formatting and spelling belong to a tool you already have
  • rewrite prose. --fix touches the mechanically verifiable fragment of a line and nothing around it
  • put a model on the main path. That is out of scope by design, not unimplemented

One false positive costs more than ten false negatives.

A linter that shouts too much gets uninstalled on first use. So driftwatch stays silent when it is unsure, and every discard rule in the extractor carries a comment naming the false positive it prevents. This is the measurement, over context files their authors wrote without knowing driftwatch exists.

341 context files66 repositories pinned to a commitnine languages

checked, no drift says something untrue One mark is one file, one run is one repository. Point at a run, or tab in and use the arrow keys.

A lit mark is a context file driftwatch reported anything at all about: 29 of the 341, carrying 37 findings between them, and 27 of those are true drift the maintainers had not noticed. The four figures below count only the false ones.

Repositories with zero false positives
58/ 66
87.9%, and the project's own bar is 90%. Said here rather than quietly: the bar is not met on the whole corpus
Same, over the validation group alone
29/ 32
90.6%. Added after the heuristics were frozen, and never used to derive one
Worst single repository
2false positives
No repo in the corpus sees more
Edits --fix would apply across all 66
6
and every one is correct, checked by hand at the level of the edit and not only of the finding

What it refuses to report

  • a path inside a URL, a glob, or a placeholder: <name>, {{path}}, $VAR, [id], EventNameHere
  • a bare word with no slash, or a single-segment directory like feat/
  • a path whose shape exists somewhere in the repo, because the document is probably writing relative to a directory its prose already named
  • anything git ignores, or that lands in a generated output directory
  • a path the line already hedges: "such as", "if exists", "(optional)", "is generated by", or a line opening with "Create ..."
  • a path inside another assistant's configuration root, in a repo that does not use that assistant

How it was measured

Twenty-six rounds, every finding classified by hand, including the four rounds where a condition broke and the two where the corpus caught a regression that reading the diff would not have.

Eleven false-positive classes have been found and closed, each with a fixture and a test naming the repository and line it came from. Every fix was measured over the whole corpus before being kept, and not one of them has ever removed a true positive.

The corpus does not run in CI, because the verdict requires a person. CI can detect that a snapshot changed; it cannot rule on whether the change is an improvement, and that ruling is the measurement.

The whole argument, with the rounds where they broke →


Most first runs find nothing. That is an answer, not a failure.

Fifty of the sixty-six repositories in the corpus report nothing at all. A tool built to stay quiet when it is unsure is quiet most of the time, so a first run has three possible endings and only one of them reports drift.

  1.  14 files · no drift · 210ms

    It read your documents and they were true

    Fourteen context files, every claim in them checked against the repository. Exit code 0.

    The file count is the proof that it looked at all. --json carries summary.claims beside it: how many individual assertions were verified. That is the only thing that tells nothing to find apart from nothing found. In CI, where nobody reads the output of a green run, it is the only number worth asserting on.

  2.  0 files · no drift · 12ms

    It found nothing to read

    Zero files does not mean your documents are clean. It means driftwatch found none to read, and it is the one result worth checking.

    Discovery runs off git ls-files, so .gitignore is respected for free, and anything it hides is invisible to driftwatch too. A repository with no .git falls back to a glob walk.

    agents-md
    AGENTS.md, at any depth
    claude-md
    CLAUDE.md, CLAUDE.local.md
    skill
    <root>/skills/**/SKILL.md, where <root> is .claude, .agents, .cursor, .codex, .github or .opencode. There is no single one, and .claude/ is not the busiest: npx skills add writes to .agents/ by default.
    subagent
    .claude/agents/*.md
    command
    .claude/commands/**/*.md
    cursor-rule
    .cursorrules, .cursor/rules/**/*.mdc
    copilot
    .github/copilot-instructions.md

    Anything else, name it yourself: driftwatch docs/ AGENTS.md audits only those paths, and a sources: key in the config makes it permanent.

  3. AGENTS.md
      ✗ 3  src/util/date.ts  path does not exist  → src/helpers/date.ts?
      ✗ 5  src/cli.ts        path does not exist
    
    1 file · 2 problems (2 errors) · 59ms
    1 fixable with --fix

    It found drift

    A claim in your own repository that an agent would have believed, on the line that makes it. Exit code 1.

    file:line is clickable in a modern terminal, so the fastest next move is to open the line and read what it claims. The arrow is a suggestion, and --fix --dry-run prints the exact rewrite before anything is written to disk.

If it is wrong about one

It will happen: six of the corpus's twenty-six findings are false. The recovery is a comment in the document itself, next to the claim it covers.

<!-- driftwatch-ignore-next-line -->
`src/planned/feature.ts` does not exist yet, it is the plan

<!-- driftwatch-ignore path/missing -->
<!-- driftwatch-ignore-file -->

With no id it silences every check on that line; with an id, only that one. Directives apply to findings rather than to claims, so an id you misspell silences nothing and the finding still shows up.

Then stop running it by hand

The first run tells you whether your documents are true today. Keeping them true is the part that has to happen on every change, which is what CI is for.

With --fix the exit code describes what is left, so a repository whose only error was autofixable exits 0. Code 2 is reserved for driftwatch itself failing (bad config, a path that does not exist, a crash) and never means drift.

Put it on the diff instead →


An autofix never guesses.

A wrong fix is not noise, it is a document that now points confidently at the wrong file, and the next agent will believe it.

So --fix applies a correction only when there is exactly one candidate above 0.8 confidence. It replaces the claim and nothing around it. And it refuses outright to rewrite a relative path in a document that is not at the repo root.

The whole list of what it will not touch, and why →

Terminal

$ driftwatch --fix --dry-run   # what it would change
$ driftwatch --fix             # change it

Try both in the field book above. It prints the real output.


Every stale claim becomes an annotation on the diff, on the line that makes it.

.github/workflows/ci.yml

- uses: actions/checkout@v7
- uses: abr4xas/driftwatch@v0.5.0

fail-on-drift: false makes it advisory. sarif: true writes a file for Code Scanning.

What the step accepts

  • paths Paths to audit, space separated. Empty means the whole repository.
  • only / skip Run only these checks, or everything except them. A prefix works: path.
  • format pretty | json | github | sarif. Annotations are the reason to run this in CI.
  • sarif Write a SARIF file and expose its path as an output.
  • fail-on-drift Whether drift fails the job, or only annotates it.
  • working-directory Where to run. Defaults to the workspace root.

Four formats, one set of findings

pretty
for a person at a terminal. The only one with a human as its consumer, and the only one that may change freely between versions.
--json
a stable contract. Breaking changes only on a major; version is what you branch on, and it is 1.
--github
one native annotation per finding and nothing else. A run with no findings emits nothing at all, not even a blank line.
--sarif
SARIF 2.1.0 for Code Scanning, carrying one rule per check that actually ran.

stdout is the report, stderr is everything else, and the exit code does not depend on the format.