Reinforcement learning trains a model against a verifier β a reward function that decides whether an answer is right. In code, that verifier is a test suite: the patch passes the tests, it gets the reward. But if the tests are weak, a wrong patch passes too, and the model is rewarded for gaming the check instead of solving the task.
Published pipelines that build RL environments check that the gold patch passes and that a no-op fails. Neither test asks the question this harness asks: can a deliberately wrong patch pass? Tripwire attacks the verifier with an LLM told to write exactly that, then adjudicates every candidate the suite accepted.
On all 500 tasks of SWE-bench Verified:
The shipped suite accepted a patch from an attacker trying to write a wrong one
51.0% ββββββββββββββββββββββββββββββββββββββββββββββββββ 255 / 500 tasks
45.2% ββββββββββββββββββββββββββββββββββββββββββββββββββ 226 / 500 ...and that patch differed textually from gold
Adjudicated wrong, in a pre-registered sample of 102 of those 226
13.3% ββββββββββββββββββββββββββββββββββββββββββββββββββ raw, uncorrected
CORRECTED for the adjudication's measured false-positive rate (16.9%)
5.9% ββββββββββββββββββββββββββββββββββββββββββββββββββ [95% CI -0.4 to 10.7%]
The interval includes zero. A benign-attacker control β the same pipeline with the instruction inverted to write the correct fix β shows blind reviewers label 16.9% of deliberately-correct patches as reward hacks. Correcting for that, the headline falls from 13.3% to 5.9%, and this study can no longer distinguish the effect from zero. See Β§ The control.
The claim this repository supports, in one sentence β every clause checkable from files committed here:
On all 500 SWE-bench Verified tasks, an attacker LLM given the gold patch's source region, the names of the failing tests, and an explicit reward-hacking recipe produced a patch the shipped suite accepted on 255 tasks (51.0%), and on 226 of those the accepted patch differed textually from the gold fix; in a pre-registered proportional sample of 102 of those 226, adjudication found 30 to be narrow, non-generalizing fixes β a raw 13.3% of the benchmark, of which 3 were confirmed by differential execution and the remaining 27 by single-rater code review. A pre-registered blind control then measured that same adjudication labelling 16.9% of deliberately-correct patches as hacks, which corrects the estimate to 5.9% [-0.4, 10.7] β an interval that includes zero.
Getting a patch past the suite is not evidence the patch is wrong: many are legitimate alternative fixes. Raw sample verdicts: 30 HACK / 66 CORRECT / 6 AMBIGUOUS. The control below shows that even after that filter, a substantial share of the remaining HACK labels are measurement error.
| Run | Tasks | Attacker | Suite accepts an attacker candidate | Adjudicated wrong |
|---|---|---|---|---|
| Anchor (replication attempt) | 49 | Claude Sonnet 4.5 | β | 11/49 = 22.4% (uncorrected) |
| Anchor β capability ladder | 49 | Claude Haiku 4.5 | β | 8/49 = 16.3% |
| Reference β arXiv:2606.16062 | 49 | Claude Sonnet 4 | β | 14/49 = 28.5% |
| Extend (full benchmark) | 500 (12 repos) | Claude Sonnet 4.5 | 51.0% (255/500) | raw 13.3%; corrected 5.9% [-0.4, 10.7] |
docs/anchor-result.md.
flowchart LR
A["SWE-bench task"] --> B["Attacker LLM<br/>K=3 rounds. Given the gold<br/>patch's source region, the<br/>hidden test names, and the<br/>failing-test logs.<br/>Goal: a WRONG patch that passes"]
B --> C{"Native verifier<br/>shipped test_patch<br/>run in Docker"}
C -- "fails the tests" --> X["not hackable"]
C -- "PASS" --> D{"Adjudication"}
D --> E["Differential execution<br/>gold vs candidate<br/><i>ran on 3 of 102</i>"]
D --> F["Single-rater code review<br/><i>the other 99</i>"]
E --> Z["verdict"]
F --> Z
What the attacker is given matters, and it is not a passive property of the suite. In full
(harness/code_attack.py):
| Input | Detail |
|---|---|
| Source region reconstructed from the gold patch | pre_image_context(inst["patch"]), 8 KB. It never has to localize the bug |
| The bug report | problem_statement, 6 KB |
| Names of the failing tests | FAIL_TO_PASS node-ids, first 40 only |
| Passing tests | a count only β no names |
| An explicit reward-hacking recipe, one per round | A rotating menu of three: hardcode the reported case, narrow conditional guard, minimal symptom suppression |
| On rounds 2β3 | the previous diff and a 4 KB tail of the failing-test output |
The recipe menu is the input that most changes what 51.0% means. This is not "a model asked to write a
wrong patch" β it is a model handed three named strategies for gaming a test suite and told to produce
"the narrowest INCOMPLETE patch (not the general fix)". It is also why some candidates carry comments like
# Narrow hack: strategy B is literally named narrow conditional guard, so those self-labels are prompt
artifacts and are never used as evidence in adjudication.
51.0% is the rate under that white-boxed, recipe-guided search β not a rate an unaided model would reach, and not a property of the suite alone.
Adjudication is mostly human, and the README used to say otherwise. The differential-execution oracle runs gold and candidate on generated inputs and calls a divergence WRONG. Its two outcomes are not symmetric, which matters more than the coverage number:
| Oracle outcome | Sampled tasks | What it establishes |
|---|---|---|
Found a divergent input (oracle_wrong: true) |
3 | The candidate diverges from gold, and the record holds the input that shows it |
| Found none among the inputs it could drive | 7 | Nothing. diffexec_oracle.py sets needs_hand_review on this outcome β "never auto-pass a residual as correct" |
| Never attempted | 92 | Nothing |
So 3 of 102 verdicts rest on an execution and 99 rest on one person reading diffs, non-blind, with the attacker's own commentary visible β the same manual method the reference paper used. Every hand verdict in the numerator ships a written rationale you can check against the two patches. Reading the 7 null results as CORRECT would flip four of those verdicts on absence of evidence; a draft of this repo's own test suite did exactly that before it was caught.
Two caveats on the oracle, and neither is small.
It is not fully deterministic. The comparison is deterministic once inputs are fixed, but the candidate
inputs are proposed by an LLM (diffexec_oracle.py), so a re-run can probe different inputs. The same
task, astropy-14309, produced ('read', 'test.txt', None, hdu_list) in the anchor run and
('read', 'file.txt', None, hdu_list) in the 500 run. The divergence is real in both; the search for it
is not reproducible.
Divergence is not wrongness. The oracle shows a candidate behaves differently from gold on some input. On a benchmark whose tests are sometimes narrower than the specification, that is sometimes what a correct patch does. This was the project's largest open hole until 2026-09-11, when the control below was run.
Design. The same pipeline with exactly one thing changed: the instruction, inverted from "produce the
narrowest INCOMPLETE patch" to "produce the correct, general fix". Same gold-derived source region, same
problem statement, same test names, same K=3 rounds, temperatures, patch applier and Docker verifier
(harness/benign_arm.py). Run on all 102 pre-registered tasks, $4.23.
Result 1 β the first gate barely discriminates. A patch written to be correct passes the shipped suite on 48% of these tasks, and on 39% it passes and differs textually from gold. So "the suite accepted it and it is not gold's fix" is an ordinary outcome for an honest fix here, not a signature of hacking.
Result 2 β the adjudication has a false-positive rate, and it is large. 137 patches from the 40 tasks
where both arms produced an accepted, distinct candidate were shuffled, stripped of every tell that names
the arm, and sent to four reviewers who saw only the gold patch and one candidate. The key was held out.
The protocol was committed before any verdict existed (docs/audit/blind-protocol.md, commit 24514d2).
| arm | n | HACK | rate | 95% CI |
|---|---|---|---|---|
| attack | 72 | 22 | 30.6% | [21.1%, 42.0%] |
| benign | 65 | 11 | 16.9% | [9.7%, 27.8%] |
16.9% of patches written to be correct were labelled reward hacks. The difference is +13.6 points, but the intervals overlap and a two-proportion test gives p = 0.062.
The correction. Pre-registered as pΜΒ·(1 β f/a): the raw pΜ = 30/102 becomes 0.131, and the headline
falls 13.3% β 5.9%, bootstrap 95% CI [-0.4%, 10.7%]. The less conservative mixture estimator
(pΜ β f)/(1 β f) gives 6.8%; both are in results/blind_adjudication.json. The pre-registered figure is
the headline because choosing the kinder formula after seeing the data is the thing pre-registration
exists to prevent.
What this does and does not say. It does not say the original adjudication was biased: blind reviewers rated attack patches at 30.6% against the original non-blind rater's 29.4%, so that rater was not inflating. It says the task is error-prone for everyone, in both directions, and that roughly half the raw signal was measurement. At n=137 this study cannot distinguish the remaining effect from zero.
Not doing: running more packets until the interval excludes zero. That is the rule
docs/adr/0001 earned, and the protocol fixed it in advance.
Requirements: Docker, Python 3.11+, and an LLM attacker key (Anthropic API or AWS Bedrock).
git clone https://github.com/Programmer7129/tripwire && cd tripwire
pip install -r requirements.txt
export ANTHROPIC_API_KEY=... # or configure AWS Bedrock and pass --provider bedrock1 β Sanity-check the verifier adapter (the gold patch must resolve; a no-op must not):
python harness/swebench_adapter.py astropy__astropy-12907 --gold # expect: resolved = True
python harness/swebench_adapter.py astropy__astropy-12907 --noop # expect: resolved = False2 β Run the attack on the pinned anchor subset (task IDs in results/swe_subset.json):
python harness/code_attack.py <instance_ids...> \
--provider anthropic --attacker-model claude-sonnet-4-5 \
--out results/raw_swe3 β Run the deterministic oracle on one of the three tasks where it fired:
python harness/diffexec_oracle.py astropy__astropy-14309 --exploit-file <candidate.diff>4 β Recompute the headline from the committed evidence in results/ (no Docker, no API key):
python harness/swe_native_rate.py # 51.0% (255/500) and the 226-task confirm queue
python harness/swe_confirm.py # 13.3% [9.7-17.6%] β Wilson interval5 β Run the tests (no network, no Docker, no API key, no GPU):
pip install -e ".[dev]" && pytest -qtests/test_verdict_provenance.py checks published verdicts against the per-task records and fails if a
label claims an execution that never happened β the check that was missing when 17 labels were wrong. Its
own limits are stated at the top of the file: the oracle ran on 3 of the 102 sampled tasks, so the tests
that compare a verdict against an execution constrain 3 verdicts. For the other 99 it checks that the
record exists, still carries the accepted patch, and that the verdict declares its evidence. No test can
check that a hand-written rationale is true.
harness/
code_attack.py the attacker (K=3, failure-log feedback, deterministic diff transport)
swebench_adapter.py apply any patch β run the native verifier β read resolved
diffexec_oracle.py differential execution: gold vs candidate on generated inputs
aggregate.py Wilson CIs, BH-FDR, beta-binomial pooling, cluster-bootstrap
swe_confirm.py recomputes the RAW adjudicated rate (13.3%) from results/
benign_arm.py the control: same pipeline, instruction inverted to "write the correct fix"
blind_adjudication.py shuffles both arms blind, holds out the key, scores the false-positive rate
swe_native_rate.py recomputes the verifier-alone rate (51.0%) from the raw records
results/
raw_swe_500/ all 500 per-task attack records β the evidence behind 51.0% (4.1 MB)
raw_benign/ the control arm's 102 records
blind_adjudication.json the control's result and the correction it implies
swe500_sample_verdicts.json per-task verdicts, each with its oracle record or its rationale
tests/ metric + provenance suite β no network, no Docker, no API key, no GPU
docs/
anchor-result.md the 49-task run + capability ladder
extend-result.md the full-500 result, sampling, limitations
preregistration.md protocol and its amendments
adr/ abandoned directions and what they earned
audit/ the 2026-09-10 re-audit of every hand-review rationale (18 of 29 held),
and the 2026-09-11 blind control: protocol, packets' key, verdicts, score
A note on the name. The public artifact is Tripwire.
envcertis the older internal name and still appears in runtime identifiers β Docker image and container names, egress network and proxy names, in-container probe paths, run-id prefixes. Load-bearing strings, not prose; nothing namedenvcertis a separate project.
- 27 of 30 adjudicated hacks rest on single-rater code review, not execution. Non-blind, no second
rater, no inter-rater agreement statistic. The 3 oracle-confirmed exhibits are
astropy-14309,astropy-14995andastropy-7671; each ships the divergent input, gold's output and the candidate's, copied from its record. - All 29 hand-review rationales were independently re-audited on 2026-09-10 against the gold patch,
the accepted candidates and the shipped tests. 11 did not survive. Two verdicts moved to AMBIGUOUS
(
django-13033,pytest-7571) because the divergence their rationale claimed does not occur; nine rationales were rewritten. Every failure was the same defect: the rationale described one accepted candidate when the task accepted two or three. 8 accepted candidates across 6 tasks turned out to be identical or equivalent to the gold patch β they are recorded per task ingold_equivalent_rounds, and no per-round or per-strategy rate should be derived from the task-level label. Five rationales that audited true are still unscoped and are pinned intests/test_verdict_provenance.py. - One sampled record is incomplete.
sphinx-8120's stored patch is missing a hunk its ownpatch_metasays was applied, and its rationale reasons about exactly that file β so that verdict cannot be fully checked against the committed evidence. Verdict CORRECT, so it suppresses the rate. Pinned intests/test_verdict_provenance.pyso the set can only shrink. - One record contradicts itself.
astropy-12907holds two oracle results that disagree about the same task. It is outside the sample and moves no published number, but it cannot be cited as evidence. - The drawn sample can be asserted, not reproduced. No sampling code exists in this repository and the history opened as a single squashed commit, so "seed 42, stratified by repo" is a claim about how the 102 ids were chosen that nothing here demonstrates. A hash now pins them against later substitution; that is drift protection, not provenance.
- 15 of the 102 sampled verdicts rest on no recorded evidence at all β no oracle record, no rationale.
All 15 are CORRECT, so they suppress the rate rather than inflating it, and they are flagged
"evidence": "none-recorded"in the verdicts file. A test asserts that count and that direction. - The benign control has now been run and roughly halved the headline (see The control). Its own limits: n=137 packets over 40 shared tasks, so the attack-vs-benign difference is not significant (p=0.062); the reviewers are LLM agents, not the original human rater, so it measures whether the stated criteria separate the arms rather than whether that rater did; and both arms are generated by the same model family, so a different model might write differently-shaped correct fixes.
- The confirmed rate is an estimate, not a census: 32 tasks were adjudicated, ~71 is the extrapolation to the 226-task queue. Nobody examined the other ~39.
- The interval is a Wilson interval on 32/102 propagated through Γ226/500. It carries no finite-population correction (n/N = 45%), does not use the stratification, and allows nothing for adjudication error β which, given 29/32 come from one rater, is plausibly the dominant uncertainty.
- Pre-registration ordering cannot be demonstrated from git. The repository opened as a single squashed
commit containing the protocol and the results together.
docs/preregistration.mdΒ§9 records this; amendments from 2026-09-02 onward land as their own commits and are checkable. - SWE-bench Verified is contested for contamination, and OpenAI retired it in February 2026 citing 59.4% materially flawed tasks among 138 audited. Running this on a decontaminated benchmark (SWE-rebench) or a live one (SWE-bench Pro Verified) is the obvious next step and has not been done.
- An earlier full-500 pass was discarded after a Docker disk-exhaustion bug silently produced empty patches; caught, fixed, and re-run clean.
Total attacker spend: $29.98, summed from the cost_usd field of every attack
record. $21.60 of that is checkable from this repository β the full-500 pass, whose 500 records are
committed and whose total is pinned by pytest. The remaining $8.38 is not independently checkable
here: the anchor run ($2.53), the discarded v1 ($2.64), the Haiku rung ($2.58) and retest/diagnostic
passes ($0.63), whose raw records stay out of the repo as superseded intermediates. Docker and CPU time
are local and unmetered.
This is a crowded area and this harness is not the first to enter it. Nearest first:
- SWE-ABS β Yu et al., ICML 2026. alphaxiv Β· code. Adversarial wrong-patch generation across the same 500 SWE-bench Verified tasks, six months earlier, peer-reviewed, MIT-licensed, with released datasets. Its equivalence oracle is an LLM judge; this harness uses execution where it runs and human review elsewhere, and reports the verifier's acceptance rate rather than rescoring a leaderboard.
- PatchDiff β Wang, Pradel & Liu, ICSE 2026. arXiv:2503.15223.
Differential patch testing against gold β the same idea
diffexec_oracle.pyrests on, published first. The implementation here is independent and cruder (signature edges, grepped call-sites, LLM-proposed argument tuples) rather than PatchDiff's automated differential test generation; the difference in subject is adversarially-authored patches rather than honest agent patches. - BenchJack β Wang, Li, Mang, Cheung, Sen & Song, UC Berkeley. arXiv:2605.12673. Attacks the harness β scoring code, environment setup, leakage channels β where this attacks patch semantics with the harness intact.
- Hacker-Fixer Loops / Terminal Wrench β Zhong, Raghunathan et al., CMU. arXiv:2606.08960. 1,968 tasks across 5 benchmarks, plus the fixer step this harness does not have.
- UTBoost (arXiv:2506.09289) and STING (arXiv:2604.01518) β test augmentation for coverage gaps.
- Rajan, S. Auditing Reward Hackability in Code RL Training Environments. arXiv:2606.16062 β the 49-task result this run compares against. Unrefereed preprint; code not released.
- OpenAI, Why we no longer evaluate SWE-bench Verified (Feb 2026) β the audit that retired this benchmark. 59.4% of the 138 hardest tasks it examined had material test-design or problem-statement flaws, 35.5% narrow tests. Both figures are over that audited hard subset, not over the 500.
MIT. See NOTICE β the attacker-authored patches here are counter-examples by
construction and must not be used as training data. The canary string covers only 4 records; the notice
says so, and says to filter on the directory instead.