tampercheck
Did this change weaken the project's own verification?
tampercheck reads a code change (a unified diff) and reports whether that
change made the checking weaker than it was before: deleted tests, focused
or skipped tests, forced-success expressions, hollowed-out assertions,
swallowed errors, and placeholders standing where behaviour should be.
It exists because an AI coding agent told to "make the tests pass" has two
options — fix the code, or make the tests stop checking. Deleting the failing
test, adding .only, appending || true, weakening an assertion, wrapping
the failure in an empty catch: every one of those produces a green run and
an honest-sounding summary. A linter can't see it (it checks the code that
exists, not the test that was removed), and CI reports green because green is
exactly what was engineered. tampercheck looks at the diff instead.
It is deliberately provider- and harness-agnostic: it reads a diff, so it does not care whether Claude, Codex, a colleague, or you wrote the change.
Install
uvx tampercheck --version # zero-install run via uv
pip install tampercheck # or install normally
Use
git diff | tampercheck # diff on stdin
tampercheck --from main --to HEAD # runs git itself
tampercheck --pr 123 # GitHub PR via the gh CLI
tampercheck --json # machine-readable output
tampercheck --min-severity critical # only criticals fail the run
Exit codes — 0 clean, 1 findings at or above --min-severity
(default high), 2 the tool itself failed. 2 is never conflated with
1: a crashed check must not look like a clean result.
The eight detections
Every detection fires only on lines the change added. Pre-existing conditions in the repository are never reported.
| # | Kind | Severity | Catches |
|---|---|---|---|
| 1 | test-deleted |
critical | a test file removed by this change |
| 2 | test-focused |
critical | a newly added .only / fit / fdescribe — silently excludes every other test |
| 3 | unconditional-success |
critical | newly added || true, bare exit 0 in CI, process.exit(0) / sys.exit(0) in tests, assert(true) |
| 4 | test-skipped |
high | newly added .skip / .todo / xit / @pytest.mark.skip / #[ignore] |
| 5 | test-filtered |
high | a newly added test-selection filter (--grep, pytest -k, jest -t, cargo test <name>) |
| 6 | assertion-weakened |
high | a test file that removes more executable assertions than it adds |
| 7 | swallowed-error |
high | a newly introduced empty catch / except: pass / empty Err(_) => {} |
| 8 | placeholder |
medium | a newly added TODO / FIXME / not implemented / unimplemented!() |
Languages: Python (pytest, unittest), JS/TS (jest, vitest, mocha,
node:test), Rust, and shell / CI YAML. Detection patterns live in one
declarative table (src/tampercheck/patterns.py); adding a language is a data
change, not a code change.
Justifying a legitimate finding
Sometimes deleting a test is correct. Acknowledge it in the diff itself, on or next to the flagged line:
# tampercheck: allow replacing this suite with test_auth_v2.py in this PR
The finding is then reported as ALLOWED with your reason and does not fail
the run. The justification travels with the change, visible to whoever
reviews it — unlike an external ignore-file, it cannot drift.
Justifying a deletion. A deleted file has no lines to annotate, so a
test-deleted finding accepts the marker on any added line in the diff —
as long as the reason names the deleted file (that binding is what stops one
stray comment from waving through unrelated deletions):
# in the replacement file, or any file in the same change:
# tampercheck: allow test_cart.py superseded by test_cart_v2.py
The finding's own output prints this remedy with the real filename, so you never have to remember the contract.
The --allow flag. For one-off local runs, or a standing exception you
want reviewed in the workflow file rather than the diff:
tampercheck --allow test-deleted:test_cart.py
Repeatable; KIND:PATHFRAG form. Prefer the in-diff marker — it keeps the
justification attached to the change it excuses. There is deliberately no
commit-message trailer: tampercheck reads diffs, and its stdin and --pr
input modes never see a commit message, so a trailer would silently work in
only one of the three input modes.
Measured false-positive rate
Patterns were tuned against 1,331 real historical commits from three actively developed repositories (civiccast, civicrecords-ai, civicclerk — Python, TS, shell, CI YAML; overwhelmingly ordinary human/agent changes):
- commits with any finding: 3.0%
- commits that would gate at the default
--min-severity high: 1.4%
Of that 1.4%, more than half are newly added broad except Exception:
swallows in production code — findings the tool is designed to raise, not
pattern errors. Genuine false positives measured ≈ 0.7%, dominated by the
assertion census reacting to honest test refactors (see decisions below).
The first untuned run measured 5.6%; the tightenings that closed the gap are
recorded in src/tampercheck/patterns.py next to the patterns they shaped.
Recorded decisions
- Implementation language: Python (≥3.10, one dependency:
unidiff). Matches the primary target stack and ships via PyPI/uvxwith nothing to install; the trade-off given up is a single static binary. || trueonly fires next to a test/check runner. On cleanup/config commands it is idiomatic shell (65/65 corpus hits were legitimate).- Conditional skips are not findings.
@pytest.mark.skipif(<condition>), runtimepytest.skip("...")guards, andtest.skip(!available(), ...)are environment guards (24/24 corpus hits legitimate). Unconditional forms —@pytest.mark.skip,skipif(True, ...),.skip("name"— still fire. - Only broad exception swallows fire.
except OSError: passin an availability probe is a deliberate, visible choice;except:/except Exception:+passis the tamper smell. An existing# noqa: S110/S112/BLE001or# nosecon the line counts as an inline justification, same as the allow marker. - Assertion census keeps the crude line-count (the corpus said multi-line assertions are not the real noise source — honest refactors are), but fires only on a net loss of 2+ assertion lines: at net-loss-1 the corpus showed pure refactor noise.
- Justification lives in the diff, not an ignore-file. An external allowlist drifts and is invisible at review time; the inline marker is reviewed with the change it excuses.
What it does NOT catch
Honesty section. tampercheck is a deterministic, line-oriented diff check.
It does not run your tests and it does not judge code quality. It will not
catch: a test whose assertion is subtly wrong rather than removed; a new
test that passes against the pre-change code and therefore proves nothing
(that requires executing tests against the base commit); mocking a dependency
so broadly the test can't fail; slow architectural degradation that passes
honest tests; or a filter/skip introduced in a file type it doesn't scan.
Multi-line assertions may confuse the assertion census. It reduces the
cheapest forms of verification tampering to zero cost for a reviewer — it
does not replace the reviewer.
Exit-code contract for CI
- name: tampercheck
run: git diff origin/${{ github.base_ref }}...HEAD | uvx tampercheck
See integrations/ for a ready-made GitHub Actions job (as used by
deterministic-detector)
and the evidence line used by
dev-rigor-stack-lite.
License
Apache-2.0 — Scott Converse.
Metadata
Release files for tampercheck 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tampercheck-0.1.1.tar.gz | 25.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tampercheck-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 43.8 kB
Release files / tampercheck-0.1.1.tar.gz
| Download URL | tampercheck-0.1.1.tar.gz |
|---|---|
| Size | 25.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
462b24711aae886a88a3f49f036efad76fe0b9784fab7abaa82533a6d35e64f1
|
|
BLAKE2b-256 checksum How to use checksums |
bed7d7a287828e7c401a9203b321b715414476444f364294d935e1b0056ee475
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Release files / tampercheck-0.1.1-py3-none-any.whl
| Download URL | tampercheck-0.1.1-py3-none-any.whl |
|---|---|
| Size | 18.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2ab138ad75657872eda19fd1f8f6e5c19d96e3885012129d86565168c0e15e4e
|
|
BLAKE2b-256 checksum How to use checksums |
8fe77473193ddb98ac585cfe28016bc7ca514a9196a6c93212bb7a0c9b725aea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|