checkwash
v0.8.0's Action no longer runs code from the pull request it judges; commands
run from a subdirectory read the repository from its root; and oversized files
that no detector reads are carried unread instead of stopping the run.
Release evidence and known limits.
A pass does not prove correctness; retain exit 2 and diagnostics as analysis
errors. With --format hook-json (the Claude Code Stop hook) an engine error
now exits 0 with a "not analysed" message. The recommended Action is pinned to
the same v0.8.0 release.
Development snapshots: the source in this checkout may be newer than the published CLI. The advertised-release manifest records the version, commit and download bytes behind the installation commands below. New changes require their own candidate evidence and remain unreleased until the publication record verifies them; candidate CI success does not mean that the recommended Action pin includes them.
Check whether a code change weakens your tests.
A test can pass after it stops checking something useful. checkwash reads your Git diff and flags known patterns such as deleted assertions, skipped tests, relaxed expectations, and CI checks that stop failing.
- assert total == 105.3
+ assert total > 0
The new assertion accepts many incorrect totals. checkwash can flag changes like this for review, including changes written by coding agents.
Runs locally. No LLM. No network during analysis. Never executes your code.
v0.3.0 replaces five file-wide exemption rules with content-bound ones. In v0.2.13 those rules could reuse a recorded approval for a later change to the same path; v0.3.0 retires their path-only keys. The upgrade notes describe the content-bound replacement, retired keys, and installation fixes.
v0.4.0 extends detection of runtime suppression, collection changes, expectation rewrites and substituted subjects, and recognizes more table refactors. The proofs retain input identity, execution context and conservative handling of unknown code. Release evidence and remaining limits
v0.4.1 repairs Node assertion recognition (#164) and covers Node test-file names, bounded import aliases and lexical shadows. Coverage warnings expose known assertion candidates the scanner cannot represent; they do not prove complete JS/TS coverage. The same assertion contract checks the source, wheel and zipapp. v0.4.1 evidence and limits
v0.4.2 strengthens JS/TS assertion evidence. Test callbacks own their
assertions; changing a scalar expected value or loosening positive
toBeCloseTo precision reaches the existing detectors. Equivalent scalar
spellings remain equivalent across subject checks. This is bounded syntax
support, not general JS/TS analysis. v0.4.2 evidence and limits
v0.5.0 closes a round of red-team reports (#172–#181). A skip in a
fixture or setup the test runs, a selector over edited tests, a package
manifest's test command, Jest's __tests__/ layout, JS block-level and
imperative skips, JS module mocks, hand-rolled JS tolerances, chai assertions
and CI control flow that stops a runner now reach the detectors; #173, #180
and #181 are closed in part. Some existing findings now carry a different fingerprint,
so allowlist entries for those shapes must be re-recorded; the JSON shape is
unchanged. v0.5.0 evidence and limits
v0.6.0 works through the reports and rulings that followed (#196–#261).
JS/TS assertions keep their meaning across Jest, chai, Vitest and node:test,
and the runner a file's imports name decides whether a moved test still runs
and what a focus stops. Hand-rolled tolerances in both frontends, pytest
selectors under explicit targets, skip guards in a body, a setup or a conftest
hook, every pytestmark spelling and a CI config the reader cannot read now
reach the detectors. Some existing findings now carry a different fingerprint,
so allowlist entries for those shapes must be re-recorded; the JSON shape is
unchanged. v0.6.0 evidence and limits
The next minor (unreleased) makes assertion-level exemptions content-bound (#421). Through v0.8.0, an exemption for one rewrite of an assertion also allowed every later rewrite of the same line. Nine assertion-level rules now key a finding by the old and the new assertion as well, TEST_DISABLED keys one by what the change disabled (#512), and their old keys stop matching: re-review and re-record those entries. Assertion-level notes
Try it
You need Python 3.11+ and Git. Download and try the offline examples first.
Windows PowerShell (including 5.1):
curl.exe -LO https://github.com/taipei49314/checkwash/releases/download/v0.8.0/checkwash.pyz
python checkwash.pyz --version
python checkwash.pyz demo
PowerShell 5.1 aliases curl to Invoke-WebRequest; use curl.exe as written.
macOS/Linux or Git Bash:
curl -LO https://github.com/taipei49314/checkwash/releases/download/v0.8.0/checkwash.pyz
python checkwash.pyz --version
python checkwash.pyz demo
Then, from the root of a repository with at least two commits:
python checkwash.pyz check HEAD~1..HEAD
This checks your last commit. Start with a change you already understand.
You can also download the file in your browser.
For uncommitted changes use python checkwash.pyz check. For a branch review,
use python checkwash.pyz check BASE...HEAD to compare from the merge base;
BASE..HEAD compares the two named snapshots directly.
From a subdirectory, check analyses the whole repository from its Git root
and says so in one line on stderr (since v0.8.0,
#416). Before v0.8.0,
run it from the repository root.
| Result | What to do |
|---|---|
| Pass · exit 0 | No finding requires blocking under your configuration. Keep running your normal tests and review. |
| Block · exit 1 | Read the finding and the diff. It may be weakened verification or a false positive. |
| Error · exit 2 | Resolve the input or analysis error before relying on the result. |
The default threshold is high: a visible warn can still pass.
REPAIR_EVIDENCE describes related changes in the same diff; it does not
prove a repair is correct.
For JSON/SARIF output and more examples, see the usage guide.
Know the limits
v0.8.0 is alpha. A pass does not prove that a change is correct or honest. Python is the main language supported; JS/TS support covers a limited set of test patterns. Known gaps remain.
In v0.8.0, JS/TS test.each, it.each and test.for tables are not
scanned: an assertion weakened or deleted in one, an edited row or a deleted
table passes. From the next minor (unreleased), each such table is a test
unit, and its rows are read when they can be counted (an inline array or a
tagged template, or a same-file const read only as a table). The rows of a
describe.each table stay unread, and a table with a computed title is not a
unit (#428).
Legitimate refactors can be flagged. The frozen historical corpus records 22 blocks out of 60 (36.7%). A separate pre-release source replay records 4/60 blocks, including 2/57 strictly qualified cases; three existing fixture errors remain. These are selected examples, not a general false-positive rate.
A changed file over 1,000,000 bytes is not read. An image, data file,
lockfile, document or non-Python production source that large is reported on
stderr and earns no repair evidence. From the next minor (unreleased,
#454), --format json
also lists it under unread, SARIF names it in a tool execution
notification, and the term report adds a ! line and, when there is no
finding, opens with "analysis incomplete"; the Stop hook's JSON and
--emit-ir do not name it. A file a detector must read (test, conftest, CI,
guardrail, snapshot, JS setup, runner script or test-command file) that large
stops the run with exit 2, naming it. So do a .py module
that large, whether added, deleted, renamed or edited, and a pytest
configuration file (pytest.ini, .pytest.ini, pytest.toml,
.pytest.toml, tox.ini, setup.cfg, pyproject.toml) at any depth:
either can change the module a test's import resolves to. So do any
non-generated file under the root scripts/ directory, read for the pytest
commands it runs, and any other changed file that large the analysis needs
as context. A shell shebang gives a file the shape of a runner script, whatever
its name, except a .py module or a JS/TS test file.
From the next minor (unreleased,
#455), checkwash reads
the first 200 bytes of a side over the limit to tell, and uses them for
nothing else; a Git side is streamed and checked against its object id.
Where those bytes are read, a file that large whose side starts with a shell
shebang stops the run, naming that side, whatever its suffix (a .py module
or a JS/TS test file that large stops the run anyway, by its own rule). A
file with no extension (a dotfile such as .envrc counts) without one, such
as a README or a LICENSE, is set aside like any document, unless another
rule above makes it one a detector must read. v0.8.0 reads no byte of such a
side, so every file with no extension that large stops the run there. On main
a Git side over 64,000,000 bytes is not streamed, so its first bytes are not
known: a file with no extension still stops the run when one of its Git sides
is that large, and a suffixed file whose shebang is only on such a side is
still set aside.
When a file that large stops the run, from the next minor (unreleased)
--format json prints {"unread": [...]} on stdout instead of nothing: one
unread entry, for the side the run refused. It is not a findings document,
and term and SARIF output stay empty. With a range, json output stays empty
too when the refused file is a symlinked pytest configuration's target that
no detector must read.
Generated output (build/, dist/, node_modules/ and the like) is set
aside like a production source unless it is a Python module, a pytest
configuration, a CI file, a runner script or a file with no extension whose
shebang is not known. A root
poetry.lock or uv.lock that large at the base is skipped with a note on
stderr, and the skip is not listed under unread; when the diff also changes
that lockfile, the change's sides over the limit are listed like any other. A
root pyproject.toml or requirements*.txt that large at the base stops
every run, naming it: term, json and SARIF output exit 2 and name it on
stderr only. The Claude Code Stop hook (--format hook-json) reports an
engine error, this one included, as a non-blocking "not analysed" message
on stdout that quotes the error, and exits 0.
From the next minor (unreleased), when the run stops on a file the hook's diff
changes and that file's working-tree side is over the limit, the hook blocks
the stop instead, naming the file and asking to shrink, split or revert it
(#456). A file whose
base side alone is over the limit, a root manifest over it at the base, and a
padded file committed before the stop keep the message; the full list is in
subject integrity.
Try it on your own changes before making it required. Coverage and limitations · Known gaps
Use it in CI
To stop a merge, make the checkwash status check required in your
repository's branch rules. Installing the tool or adding a workflow alone
does not enforce its verdict.
The recommended Action and the CLI above are both v0.8.0. Record which version you use. Full setup and exemptions
Advisory: Action trust boundary. Every published Action release up to and
including v0.7.0 runs python -m pip and then checkwash with the pull
request's checkout as the working directory, so a module in that checkout (for
example a top-level pip package) can replace the installer and then the
checker, and the pull request can tamper with the result it is judged by. Under
checkwash's threat model that applies to every pull request it judges. v0.8.0
fixes it (#372); pin the
v0.8.0 Action below. If you stay on v0.7.0 or earlier, treat a pass as
advisory, review the pull request before merging and keep comment-pr off.
Known issue
Copy the GitHub Actions workflow and require the check
Save this as .github/workflows/checkwash.yml:
# .github/workflows/checkwash.yml
on: [pull_request]
permissions:
contents: read
jobs:
checkwash:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- uses: taipei49314/checkwash/action@f02b5d8a07fa39ee1898881f66c571eb164cb11c # v0.8.0
After the workflow runs, open Settings → Rules → Rulesets and require
the checkwash status context. Keep the job unconditional.
If you administer the repository and have this project's ruleset file, you can instead create the rule with:
gh api repos/OWNER/REPO/rulesets --method POST --input action/required-ruleset.json
This adds a ruleset; it does not replace existing ones. checkwash doctor
can inspect the local workflow, but cannot verify live branch protection.
Why is the pin added after the release? A release
cannot embed its own commit SHA, so a release commit carries the prior
release's pin; once a release's publication is verified, a follow-up on main
recommends that release's own peeled commit. The recommended v0.8.0 Action includes the engine
published on 2026-10-15. A CLI upgrade does not update an existing Action. To verify another trusted release, use
git rev-parse 'vX.Y.Z^{commit}'.
Action reference
More options and evidence
Install as a CLI or try the included examples
If you already use pipx, install the fixed version:
pipx install checkwash==0.8.0
# or from the release tag:
pipx install git+https://github.com/taipei49314/checkwash@v0.8.0
checkwash check HEAD~1..HEAD
checkwash demo # 8 real tampering cases, blocked, offline
checkwash demo replays eight real tampering cases and one honest fix.
It illustrates known patterns; it is not a coverage guarantee.
You can run the same examples with python checkwash.pyz demo.
Read the measurements and their limits
The historical six-repo sweep recorded 46 / 1800 = 2.56% blocks: 31 false positives (1.72%), 15 legitimate policy blocks (0.83%). The tracked artifacts record engine v0.3.0 (the release commit, swept 2026-09-07) on a corpus used to tune the detectors, not a held-out result.
1.33% of the corpus (24/1800) records opaque production changes. That flag does not establish that each verdict changed or each diff was unanalyzed.
Review methods vary: the three-rater agreement study covers an older
35-diff cohort, not all 46 blocks. The dedicated 22/60 refactor result
(benchmarks/refactors/results-latest.json, a dated v0.3.1 snapshot) is a
separate population; the general-commit rate does not predict it.
Measurements and source data · Generated results · Failure ledger
| Looking for… | Start here |
|---|---|
| Installation checks, versions and first use | v0.8.0 release guide |
| JSON/SARIF contracts and upgrades | Stability · Embedding guide · Findings schema |
| Assertion support, coverage warnings and artifact checks | Assertion coverage |
| Required checks and reviewed exemptions | Enterprise setup |
| Contributing or reporting a problem | Contributing · Issues · Security reports |
| Readiness for 1.0 | Criteria — not met |
Related projects: checkwash-corpus and smallestlie hold evaluation work.
The package also includes a limited quality preview for coverage, Ruff and mypy configuration review. Setup and CI adoption guide. Its frozen legacy-byte comparison remains red; publication does not establish natural-case acceptance or effective enforcement.
Alpha pre-release. 22 detectors, 20488 tests in the current source tree. Zero runtime dependencies. Apache-2.0.
Metadata
Release files for checkwash 0.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| checkwash-0.9.0.tar.gz | 4.4 MB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| checkwash-0.9.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 5.3 MB
Release files / checkwash-0.9.0.tar.gz
| Download URL | checkwash-0.9.0.tar.gz |
|---|---|
| Size | 4.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
718f4ecc22cb5f308a8fb4f0e2c14d1be4c2a0df6c61487b3bc413c0f48dcee9
|
|
BLAKE2b-256 checksum How to use checksums |
1f8822aba2f4d5c5f48a739841e70c6c7a8a056c1c53afcb804bf0918711e743
|
| 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 Oct 22, 2026.
Transparency logRelease files / checkwash-0.9.0-py3-none-any.whl
| Download URL | checkwash-0.9.0-py3-none-any.whl |
|---|---|
| Size | 935.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8810f7500301a6e31e811f9b8838b31548c78ad798db022d64b72857921ec53f
|
|
BLAKE2b-256 checksum How to use checksums |
145d57cf1e0c8ba1689c3246ca3b0cb96f40a1909f104b236d893d1944a09037
|
| 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 Oct 22, 2026.
Transparency logSecurity for checkwash 0.9.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for checkwash-0.9.0.tar.gz
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 Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.