Generates a CLAUDE.md in which every line names the file it came from, and
fails CI when the repository no longer matches it. No model call, no network, no
subprocess, no tokens — Python standard library only.
Built because I needed deterministic, offline generation for air-gapped work,
where a model-driven /init is not an option and a tool that phones home is not
allowed. It is a personal tool published in case it is useful to someone with
the same constraint.
- You cannot call a model. Air-gapped network, on-prem, a client contract
that forbids sending source to a third party, or a CI runner with no API key.
/initis not available to you; this is. - Your
CLAUDE.mdtells the agent to run a command that no longer exists. A script got renamed, a directory moved, and the file kept saying otherwise.--checkturns that into a failing pull request instead of a wrong answer three months later. - You do not trust a generated context file you did not watch being written. Every line here names the file it came from, so you can audit it in one pass instead of re-deriving the whole thing.
- Your repo is JVM, Go or Rust and the tooling assumes npm. Maven and Gradle
coordinates,
pom.xmlcompiler level, multi-module layouts,go.modandCargo.tomlare all first-class here. - Your build truth lives in CI, not in the manifest. No
engines.node, no lockfile, butsetup-nodepins 20 and arun:step calls npm — that gets read.
If none of those describe you, one of the tools in the next section is probably the better fit, and that section exists to send you there.
Several projects already cover adjacent, overlapping ground, and most of them are further along:
| Project | What it does |
|---|---|
| ctxlint | The most established linter here. Rules include paths/not-found, paths/glob-no-match, commands/script-not-found, commands/make-target-not-found, staleness/stale |
| agnix | 455 rules, GitHub Action, SARIF output, IDE extensions |
| agentsgen | Python, deterministic, marker-based safe updates, --check and a drift-gating Action |
| agents-lint | Generates with agents-lint init, then lints the result |
| rulesync | Deterministic generation across 40 agent formats, with generate --check |
| plinth | Generates AGENTS.md for Java projects, distributed via Maven Central |
| claude-drift · cclint | Audit CLAUDE.md, skills and agents against the codebase |
If you want a maintained linter, use ctxlint or agnix. If you want generation on the JVM, use plinth. Reach for this one only if you specifically need generation that runs with no network, no model, and no dependencies — which is the narrow reason it exists.
One more thing worth knowing before you adopt any of these: Anthropic's own
guidance tells you to keep pitfalls, rationale and conventions out of the
derivable pile, and Claude Code's /doctor offers to delete directory
layouts, dependency lists and architecture overviews from a checked-in
CLAUDE.md. A measurement across 138 repositories and 5,694 PRs
(arXiv:2602.11988) found context files did
not improve task success while raising inference cost by over 20%. Generated
repo overviews are not free, and they may not be worth their context budget.
Deterministic code, not a prompt. It reads your manifests and your filesystem, and if it cannot prove something it does not write it — it tells you what it looked for and did not find.
$ verified-init --report .
detectors: java, ci, repo
VERIFIED (12) read from a declared source
+ java.maven.artifact
Maven project `parking-api`.
evidence: pom.xml -> <artifactId>
+ java.maven.stack
Built on Lombok, Spring Boot Web (MVC), Spring Data JPA.
evidence: pom.xml -> declared dependencies
+ ci.toolchain.java
CI builds on Java 17.
evidence: .github/workflows/build.yml -> java-version
+ java.maven.wrapper
Use the bundled Maven wrapper (`./mvnw`); no system Maven required.
evidence: mvnw present
+ java.version
Targets Java 17.
evidence: pom.xml -> Java level property = 17
+ ci.commands
CI runs, in order: `./mvnw -B package`.
evidence: .github/workflows/build.yml -> run: steps
+ java.maven.testframework
Test stack: Spring Boot Test, Testcontainers.
evidence: pom.xml -> declared dependencies
...
INFERRED (3) matched a convention, not declared
? java.layout.src.test.java
Tests live in `src/test/java/`.
evidence: src/test/java/ exists (standard Maven/Gradle layout, not declared in the build file)
...
12 verified, 3 inferred, 0 not foundReal output from a Spring Boot project, abbreviated at the ... marks.
Note java.version and ci.toolchain.java sitting side by side. The manifest and
the workflow are two different sources, so both are reported. They agree here;
when they disagree, that is the finding.
Note what is not claimed. Built on Spring Boot Web (MVC) is verified because
spring-boot-starter-web is declared in pom.xml. Tests live in src/test/java/
is only inferred, because the directory exists but no build file says that is
where tests come from. The tool will not upgrade the second one to the first.
git clone https://github.com/mgj96/verified-init.gitThen link the skill into Claude Code:
# macOS / Linux
ln -s "$PWD/verified-init/skills/verified-init" ~/.claude/skills/verified-init# Windows
cmd /c mklink /D "%USERPROFILE%\.claude\skills\verified-init" "%CD%\verified-init\skills\verified-init"Copying the directory works too. Ask Claude Code to "write a CLAUDE.md for this
repo" or "check whether our CLAUDE.md is still accurate" and the skill takes
over. The repo also ships .claude-plugin/plugin.json, so it can be installed
as a plugin from a marketplace entry.
The skill's entire job is to run the tool and not paraphrase it. That is the design, not a limitation: a skill made of prompt instructions drifts when the model changes; a skill that shells out to deterministic code does not.
pip install verified-init # or just run it from the cloneverified-init # write/update CLAUDE.md + .verified-init.json
verified-init --report # print the evidence table, write nothing
verified-init --check # exit 1 if the repo drifted from the baseline
verified-init --json # machine-readable claims
verified-init --out AGENTS.md # the cross-agent filename insteadFrom a clone with nothing installed: python -m verified_init --report .
On --out AGENTS.md: agents.md lists 23 agents that read
it — Codex, Cursor, Jules, Aider, goose, Zed, Windsurf, Gemini CLI and others —
and it is stewarded by the Agentic AI Foundation at the Linux Foundation. Claude
Code is not one of them: its own docs open with "Claude Code reads CLAUDE.md,
not AGENTS.md." There is no fallback read. To serve both, generate
AGENTS.md and put a single line in CLAUDE.md:
@AGENTS.mdThe @ import is expanded from the live file at launch, and on Windows it is the
only option — a symlink needs Administrator or Developer Mode.
Exit codes: 0 ok / no drift, 1 drift detected, 2 nothing provable.
| Tier | Meaning |
|---|---|
| plain bullet | Verified — read from a file the project maintains |
_(inferred)_ |
Inferred — a convention matched, nothing declares it |
## Not found |
Checked for and absent |
Most tools only tell you what they found. The third tier separates "this repository has no test directory" from "nobody looked."
src/test/java/ existing does not prove tests live there — it proves a
directory exists, so that is _(inferred)_. <maven.compiler.release>17 in
your pom.xml is a declaration, so that is verified. The distinction is the
whole product.
- run: python -m verified_init --check .Commit .verified-init.json and every pull request that renames a script or
deletes a directory fails until CLAUDE.md is regenerated:
$ verified-init --check .
verified-init: CLAUDE.md has drifted from the repository
- repo.layout.src (no longer true)
~ node.script.test
was: `npm run test` -> `jest`
now: `npm run test` -> `vitest`
Run `verified-init` to regenerate.The generated text lives between two markers. Anything outside them survives regeneration, so hand-written context can live in the same file:
<!-- verified-init:begin -->
...generated, do not edit...
<!-- verified-init:end -->
## Architecture notes
Written by a human. Never touched by the tool.| Detector | Sources |
|---|---|
node |
package.json — description, license, packageManager, engines, scripts, workspaces, type, declared dependencies → stack and test runner (Next.js, React, Vue, VitePress, Astro, Nuxt, Express, NestJS, Electron, Vitest, Jest, Playwright, …); lockfiles, tsconfig.json, .nvmrc |
java |
pom.xml — artifactId, compiler level, modules, declared <artifactId>s → stack (Spring Boot Web/WebFlux/Data JPA/Security, Hibernate, Lombok, MapStruct, Quarkus, Micronaut, JUnit 5, Mockito, Testcontainers, …); build.gradle[.kts] toolchain and coordinates, settings.gradle[.kts], mvnw / gradlew |
misc |
pyproject.toml (description, requires-python, dependencies → Django / FastAPI / Flask / SQLAlchemy / pytest …), requirements.txt, uv.lock / poetry.lock, go.mod (require → Gin / Echo / GORM / Cobra), Cargo.toml ([dependencies] → Axum / Actix / Tokio / Serde), Makefile (.PHONY only) |
ci |
.github/workflows/*.yml — setup-node / setup-java / setup-python / setup-go versions, the run: steps CI actually executes, the package manager it invokes, GitHub Pages deploy target and published path |
repo |
.editorconfig, .gitignore, LICENSE, HANDOVER.md, CONTRIBUTING.md, ARCHITECTURE.md, CONVENTIONS.md, SECURITY.md, other agent instruction files, conventional source and test directories |
CI gets its own detector because it is the version of the build that has to
work. It also answers questions the manifest leaves open: a repo with no
engines.node and no lockfile still pins Node in setup-node and still names
its package manager in a run: step. Without reading workflows, both get
reported as absent when they are merely declared elsewhere — and both claims
stand when the manifest and CI disagree, because that disagreement is worth
seeing.
Stack detection reads the dependency the project declared, so it stays verified. An unrecognised dependency produces no claim at all — a wrong stack label would be worse than a missing one.
- Execute your code. No build, no scripts, no
--dry-run. Safe to run on a repository you have not read, and safe in CI. - Open a socket. Nothing uploaded, nothing fetched.
- Call a model. Zero tokens, on any plan, forever.
- Invent a claim.
Claim()raises if handed no evidence — the rule is enforced by the constructor, not by discipline. - Claim its own output. The file being written is excluded from detection,
so generating it cannot make the next
--checkreport phantom drift. - Contradict itself. A specific claim silences the generic one it answers:
once
src/test/java/is reported, the generic "no test directory found" line is suppressed rather than printed three lines below it.
The same tree yields the same bytes: on any machine, on any day, driven by any agent or model version. Directory listings are sorted, claims are ordered by section then id, and no timestamp is recorded.
python -m unittest discover -s tests -v67 tests, no dependencies. TestDeterminism pins the property down explicitly,
alongside a regression for the self-reference bug the suite caught during
development, and TestSkillPackaging guards the skill directory so the runner
cannot silently stop resolving the package.
- The staleness it guards against is real but not frequent. I measured 22 public repositories with hand-written agent context files: roughly one demonstrably-broken claim per repo, and about half had none at all. Re-running the measurement on the same files disagreed with itself often enough that I would not defend a precise figure. Breakage tracked whether maintainers looked after the file, not how much the code changed.
- The provenance tiers are for humans, not the model. As visible text they spend context budget; as HTML comments they are stripped before injection. They help a reviewer decide whether to trust a line. They do not change agent behaviour.
- Most of what it can prove is derivable anyway. An
@package.jsonimport in yourCLAUDE.mdis always fresh, costs no tool, and never rots. Prefer it wherever it fits; this tool earns its place mainly when you also want the CI drift gate and the tiering. - Not a linter. It does not check prose you wrote. ctxlint and agnix do.
MIT