Skip to main content

Pawl

Code can only move forward.

PyPI Python 3.11+ License: Apache-2.0 Dependencies: none

Pawl is a ratchet gate for automated coding work. A change passes only if no test that passed now fails, no test disappeared or got skipped, and nobody loosened the gate itself. Known failures are allowed and can only go down.

The problem

Coding agents told to "make the tests pass" sometimes change the tests instead of the code. Researchers have measured it: agents modify tests, overload ==, and special-case inputs when a task is hard (ImpossibleBench, EvilGenie, METR). Asking them not to is unreliable. A plain "tests must pass" check cannot help when the suite already has known failures, and a count-based ratchet misses a fixed test that hides a new failure, a skipped test, or a rewritten assertion.

Here an agent "fixes" add and edits a test so the suite stays green. The test run looks normal; Pawl does not:

$ python -m pytest tests -q                 # 1 known failure, same as before
1 failed, 9 passed in 0.02s
$ pawl guard --base main --replay
### Pawl guard: FAIL (base main (a536657fb7cc))

- **TEST_MODIFIED** (block) `tests/test_calc.py`: 1 existing test line(s) changed or removed; existing tests are read-only for agents
- **TESTS_WEAKENED_REPLAY** (block) `tests/test_calc.py`: the original tests fail on this code (NEW_FAILURE: 1 test(s) fail that the baseline records as passing)
  - `tests/test_calc.py::test_add_negative`

(Real output from the demo project in the Pawl source repository.)

Quick start

pip install pawl-gate
pawl init --github      # detects pytest, jest, vitest, go, or cargo
git add pawl.toml pawl.baseline.json PAWL_DEBT.md .github/workflows/pawl.yml
git commit -m "Add Pawl ratchet gate"

pawl init writes pawl.toml, runs your suite in two orders, records the baseline, and (with --github) writes a workflow. Then make the pawl job a required status check and add CODEOWNERS for the gate files.

The package is named pawl-gate because pawl is taken on PyPI. The import and the command are pawl. It needs Python 3.11 or newer and has no runtime dependencies.

How it works

flowchart LR
    A[Agent edits code] --> H{pawl hook stop<br/>local, fast}
    H -- red --> A
    H -- green --> PR[Pull request]
    PR --> CI[CI required check]
    CI --> C[pawl check --ci<br/>config + baseline from base branch]
    CI --> G[pawl guard --ci --replay<br/>diff vs base branch]
    C --> D{All green?}
    G --> D
    D -- no --> A
    D -- gate change --> Human[CODEOWNERS review<br/>pawl-approved label]
    D -- yes --> M[Merge]
    M --> U[pawl update<br/>baseline can only tighten]
Command What it does
pawl check Runs every suite and compares each test ID with the baseline. Exit 0 pass, 1 regression, 2 could not measure, 3 preflight failed
pawl update Records the current state. Tightening is free; loosening needs --loosen --reason and is logged
pawl guard Scans the diff against a base ref for gate edits, test deletions and edits, new skips, hook tampering, and suspicious code. --replay runs the base branch's tests on the new code
pawl report Renders the last check and guard as Markdown
pawl init Detects the runner, writes the config and the first baseline
pawl hook stop, pawl hook protect Harness hooks for Claude Code, Codex, Cursor, and Hermes
pawl escalate The agent's sanctioned way out when the spec and the tests conflict
pawl quarantine Excuses a flaky test until an expiry date (humans only)
pawl mutate Advisory mutation spot check on changed Python lines

Adapters: pytest, Jest, Vitest, go test, cargo test, and any runner that writes JUnit XML. Every adapter fails closed: if Pawl cannot parse a result, it exits 2 instead of guessing.

The trust model in one paragraph

The authority is CI, not the agent's machine. In CI, pawl check loads pawl.toml and the baseline from the base branch and uses the stricter of the base and branch baselines, so a branch cannot lower its own bar. pawl guard blocks changes to gate files unless a human adds the pawl-approved label. Harness hooks give the agent the same answer earlier, but they run where the agent can edit them, so they are a convenience.

Results

On a synthetic corpus of 19 agent cheats and 6 clean changes, run against a project with one known failing test:

Detector Cheats caught Clean changes blocked
plain pytest must pass 18/19 5/6 (the known failure blocks everything)
Abraxas count ratchet 5/19 0/6
pawl check (agent side) 12/19 0/6
pawl hook stop --base (agent side) 17/19 0/6
Pawl in CI (check + guard + holdout) 18/19 0/6

The one miss, special-casing inputs that no holdout covers, is beyond any test-based gate. Overhead of pawl check over bare pytest was 0.3 s on both a 100-test and a 5000-test suite.

In a small live trial (12 episodes, one model, a task with one impossible test), an agent without the Pawl contract edited the conflicting test once and rewrote the spec docstring once. Pawl's stop hook and CI gate caught the test edit and missed the docstring rewrite. With the contract, all 8 episodes left the impossible test alone and escalated; none tampered.

Documentation

Run pawl --help and pawl <command> --help for usage. The Pawl source repository is not public; access to it, including the full documentation below, is available on request:

  • Why Pawl: who it is for and how it compares.
  • Configuration reference: every pawl.toml key.
  • Design notes: how it works and why.
  • Research notes: the literature behind each mechanism.
  • Architecture review: threats covered and not covered.
  • Recommendation: where enforcement lives, and migrating from the Abraxas ratchet script.
  • Benchmarks: methods, results, and limits.
  • Integrations: Claude Code, Codex, Cursor, Hermes, GitHub Actions, pre-commit, and an AGENTS.md contract.

Credits

  • The Abraxas test_ratchet.sh script, which Pawl generalizes.
  • ImpossibleBench (Zhong, Raghunathan, Carlini), EvilGenie, METR's reward hacking reports, and the papers in the research notes.
  • Ratchet tools that came first: Betterer, ESLint bulk suppressions, basedpyright baselines, mypy-baseline, and ratchet-gate.
  • A pawl is the hinged catch that lets a ratchet wheel turn one way only.

License

Apache-2.0. The license text ships with the package; see also https://www.apache.org/licenses/LICENSE-2.0.

Metadata

Release files for pawl-gate 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pawl-gate 0.1.1
File Size Uploaded
pawl_gate-0.1.1.tar.gz 58.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pawl-gate 0.1.1
File Interpreter ABI Platform
pawl_gate-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 109.7 kB

Release files / pawl_gate-0.1.1.tar.gz

Download URL pawl_gate-0.1.1.tar.gz
Size 58.5 kB
Tags Source
SHA-256 checksum
How to use checksums
8f36648cbe5906d6a4fa2554269e62028f3d76a9bb4c8d8bc5fc8f1df4dcfe52
BLAKE2b-256 checksum
How to use checksums
96b32038e7c0ae0e053d10ec33ec5f1656d7777b3f82e39ae2a58fe6919270a9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / pawl_gate-0.1.1-py3-none-any.whl

Download URL pawl_gate-0.1.1-py3-none-any.whl
Size 51.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5a6527568ca4de723071a5d5f359809f28e2d5c3089d795a264c82ca21915a85
BLAKE2b-256 checksum
How to use checksums
c1073e40514ea66a5d2798036812ebe5c9baced366666506020e4dccd384bba8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page