Skip to main content

extant

Your docs cite commits. Some of those commits no longer exist.

tests license: MIT python: 3.9+ dependencies: none on 3.11+

extant (adj.) - still in existence; surviving.


The problem

A plan document says the work landed in 8f2a91c. A spec says its predecessor merged to develop at abc1234. A status file says a branch is not merged yet.

Then someone squash-merges, or rebases, or force-pushes a cleanup, and those sentences quietly stop being true. Nothing complains, because documentation is just writing. Tests check code. Type checkers check types. Nothing checks whether a commit you wrote down still exists.

This got much worse recently, and not for a subtle reason. Coding agents write plan documents, spec documents and status files at a volume humans never did, and those documents are dense with commit SHAs, branch names and file paths. Then the next agent session reads them back as fact. It cannot tell that a line expired, so it plans around something untrue and hands back confidently wrong work.

Here is one real project, swept with a single command and no configuration. 49 tracked markdown files, 43 findings, counted by rule:

Count Rule
37 dead-sha a commit that no longer resolves
5 bare-dead-sha the same, written without backticks
1 dead-path-pointer a file that moved

42 of 43 were dead commit references, and every located finding sat in the project's plan and spec directories - the documents written during agent sessions. Zero in the README. Zero in CONTRIBUTING. The human-facing docs were fine; the machine-facing ones had rotted where nobody looks.

That is the shape of the problem, and a link checker will not find any of it, because none of these are links.

What it looks like

A plan document written during an earlier session. It says the work merged to main. The commit is real, so nothing looks wrong, but it sits on a branch that was never merged:

$ extant --repo . --sweep

UNREVIEWED - surveyed only, not gated
docs/plans/phase-3.md: line 4: [dead-sha] `8f2a91c` does not resolve in this repo
docs/plans/phase-3.md: line 3: [false-merge-claim] claims work merged to main at `1828109`, but that commit is not an ancestor of main
docs/plans/phase-3.md: line 6: [dead-path-pointer] points at `docs/plans/phase-2.md`, which does not exist

swept 2 markdown file(s): 0 configured (0 finding(s)), 2 unreviewed (3 finding(s))
  2 repository-wide rule(s) ran once (0 finding(s))
  examined: dead-sha 1, stale-live-claim 0, unknown-branch 0, false-merge-claim 1, dead-release-tag 0, dead-path-pointer 1, dead-md-link 0, dead-md-anchor 0, inconsistent-artifact 0, dead-pinned-ref 0, raw-lfs-blob 0, manifest-floor-mismatch 0, dead-line-pointer 0
  NOTE: these rules examined nothing anywhere here - either no document makes such claims, or the pattern does not match how this project writes them: stale-live-claim, unknown-branch, dead-release-tag, dead-md-link, dead-md-anchor, inconsistent-artifact, dead-pinned-ref, raw-lfs-blob, manifest-floor-mismatch, dead-line-pointer
  nothing is configured, so nothing here can fail. Set primary_doc or extra_docs in .extant.toml to gate on a file.

That is real output, not an illustration. The middle line is the one nothing else will give you: the commit exists, and the sentence about it is still false. Answering that means asking git for ancestry, which a text linter has no way to do.

The last three lines count what it looked at, not what it found, and on a repository this small most rules have nothing to bite on. Saying so is the point: a rule reporting 0 examined either has no such claims to read here or does not recognise how this project writes them, and neither is the same as a pass. It matters as much as the findings, and there is a section about why.

Try it in one line

Nothing installed, nothing written into your project, no configuration, no file to name:

$ uvx extant --repo . --sweep

That reads every .md, .markdown, .mdx and .rst file git tracks and tells you what has rotted. On a real project it took one command to surface 42 dead commit references across 49 files.

A sweep cannot fail your build. Findings in files you have not configured are surveyed and reported, never gated on, because some of them will be examples rather than claims. It always prints how many files it looked at, so "nothing found" is distinguishable from "nothing checked".

The full set of modes:

Command For
--sweep the survey. No config, exits 0, shows everything
--deleted-since <ref> claims removed while still false. Always exits 0; pass the merge base in CI
--validate <file> one document, exits 1 on findings
--verify every document .extant.toml names. What the git hooks run

Four ways to install properly are below.


What it covers

Thirteen rules. Every one answers a question git or the filesystem can settle.

