Skip to main content

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 codes0 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.

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/uvx with nothing to install; the trade-off given up is a single static binary.
  • || true only 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>), runtime pytest.skip("...") guards, and test.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: pass in an availability probe is a deliberate, visible choice; except: / except Exception: + pass is the tamper smell. An existing # noqa: S110/S112/BLE001 or # nosec on 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tampercheck-0.1.0.tar.gz (24.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tampercheck-0.1.0-py3-none-any.whl (17.8 kB view details)

Uploaded Python 3

File details

Details for the file tampercheck-0.1.0.tar.gz.

File metadata

  • Download URL: tampercheck-0.1.0.tar.gz
  • Upload date:
  • Size: 24.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.13

File hashes

Hashes for tampercheck-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2bddf74aa09e9f82f42eb519331825bdbb22cd1440fdfe35374bd15fc37d4dac
MD5 9a418ce06a022a265229981a3b76946d
BLAKE2b-256 e1f8513dd7af4b782ca35b2e2b788ff670fb744e379225995efbd07a58db2d78

See more details on using hashes here.

File details

Details for the file tampercheck-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: tampercheck-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 17.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.13

File hashes

Hashes for tampercheck-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1148bdb3c43144ae6f094dc8574d4a12fe97015b5ae976f70c936700ff257ccf
MD5 de488a494318a68d9d8e5fbcf6859ea5
BLAKE2b-256 1067c8e4e089eb82bc1be8bc8c688620fa193fd3b810beeb70e3964f489e1d2f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page