Does your version label name exactly one version of your code? A read-only, zero-dependency detector for version-label drift in git repositories.
- ✓Open-source license (Apache-2.0)
- ✓Actively maintained (<30d)
- ✓Clear description
- ✓Topics declared
- ✓Documented (README)
claude mcp add closure-drift -- uvx --from{
"mcpServers": {
"closure-drift": {
"command": "uvx",
"args": ["--from"]
}
}
}MCP Servers overview
# closure_drift
**Does `v1.4.2` mean one thing in your repository?**
```
$ git clone -q https://github.com/psf/requests && cd requests
$ closure-drift --compare v2.16.0 v2.16.1
A v2.16.0 (commit bcd0e170ac20)
label 2.16.0
closure 3d2886b87c2a7b94 17 file(s)
B v2.16.1 (commit f687e9f3d8b0)
label 2.16.0
closure 696b71ed6023f563 18 file(s)
changed requests/__init__.py
only in B requests/packages.py
==============================================================
DIFFERS UNDER ONE LABEL: both declare 2.16.0, and the code differs
in 2 path(s). If both were published, that label names two things.
```
Check it without this tool: `git show v2.16.1:requests/__version__.py` says `2.16.0`. closure_drift reads
the git history, not a package index: it shows what the tags contain, not which commit a published package
was built from.
A version is a string a human edits. When two releases share a version and differ in code, one
address names two artefacts — and nothing notices, because the version is all that was recorded.
One command checks it. Read-only, one file, no dependencies, no network.
## Run it
```bash
curl -sO https://raw.githubusercontent.com/luizfnsilva/closure_drift/v1.1.3/closure_drift.py
python3 closure_drift.py # inside any git repository
```
or `pipx run --spec git+https://github.com/luizfnsilva/closure_drift@v1.1.3 closure-drift`.
Needs Python 3.9+ and git. To see the three possible answers first: `python3 examples/demo.py`, or read [`docs/DEMOS.md`](docs/DEMOS.md).
## Check before you tag
`--would-tag` answers: *if I tag this commit now, does its version already name different code?*
```yaml
# GitHub Actions
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: luizfnsilva/closure_drift@v1.1.3
```
```yaml
# pre-commit, on git push
- repo: https://github.com/luizfnsilva/closure_drift
rev: v1.1.3
hooks: [{ id: closure-drift-would-tag }]
```
Other pipelines: [`docs/CI.md`](docs/CI.md).
## For AI agents
[](https://glama.ai/mcp/servers/luizfnsilva/closure_drift)
An agent that tags and publishes can ask the same question first, through a Model Context Protocol
(MCP) server, standard library only, with a `would_tag` tool:
```bash
claude mcp add closure-drift -- uvx --from git+https://github.com/luizfnsilva/closure_drift closure-drift-mcp
```
Other clients and a rule to paste into `AGENTS.md`, `CLAUDE.md` or `.cursorrules`:
[`integrations/`](integrations/). The server makes the check available; the agent has to call it
and respect the answer. The barrier is CI.
## The answer
| verdict | exit | |
|---|---|---|
| `clean` | 0 | every label names one closure, over at least as many points compared as not |
| `drift` | 1 | a label names more than one closure |
| any other verdict ([list](docs/REPORT.md)) | 2 | not enough to tell; the report says why |
| refusal | 2 | cause on stderr |
Exit 0 means `clean` and nothing else. No input we tried produces a traceback, and no failure exits 1.
- **label** — the version your project declares at each tag: in its build files, in the module
they point to, or the tag itself when the version is derived from it
- **closure** — SHA-256 over `(path, git object id)` of the files that determine your output
The report also says how many tags it could **not** compare.
## Options
| | |
|---|---|
| `--would-tag` | would tagging this commit reuse a label? |
| `--tags 'py-*'` | only these tags are publication points (monorepos) |
| `--at commits` | you publish at every commit |
| `--closure 'src/**'` | which files determine your output |
| `--strict` | `clean` only if every point was compared |
| `--explain LABEL` | which paths differ under a label in drift: `--compare` over every tag of that label, found by the scan |
| `--compare A B` | two tags side by side |
| `--version-file`, `--version-regex` | where the label is |
| `--published FILE` | only tags of versions you published: a local list, one per line. `--tags` picks by name; use this when a tag that matches was never released |
| `--label-equality version` | `1.0`, `1.0.0` and `v1` are one label |
| `--modes` | a file's mode (executable bit, symlink) is part of the closure |
| `--component NAME` | one component of a monorepo (below) |
| `--json`, `--badge`, `--diagnose` | report ([contract](docs/REPORT.md)), README badge, bug-report block |
Settings can be committed in `.closure-drift.json`. Flags override it, except beside `components`,
where they need `--component`; a broken file is refused.
A monorepo declares its components, and gets one verdict each:
```json
{"components": {
"python": {"tags": ["py-*"], "version_file": "py/pyproject.toml", "closure": ["py/**"]},
"rust": {"tags": ["rs-*"], "version_file": "rs/Cargo.toml", "closure": ["rs/**"]}
}}
```
The answer is `drift` if any component is in drift, `clean` if all are clean, otherwise
`incomplete`. Components are declared, never guessed. A gate in such a repository passes
`--component NAME` to `--would-tag`.
## What it found
**The 100 most-downloaded PyPI projects with a public repository, at the defaults**, measured with a build of 0.10.0 that prints
the same reports as the release — rule and method
fixed before the first run; no repository tuned ([full table and every collision](tools/study/STUDY.md)):
| | repositories |
|---|---|
| `clean` | 59 |
| `drift` | 29 |
| `incomplete` — fewer tags compared than not | 4 |
| no version label found, or inconclusive | 8 |
A version label names two different code states in **29 of the 88** decided, 29 of all 100. Each
of the 87 labels is in `collisions.tsv`, one line per tag involved, reproducible by hand:
```
$ closure-drift --compare v2.16.0 v2.16.1 # in psf/requests
DIFFERS UNDER ONE LABEL: both declare 2.16.0, and the code differs in 2 path(s).
```
A collision is not a verdict on a project. The usual causes are a tag created without bumping the
version, branch markers such as `7.x`, and tag families in a monorepo. A first run, kept in the
repository, decided only 63 of 100 — it read the version from one file chosen at HEAD — and that
is why this release finds the label where each project keeps it.
Six reference repositories, measured with 1.0.0 at every tag ([`tools/reference/`](tools/reference/)):
| Repository | verdict | labels in drift | tags compared |
|---|---|---|---|
| `pallets/click` | drift | 3 of 66 | 71 of 71 |
| `psf/requests` | drift | 3 of 140 | 145 of 162 |
| `pypa/packaging` | clean | 0 of 50 | 50 of 53 |
| `encode/httpx` | clean | 0 of 88 | 88 of 88 |
| `impress/impress.js` | drift | 2 of 4 | 6 of 15 |
| `lodash/lodash` | drift | 60 of 107 | 209 of 440 |
Two causes: a release tagged without bumping the version (`impress.js`), and tag families sharing
one version file (`lodash`). Neither project is badly run. `--would-tag` addresses the first,
`--tags` or components the second.
## Limits
- It never runs your code and attests nothing. `clean` is about addressing, not reproducibility.
- A change of file mode alone is not seen, unless `--modes`.
- Never in the closure: folders `test/`, `tests/`, `spec/`, `docs/`, `vendor/`, `node_modules/`,
`.git/`; files named `*_test.*`, `*.test.*`, `*.md`.
- The default closure globs are a guess. Pass `--closure`.
- Labels are compared as written: `1.0` and `1.0.0` are two labels, unless `--label-equality version`.
- It reads git, not a package index: a tag never released counts, unless `--published` lists what
was. A tag left off the list is left out of the analysis, not shown unpublished.
- How the label is found is a set of rules, not a build: [`docs/LABELS.md`](docs/LABELS.md).
- In a gate, pass `--version-file`, or `--published`: the rules can still read the wrong file when
one tag happens to agree with it.
Every failure found so far: [`docs/FAILURES.md`](docs/FAILURES.md). More: [`SCOPE.md`](SCOPE.md),
[`docs/WHY.md`](docs/WHY.md).
## Safety
It starts `git` and, for a version pattern you supply, itself; nothing else. It never writes to the repository, and does not run commands named
in that repository's git config. What is defended and what is not:
[`THREAT_MODEL.md`](THREAT_MODEL.md). Reporting: [`SECURITY.md`](SECURITY.md).
## Tests
Four suites, each pre-registered before the code. Scores are never added together.
| suite | macOS, Python 3.14 |
|---|---|
| `tests/battery.py` — acceptance proofs | 199 declared · 198 green · 0 red · 1 not run |
| `tests/negative_controls.py` — the battery must fail on a broken detector | 56 mutants · 56 caught · 0 not caught |
| `tests/adversarial.py` — written by reviewers who did not write the fixes | 315 attacks · 304 as required · 2 loose · 9 not run; the 2 loose are declared limits ([`docs/FAILURES.md`](docs/FAILURES.md) O2a, O2b) |
| `tests/properties.py` — 60 generated repositories against `tests/oracle.py`, a second implementation written from a specification by someone who did not read this one | 17 properties · 17 green · 0 red · 10 controls · 10 caught |
CI runs the same four on Linux (Python 3.9 to 3.14), macOS and Windows; what each platform could not
run is in
[`tests/RECORD.md`](tests/RECORD.md). These scores describe the cases executed, not inputs nobody
tried. `./reproduce.sh` runs all of it. The 100 projects of the study are also a regression corpus,
pinned to recorded commits: 1.1.0 gives the recorded answer on all 100, as predicted before it ran
([`tools/regression/`](tools/regression/)).
**Ten large repositories** (the Linux kernel, LLVM, CPython and seven more), protocol written
first. Every tag of the kernel: 6.5 GB for the whole process tree with 0.9.1, 900 MB with 1.0.0.
0.9.1 gave three wrong or near-empty answers there; 1.0.0 gives none of them. With a declared
version file ([`docs/LABELS.md`](docs/LABELS.md)) every tag of the kernel is compared: `clean`,
947 of 947, in four minutes.
[`toolWhat people ask about closure_drift
What is luizfnsilva/closure_drift?
+
luizfnsilva/closure_drift is mcp servers for the Claude AI ecosystem. Does your version label name exactly one version of your code? A read-only, zero-dependency detector for version-label drift in git repositories. It has 0 GitHub stars and its last recorded update is dated 2026-10-18.
How do I install closure_drift?
+
You can install closure_drift by cloning the repository (https://github.com/luizfnsilva/closure_drift) or following the README instructions on GitHub. ClaudeWave also provides quick install blocks on this page.
Is luizfnsilva/closure_drift safe to use?
+
Our security agent has analyzed luizfnsilva/closure_drift and assigned a Trust Score of 95/100 (tier: Verified). See the full breakdown of passed checks and flags on this page.
Who maintains luizfnsilva/closure_drift?
+
luizfnsilva/closure_drift is maintained by luizfnsilva. The last recorded GitHub activity is dated 2026-10-18, with 3 open issues.
Are there alternatives to closure_drift?
+
Yes. On ClaudeWave you can browse similar mcp servers at /categories/mcp, sorted by popularity or recent activity.
Deploy closure_drift to your cloud
Ship this repo to production in minutes. Each platform spins up its own environment with editable env vars.
Maintain this repo? Add a badge to your README
Drop the badge into your GitHub README to show it's tracked on ClaudeWave. Each badge links back to this page and reflects the live Trust Score.
[](https://claudewave.com/repo/luizfnsilva-closure-drift)<a href="https://claudewave.com/repo/luizfnsilva-closure-drift"><img src="https://claudewave.com/api/badge/luizfnsilva-closure-drift" alt="Featured on ClaudeWave: luizfnsilva/closure_drift" width="320" height="64" /></a>More MCP Servers
Reverse engineer anything with agents, from app behavior down to native binaries.
Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.
User-friendly AI Interface (Supports Ollama, OpenAI API, ...)
An open-source AI agent that brings the power of Gemini directly into your terminal.
Real-time global intelligence dashboard. AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface
🕷️ A Web Scraping framework that handles everything from a single request to a full-scale crawl! Don't be shy, join here: https://discord.gg/EMgGbDceNQ and follow here for daily tips and tricks: https://x.com/Scrapling_dev