Rule Catches
dead-sha "released in commit abc1234" when that commit does not exist
false-merge-claim "merged into develop at abc1234" when that commit is not on develop
stale-live-claim "not merged yet" about something merged last week
unknown-branch "work is on branch X" when git has never seen that name
dead-release-tag "released in v2.1" when that tag is in your repo but reached no integration branch. Also when no such tag exists, if you set release_claims_name_our_tags - the installer does
dead-path-pointer "see the file at this path" when the file moved
dead-md-link [a link](to/a/file.md) whose target is gone
dead-md-anchor a #jump-to-section link with no such heading, in this file or a linked one
inconsistent-artifact two files in your project stating different values for the same thing
dead-pinned-ref an install snippet pinning a version of your project that does not exist
raw-lfs-blob an asset your .gitattributes says is in Git LFS, committed into git as a real binary instead
manifest-floor-mismatch your README says "requires Python 3.8+" while pyproject.toml says >=3.10. Only in a README or install guide, never in a changelog
dead-line-pointer "see core/engine.py:123" when that file has 40 lines. Only for files git tracks, and it never judges what line 123 now contains

Five details that are easy to miss:

More than one integration branch works. A merge claim names the branch it means, so "merged to develop at abc1234" is checked against develop and "merged to main at ..." against main. Nothing to configure. Gitflow teams write both kinds, and against a single configured trunk half of those claims were never examined at all - not judged and found true, simply not read.

Case matters, and only on some machines. Windows and macOS open docs/PLAN.md happily when the file is docs/plan.md. Linux does not. Without the check a document passes on your laptop and fails on the server, or worse, passes everywhere while misleading every Linux reader. Wrong-case paths are reported on every platform.

Examples are left alone. Claims inside fenced code blocks are not read as promises, so a README showing what a claim looks like is not accused of making one.

Install snippets are the exception. dead-pinned-ref is the one rule that reads inside code blocks, because an install snippet is the opposite of an example: it is the block a reader copies verbatim, and a version that does not exist fails for them on first use. It only checks pins whose repo: names your repository, so documenting somebody else's hook is never flagged.

Renames are followed. When a file has moved, you get told where:

line 5: [dead-md-link] links to `docs/old-name.md`, which does not exist;
        git shows it renamed to `docs/new-name.md`

What it deliberately ignores

It never judges whether a number or a date is right. "We had 2238 tests in March" was true in March. It is not wrong now, only old, and there is nothing to check it against.

That restraint is the whole design. A tool that cries wolf gets ignored, and an ignored tool is worse than none. This one reports only what it can prove.

The single exception proves the rule: it will compare two files against each other, because that has a definite answer. See files that contradict each other.


Is this for you?

Strongest fit You run coding agents, and sessions leave behind plan, spec, design or status documents. Those cite commits and branches, rot within days, and are read back as fact by the next session. This is where 42 of the 43 findings above came from.
Good fit You write ADRs, RFCs, postmortems, migration notes or release notes that reference commits, branches or tags. Same shape, written by hand.
Probably yes Your project uses git and has documentation that names files and paths. The link and path rules work on any markdown.
Probably not Your documentation is one paragraph that never mentions a file, a commit, or a version. There is nothing here for it to check, and it will honestly tell you so.

Note what is not required: a status file, a changelog, a particular workflow, or any change to how you work.

Be clear about the trade. This checks claims git can settle. It does not check external URLs, whether a code sample still compiles, or whether a documented flag still exists. If dead external links are your problem, use lychee; if committed credentials are, use gitleaks. They are better at those than this will be, and they cannot answer whether abc1234 is on main.


Requirements

Git, and Python 3.9 or newer. Check both:

$ git --version
$ python --version
If missing Get it
Git https://git-scm.com
Python 3.9+ https://www.python.org

On macOS and Linux you may need python3 rather than python.

The tool itself has no third-party dependencies. Standard library and git, nothing else.

3.9 and 3.10 are supported for the enterprise distributions that ship them: RHEL 9 and Debian 11 are on 3.9, Ubuntu 22.04 LTS on 3.10. The one thing newer than 3.9 anywhere in the tool is tomllib, which joined the standard library in 3.11. Below that the tool runs on its defaults with no parser at all, and reading a .extant.toml wants pip install tomli - the same parser under its original name, so a config file is read identically on every version.


Install

Four ways in. The table picks one for you.

