Skip to main content

Reads a code change and reports whether it weakened the project's own verification — deleted tests, focused/skipped tests, forced-success expressions, hollowed assertions, swallowed errors.

Project description

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.

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

Project details


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.1.tar.gz (25.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.1-py3-none-any.whl (18.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: tampercheck-0.1.1.tar.gz
  • Upload date:
  • Size: 25.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.1.tar.gz
Algorithm Hash digest
SHA256 462b24711aae886a88a3f49f036efad76fe0b9784fab7abaa82533a6d35e64f1
MD5 e9cd6e6b74ab4b9e4ea97126935b47cb
BLAKE2b-256 bed7d7a287828e7c401a9203b321b715414476444f364294d935e1b0056ee475

See more details on using hashes here.

File details

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

File metadata

  • Download URL: tampercheck-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 18.6 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2ab138ad75657872eda19fd1f8f6e5c19d96e3885012129d86565168c0e15e4e
MD5 b0ab5a070bb27a271dd1a2dc2510022c
BLAKE2b-256 8fe77473193ddb98ac585cfe28016bc7ca514a9196a6c93212bb7a0c9b725aea

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