If you Use You get
already use pre-commit A validation on every commit, one config block
want to look before committing to anything B the validator as a command, nothing written into your project
use Claude Code and want the slash command C the plugin, plus everything in D
want the hooks and agent instructions in-repo D the full install, no plugin manager

Options A and B give you the validator only. The git hooks and the /extant command have to live inside the repository they check, so C and D are the routes that install those. A route quietly delivering half the tool would be worse than no route.

Option A: pre-commit

If you already use pre-commit, this is the whole setup. Add to .pre-commit-config.yaml:

repos:
  - repo: https://github.com/scooter-sensei/extant
    rev: v0.22.0
    hooks:
      - id: extant

Then pre-commit install, and create a .extant.toml naming the document to check:

primary_doc = "README.md"
extra_docs = ["CONTRIBUTING.md"]

It runs on every commit whether or not you touched documentation. That is deliberate: merging a branch can make a sentence false without editing a line of prose, and that is the case this exists for.

A second hook id, extant-annotate, emits GitHub Actions annotations instead of plain text.

Option B: pip, pipx or uv

Installs the validator as a command. Nothing is written into your project, so this is the quickest way to see what the tool says before deciding anything:

$ uvx extant --repo . --validate README.md

uvx downloads and runs it without installing. To keep it around:

$ pipx install extant
$ extant --repo . --validate README.md

Once a .extant.toml names your documents, --verify checks all of them at once and is what you want thereafter. Before that file exists it will report no such document: NEXT_SESSION.md, because that is the default primary_doc and your project has no reason to have one.

pip install extant works too, though a tool you run against many projects is usually happier in its own environment.

This is the VALIDATOR only. The git hooks and the /extant command have to live inside the repository they check, so use Option C or D to install those. Point it at a document with --validate, or create a .extant.toml naming one, exactly as in Option A.

Option C: Claude Code

Two lines:

/plugin marketplace add scooter-sensei/extant
/plugin install extant@extant

Then open the project you want to protect and ask:

Set up extant in this project.

It inspects your repository, works out the settings, and reports what it found and how confident it is about each value.

Option D: by hand

Works with or without Claude Code.

1. Get the files. Download the ZIP from the repository (green Code button, then Download ZIP), or clone it:

$ git clone https://github.com/scooter-sensei/extant

2. Preview. This changes nothing:

$ python extant/plugin/skills/extant/install.py --repo /path/to/your/project --dry-run

Read what it prints. If it looks wrong, stop, and nothing has happened.

3. Do it for real. The same command without --dry-run, plus a preset:

$ python extant/plugin/skills/extant/install.py --repo /path/to/your/project --preset readme

4. Turn on the automatic checks.

$ cd /path/to/your/project
$ sh tools/hooks/install

They run after a commit is already recorded, print what they found, and never stop you doing anything.

What actually lands in your repo

Worth being precise about, because "installs a Claude thing" is the usual assumption and it is wrong:

File Needs
tools/extant_collect.py, tools/extant_config.py Python 3.9+ and git
tools/hooks/* a POSIX sh
.extant.toml nothing
.agents/skills/extant/SKILL.md nothing. Agent Skills is an open standard, and this is its cross-tool path: read by Codex, Gemini CLI, Copilot, Cursor and Kimi as well as Claude
.claude/commands/extant.md Claude Code

Only the last line is tool-specific, and it is the only one written conditionally. It appears if your repo already has a .claude/ directory or a CLAUDE.md; otherwise the installer says it skipped it and names the flag. --claude-command and --no-claude-command decide it outright.

The validator is standard-library Python and git subprocesses. There is no agent framework in it, nothing to adapt for one, and no runtime dependency on any assistant: options A and B do not write an agent file at all.

Presets

A preset picks the documents and the shape, so you are not deriving configuration before you have seen the tool work once.

Preset For
readme any project. Your README and CONTRIBUTING. Nothing else needed.
node the same, plus package.json and CHANGELOG.md version agreement
python the same, with pyproject.toml
rust the same, with Cargo.toml
enterprise long-lived projects. Also SECURITY.md, SUPPORT.md, UPGRADING.md, MIGRATION.md
ml data and model projects. Also MODEL_CARD.md and DATA_CARD.md, and that pyproject.toml and environment.yml pin the same Python
legacy-web older web apps. Also INSTALL.md, DEPLOY.md, UPGRADING.md, and that .nvmrc and package.json agree on Node
go a Go module. Also SECURITY.md, and that go.mod and your Dockerfile build with the same Go
jvm Gradle or Maven. Also UPGRADING.md, MIGRATION.md, and gradle.properties against the changelog
k8s Helm charts. Also RUNBOOK.md, and Chart.yaml's chart version against the changelog
terraform Terraform modules. Also UPGRADING.md and MIGRATION.md, beside the terraform-docs README
docker images and compose. Also DEPLOY.md, RUNBOOK.md, OPERATIONS.md
monorepo a workspace root. Also ARCHITECTURE.md, docs/README.md, and the root version
mobile iOS and Android. Also RELEASE_NOTES.md, PRIVACY.md, and one marketing version across both stores
unity a Unity project. Checks the editor-version badge against ProjectSettings/ProjectVersion.txt
godot a Godot project. Checks the version in doc/setup_instructions.md against project.godot
agent files AI agents read as fact: AGENTS.md, CLAUDE.md, GEMINI.md, copilot-instructions.md
status a running status file with dated entries

agent is worth a note. AGENTS.md is the cross-tool standard for instructing AI coding agents, and CLAUDE.md and GEMINI.md are the vendor-native equivalents. Those files are mostly paths and commands, and an agent cannot tell an expired line from a current one. A dead path in a README wastes somebody's afternoon; a dead path in an AGENTS.md becomes confidently wrong work.

Where two files look like they should agree and do not, no check is written. Helm documents appVersion as unrelated to the chart version. Terraform's required_version is a constraint (">= 1.5.0") while .terraform-version is an exact pin. Go's go directive is a minimum and toolchain is exact. A mobile versionCode is a build counter, not the version anyone sees. Every one of those pairs would report a correct repository, so docker and terraform carry no cross-check at all, and the rest compare only what genuinely must match.

The last three exist because long-lived documentation rots in a particular place. An enterprise project rarely has a stale README. It has a MIGRATION.md last edited in 2021 pointing at three files that have since moved.

Two rules govern every preset:

A preset never overrides something measured. Detection reads your repository for the trunk name, branch naming and commit conventions. A template would be guessing at those, and a preset that quietly replaced your real branch name would be the copied-configuration problem this tool was built around. Choosing the document is your call, which is why passing a preset settles that one.

Checks whose files are absent are skipped and reported. A preset never opens by complaining about a file you do not have. A tool whose first act is a false positive has taught a lesson that is very hard to unteach.

Works with any coding agent

The checker is Python, git hooks and a pre-commit entry, so it never depended on a particular assistant. What did was one line deciding where the agent-facing instructions went.

Setup now writes them to .agents/skills/extant/SKILL.md, the location the Agent Skills open standard defines. One file, read by OpenAI Codex, Gemini CLI, GitHub Copilot, Cursor, Kimi Code, Claude Code and the twenty-odd other tools that adopted it.

It is rendered for your repository rather than copied, so it names your document and your paths, and it carries the discipline that matters: read the denominator, never make a document pass by deleting the claim.

Claude Code additionally gets /extant, a slash command for the end-to-end workflow. Both are rendered from the same observations, so they cannot end up describing different documents.

What lands in your project

tools/ the checker
.extant.toml settings, written by reading your project
git hooks the automatic checks. They report, they never block
.agents/skills/extant/SKILL.md instructions any agent reads, rendered for your project
/extant command Claude Code only, for the full workflow

Features

Every check reports its denominator

The summary line counts what was examined, not what was found:

checked STATUS.md: dead-sha 36, stale-live-claim 1, false-merge-claim 2,
  dead-path-pointer 5

If one of those reads 0, that check found nothing to look at, which usually means a setting is wrong rather than that your file is spotless. Any rule that examined nothing is named on a NOTE: line.

--sweep reports the same counts summed over every file it read, so a rule that is inert across a whole repository says so on the first run rather than after you have started trusting it.

This is the single most important line in the output. "Found no problems" and "did not look" print identically otherwise, and only one of them is good news.

It can prove its own checks fire

The worry above deserves more than a warning, so there is a command:

$ python tools/extant_collect.py --selftest

It takes a real claim from your document, breaks it on purpose, confirms the matching rule notices, and puts everything back. Nothing is written.

  dead-sha             FIRED
  stale-live-claim     FIRED
  dead-path-pointer    FIRED
  dead-release-tag     NO PROBE       nothing to corrupt

  3 fired, 1 had nothing to corrupt, 0 stayed silent

A check that stays silent after you break something it should catch is broken. This runs in CI here on every change, so the tool is not merely tested, it is watched failing.

Findings inside pull requests

$ python tools/extant_collect.py --verify --format=github
$ python tools/extant_collect.py --verify --format=sarif

github emits GitHub Actions annotations, so each problem is highlighted on its own line in the pull request diff rather than buried in a log nobody opens. Add the flag to your existing step. Nothing else is needed, and it requires no extra permissions.

sarif emits the standard format code-scanning tools exchange, as pure JSON on stdout, so it pipes straight to a file. To get results into GitHub's Security tab:

      - name: Check the documentation
        run: python tools/extant_collect.py --verify --format=sarif > extant.sarif
        continue-on-error: true

      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: extant.sarif

That upload needs permissions: security-events: write. This repository uses the annotation route instead, so treat the SARIF upload as a starting point rather than something proven here.

Severity says whether the finding can fail your build. A finding in a document you configured arrives as error, because it decides the exit code. A finding from --sweep in a file you have not reviewed arrives as note, because a sweep exits 0 and cannot gate. Each result also carries properties.gates, so a policy can filter on it without parsing severities. Before 0.20.0 everything was published as error, which contradicted the exit code the same command returned.

The alert shows the claim, not a line number. Each result carries the cited line as a snippet, and the column range points at the token itself - the SHA, the tag, the path - so the alert underlines abc1234 rather than pointing vaguely at line 12.

The run states what it examined. properties.examined carries the per-rule count, and the invocation's notifications repeat it in prose along with a warning naming any rule that examined nothing. Zero results with a full denominator means the documentation is clean; zero results with zeros everywhere means nothing was checked, and those two must never look alike.

A sweep upload and a verify upload use different automationDetails.id values (extant/sweep, extant/verify), so uploading both does not silently replace one with the other.

Files that contradict each other

The tool never asks whether a number is correct. It will ask whether two files disagree, which has a definite answer needing only the filesystem.

This project shipped the bug that motivated it: three manifests said the version was 0.1.0 while the changelog said 0.3.0, and everyone installing was told they were getting the first release.

[extant.consistency.version]
"package.json" = '"version":\s*"([^"]+)"'
"CHANGELOG.md" = '^## (\d+\.\d+\.\d+)'

Each line names a file and how to find the value inside it. Disagree, and you are told which file says what.

Four shapes are refused when the settings load, because each would produce a check that can never fail: a check listing one file, a pattern with no capture group, a pattern with more than one, and the same file named twice under different spellings. A pattern that matches nothing is reported rather than passing quietly.

Corrections as a patch, never an edit

When a file has been renamed, the tool already tells you where it went. It can also write the correction:

$ python tools/extant_collect.py --verify --suggest-fixes

This prints a patch and changes nothing. Read it, and apply it if you agree:

$ python tools/extant_collect.py --verify --suggest-fixes | git apply -

It only offers corrections for files git actually recorded as renamed. If a file is simply gone it says nothing, because guessing where it went means writing something that might not be true, and that is the one thing this refuses to do. Its authority rests on checking claims and never authoring them.

Search across the archive

If you keep a status document, old entries get archived so the live file stays short. That helps until you need to remember why a decision was made.

$ python tools/extant_collect.py --search "checkout"

It searches the live document and the archive together and returns whole entries rather than matching lines. That is the only reason it beats grep: a decision lives in a dated entry with its reasoning, and a line from the middle tells you a phrase exists rather than what was decided.

Every other document you keep

Your primary document is not the only one that rots. A CLAUDE.md, an AGENTS.md, a CONTRIBUTING.md all make the same kinds of claim:

extra_docs = ["CLAUDE.md", "AGENTS.md", "CONTRIBUTING.md"]

They get every rule that does not depend on dated entries, which is most of them.

Documents that are input to a test

Some markdown is not a promise to a reader. A renderer's test fixtures link to files that are missing on purpose, to exercise the error path. Nothing git or the filesystem can answer separates that from a real broken link, so it is not a rule and never will be:

exclude_paths = ["testdata", "**/test/fixtures/**"]

Empty by default, because a skip-list that ships with entries is one nobody audits. Patterns are gitignore-shaped: * stops at a separator, ** spans them, and a bare name matches a segment at any depth, so testdata finds it wherever it lives.

The sweep prints what it removed, per pattern, and names any pattern that matched nothing. That second half is not decoration. A skip-list fails silently in both directions, and the same two patterns are right for one project and dead in another:

  excluded 3 of 1016 tracked file(s) via 3 exclude_paths pattern(s)
        0 **/e2e/fixtures/**
        0 **/test/fixtures/**
        3 testdata
  matched nothing, so they exclude nothing and may be stale: **/e2e/fixtures/**, **/test/fixtures/**

Excluding a document that primary_doc or extra_docs also names is refused rather than resolved. One setting says gate on this file and the other says never read it, and quietly dropping a document you asked to gate on is the direction that costs you something.

Adopting on a project that already has years of prose

Point this at a ten-year-old repository and the first run reports everything at once. That is accurate and useless: CI goes red, nobody has a week for decade-old documentation, and the tool comes back out.

Record what is already there, once:

$ python tools/extant_collect.py --verify --write-baseline
recorded 47 finding(s) in .extant-baseline.json

Then every run checks new claims and ignores the recorded ones:

$ python tools/extant_collect.py --verify --baseline
1 new finding(s), 47 suppressed by .extant-baseline.json

New documentation is held to the standard from day one; the backlog waits.

It always says how much it is hiding. "No findings" and "no new findings, 47 suppressed" are different facts, and a baseline that concealed its own size would be exactly the failure this tool exists to surface, reintroduced by one of its own features.

Nothing is ever recorded implicitly. --write-baseline is a separate, deliberate command. A baseline that rewrote itself on every run would forgive whatever it had just found, and the check would decay to nothing while still reporting success.

An amnesty must not outlive its finding:

$ python tools/extant_collect.py --verify --baseline-check
baseline: 47 entr(y/ies), 45 still occur, 2 do not
  STALE  docs/setup.md: [dead-md-link] links to `scripts/old.sh`, ...

Those two were fixed. Their entries now forgive something that is not there, so they are reported and should be deleted. A baseline nobody prunes becomes a permanent exemption, and it is itself a stale claim.

The file is JSON, and deliberately readable: each entry carries the path, the rule and the message alongside its fingerprint. It is a list of things your project has agreed to leave broken for now, so it belongs in review like any other change.

The optional wrong-branch guard

One check is off unless you ask for it, because unlike everything else here it can refuse to save your work:

$ sh tools/hooks/install --with-trunk-guard

What it solves. Git lets you keep several versions of a project going at once. If you lose track of which one you are on, you can commit to the wrong branch. The work is not lost, but it is filed in the wrong place, and finding out later is unpleasant.

Why it is off by default. It has nothing to do with your documentation. You came for a tool that checks whether your writing is still true, and a tool that suddenly refuses to save your work for an unrelated reason is a tool people uninstall.

Should you? If you work on one branch, no. If you juggle several, or have ever pushed work and found it on the wrong branch, yes. It is also worth it if AI assistants commit in your project, because they are particularly good at losing track of which branch they are on.

Remove it by deleting pre-commit from your project's .git/hooks, or bypass it once with git commit --no-verify.

Any language

The checking does not care what your project is written in. Only the optional "run the tests" step does, and you name the command:

suite_command = ["npm", "test"]
suite_command = ["cargo", "test"]

Fast enough to leave on

A 16,000-line document validates in under a second. A 100,000-line document in about four. The rules that query git batch their questions, so a document naming two thousand distinct commits asks git once per commit rather than twice per claim.


Read the settings it writes

Setup works out your project's habits by reading it, and prints each value with how confident it is:

  trunk         [derived ] origin/HEAD -> main
  branch_token  [derived ] 128 branches sampled
  entry_prefix  [guessed ] highest-scoring header '## Release'
  merge_claim   [unknown ] no matching phrasing found

Anything it could not work out is left switched off rather than guessed.

This is the part that matters. If a setting is wrong, that rule quietly does nothing and you get a tool reporting "all clear" forever without looking at anything. It is the one way this fails badly. Read what setup prints, and see porting.md to fill gaps.


What it cannot do

  • Only catches sentences that can be proven wrong. "Nearly finished" is beyond it, on purpose.
  • Cannot judge whether a summary is a good summary.
  • Expects consistent headings for dated entries. A heading that does not match is skipped.
  • Reads the branch a merge claim NAMES, plus the conventional integration branches that exist. A branch with an unconventional name that no claim names is not treated as an integration branch.
  • Checks links to your own files, not to the web. Nothing here touches the network. Checking external links would make a passing run depend on someone else's uptime and rate limits, turning a definite answer into a coin flip. Issue and pull request links go unchecked for the same reason.
  • Does not complain about a branch merged and then deleted. That is normal tidying, and the branch is still named in the merge commit. Only a name git has never seen is reported.
  • Settings load next to the tool, not next to the folder you point at. Correct once installed in your project, wrong if you run it from elsewhere. It says so on stderr rather than quietly using the wrong ones.
  • It cannot tell a corrected claim from a deleted one. Removing the sentence it complained about makes a document pass. The /extant workflow works around this by reporting first-run findings even after fixing them, so a deletion is visible in both the report and the diff. The checker alone cannot know.
  • A pattern you write yourself can hang it. Settings take regular expressions, and a badly shaped one can take effectively forever on some text. The fix is to simplify the pattern, but the failure looks like a freeze rather than a complaint.

If something goes wrong

"python is not recognised" on Windows, or "command not found" on macOS and Linux: try python3, or reinstall Python with the "add to PATH" box ticked.

The checks never run. Confirm you did step 4, then run --verify by hand.

It flags something you think is fine. Do not silence it by deleting the sentence. Either that sentence really is out of date, or a setting needs adjusting. Both are worth knowing.

Everything reports 0. A setting is almost certainly wrong. See Read the settings it writes.

Deeper documentation

All under plugin/skills/extant/:

File What is in it
references/porting.md Getting the settings right for your project. Read first.
references/config.md Every setting explained.
references/design.md Why each rule works as it does, and the real mistake behind each decision.
SKILL.md What Claude reads.
What is in this repository
.claude-plugin/marketplace.json   lets Claude Code install this
.pre-commit-hooks.yaml            lets pre-commit install this
plugin/
  .claude-plugin/plugin.json      plugin details
  skills/extant/
    SKILL.md                      what Claude reads
    install.py, detect.py         the setup program
    payload/                      what gets copied into your project
    references/                   the deeper documentation
tests/                            the unit suite
tests/harnesses/                  slow audits, run by hand
NEXT_SESSION.md                   this project's own status document

That last one is not decoration. This project runs its own tool on its own document, on every change, in CI. If it stopped working, this repository would be the first to find out.

tests/harnesses/ holds the audits pytest cannot perform: one that breaks the code on purpose to see whether any test notices, one that installs into unlike real projects, one that tries to abuse the tool, one that sweeps a corpus of outside repositories and compares the result against a recorded baseline, and ones that measure speed and load. Each prints its own counts when it runs; none are repeated here, because a number in prose is not checked by anything. Between them they found every defect fixed in 0.3.0, and the stale assertion caught in the audit before 0.6.0. The unit suite found none of them, because the unit suite was the thing being audited.

The bug fixed in 0.6.1 was found by none of them. It took installing the published release and using it as a stranger would, which is its own lesson.

The one idea worth taking elsewhere

When you write a rule that checks something, look at your real data first and shape the rule to fit it, rather than writing what the rule ought to look like and hoping. Done the second way, the file-path rule produced 23 false alarms on the first project it ran against. Done the first way it produced none, and still caught the real problem.

And always report how many things you looked at. "Found no problems" and "did not look" print exactly the same otherwise.


MIT licensed. Use it freely, including commercially. See LICENSE.

Metadata

Release files for extant 0.22.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for extant 0.22.0
File Size Uploaded
extant-0.22.0.tar.gz 296.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for extant 0.22.0
File Interpreter ABI Platform
extant-0.22.0-py3-none-any.whl Python 3 none any Details

Total release size: 419.9 kB

Release files / extant-0.22.0.tar.gz

Download URL extant-0.22.0.tar.gz
Size 296.2 kB
Tags Source
SHA-256 checksum
How to use checksums
3e00e7576e471d39dcb9d4acf32076fedc61f648485a9a0a3c0e815210b34012
BLAKE2b-256 checksum
How to use checksums
497036b1ba41fb3e60f2956931df2899223d5b704ae72b342a7280f76f17ab9d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 9, 2026.

Transparency log

Release files / extant-0.22.0-py3-none-any.whl

Download URL extant-0.22.0-py3-none-any.whl
Size 123.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a3eb8133a4f8c646289de729bb9a59db7fa20242e5154b171f062e9fab5cf7f6
BLAKE2b-256 checksum
How to use checksums
b7dc656df4f783af8b9c96903733b8e8060dbaccb12f96e5d2ea02521fe49c9b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 9, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page