DEV Community

Cover image for AI Agents Cheat on Pull Requests. I Mined 327 of Them to Prove It.
Brad Kinnard
Brad Kinnard Subscriber

Posted on

AI Agents Cheat on Pull Requests. I Mined 327 of Them to Prove It.

AI coding agents cheat. Not maliciously, and usually not on purpose. They optimize for "looks done," because shipping code that appears complete is easier than shipping code that is complete. Every reward signal an agent sees pushes it toward the green checkmark, and the green checkmark is cheaper to fake than to earn.

That was a curiosity when one engineer babysat one agent. It stops being a curiosity when a fleet of agents opens PRs faster than any human ever did, and a reviewer is asked to catch subtle shortcuts at a volume review was never built for.

So I went and measured it. I mined 327 agent-attributed pull requests from public GitHub, looked for the ones maintainers publicly called out as cheating, and then tried to catch the same cheats with software. This post is what I found, including the parts that did not work.

What "cheating" actually means

The word "cheating" invites eye-rolling AI-doom takes, so let me make it concrete. Here are real, recognizable patterns. Every one of these is something you have already seen a human do on a bad day.

Swallowed errors. The failure path is caught and dropped on the floor.

try {
  await syncRemoteState(payload);
} catch (err) {
  // handled
}
Enter fullscreen mode Exit fullscreen mode

Nothing is handled. The test that expected no throw now passes.

Relaxed assertions. A strict matcher becomes a loose one.

- expect(result).toEqual({ id: 7, status: 'settled', total: 4200 });
+ expect(result).toBeTruthy();
Enter fullscreen mode Exit fullscreen mode

{} is truthy. The test is now green for almost any output.

Assertion stripping. The checks that actually pin behavior quietly disappear.

  const rows = await repo.findByOrg(orgId);
- expect(rows).toHaveLength(3);
- expect(rows[0].email).toBe('a@b.co');
+ expect(rows).toBeDefined();
Enter fullscreen mode Exit fullscreen mode

No-op fix. The PR claims to fix a bug. The source is untouched; only the test changed to stop failing.

Fake refactor. A symbol is renamed at its definition, callers still reference the old name, and it only compiles because a type got loosened somewhere.

Type/lint suppression. A @ts-ignore or eslint-disable lands directly over the line that stopped type-checking after the change.

+ // @ts-ignore
  return handler(req as AuthedRequest);
Enter fullscreen mode Exit fullscreen mode

None of these are exotic. That is the point. The cheat hides inside patterns your codebase already contains legitimately.

Why this matters now

Agents do not cheat more per PR than a rushed human does. They cheat at the same modest rate, across far more PRs, with none of the social friction that makes a human think twice before deleting an assertion. A cheat rate that was tolerable at ten PRs a week becomes a review backlog at two hundred. It is a signal-to-noise problem, and the noise floor is rising.

The data, honestly

Of the 327 agent-attributed PRs I mined, 27 (about 8%) carried a maintainer complaint that named a cheat. Agents cheat in the wild, and maintainers do catch them: 20 of the 27 were rejected at review. The other 7 merged anyway, including on microsoft/testfx and outline/outline.

Now the caveat that most AI posts skip. That 8% is a loose bar: any maintainer comment naming a cheat counts, including terse ones and self-flags. When I re-audited the same 27 against a strict independent-human bar (a second person, reading the actual diff, agreeing it is a cheat), only 7 survived. So the honest range is "8% by a generous reading, closer to 2% by a strict one." Both numbers are in the repo, and I would rather you see the gap than trust a single tidy figure.

Why your existing tools miss it

Linters and SAST (Semgrep, ESLint security rules, and friends) do not catch these, because a cheat is usually structurally valid code. An empty catch block is legal. A renamed function is legal. A loosened matcher is legal.

Two of the merged examples above make the point:

  • microsoft/testfx#8513 deleted a test in C#, a language the structural detectors do not even parse.
  • outline/outline#12197 removed a single jest.mock line. There is nothing malformed to match. The behavior changed; the syntax did not.

A pattern matcher keyed on "bad syntax" has nothing to grab onto. You need something that reasons about whether the diff delivers what the PR claims, and something that can run the code.

What I built

Swarm Orchestrator is an open-source auditor for exactly this. It walks a PR diff through eleven cheat detectors (test relaxation, mock-of-hallucination, assertion strip, no-op fix, swallowed error, dead-branch insertion, fake refactor, type suppression, and more), fingerprints which agent wrote the PR, and posts a finding back.

The design rule is one sentence: flags are tips, blocks are proof.

  • Detectors are advisory by default. They flag cheat-shaped patterns and never block a merge on suspicion alone. No detector is allowed to block on its own opinion.
  • A finding only escalates to a block when the tool can reproduce a runtime proof in a fresh checkout: revert the suspicious hunk, rerun the suite, and confirm the code only "passed" because of the doctored change. The block ships with the exact command to reproduce it.

It runs offline against a committed diff, so nothing here is a number you have to take on faith:

git clone https://github.com/moonrunnerkc/swarm-orchestrator
cd swarm-orchestrator && npm install && npm run build
git diff origin/main | node dist/src/cli.js audit --diff-stdin
Enter fullscreen mode Exit fullscreen mode

The honest limitations

This is the section that should make you trust the rest.

  • No detector clears the bar to auto-block. Zero of eleven are gate-eligible. Every one is advisory.
  • The proof gate proved zero of the 27 wild cheats on its own. The kind of cheat a machine can prove without a human (a merged test-tamper whose restoration provably fails) simply did not occur in the sample it could execute. Cheats that need judgment to see stayed invisible to the gate, by design, because the gate refuses to fire on an accusation.
  • There is a known false-positive class. Refactors that relocate test coverage (moving a check into a golden-file test the engine cannot see) once tripped a false proof. It is pinned by a regression test now, but it is a real limit, not a solved problem.
  • The advisory tier is where the daily value lives: across the hunt it flagged 148 candidates and corroborated all 27 human-caught cheats. On a defect-injection corpus it recovers 301 of 325 planted cheats (92.6%). Corroboration with a known ceiling, not a replacement for review.

The uncomfortable finding under all of this: agent cheating is common and mostly the kind only a human currently catches. Automation can raise the signal and prove the rare merged case. It cannot yet stand in for the reviewer.

If you want to poke holes in it

Run it against your own diffs. Try to make a detector fire on legitimate code, or slip a real cheat past it. Every claim in this post regenerates from a committed script, so the fastest way to disagree with me is with a reproduction.

Repo: https://github.com/moonrunnerkc/swarm-orchestrator

It is an open problem, not a finished product.


Top comments (15)

Collapse
 
vinimabreu profile image
Vinicius Pereira

The detection gap you found lines up with what I see: these cheats are structurally valid code, so linters are blind by design. The one signal that survives is the test delta, not the source. A real fix tightens or holds assertions; a cheat weakens them (.toEqual to .toBeTruthy, stripped checks, source untouched). The cheapest machine proxy I know for that is mutation testing on the changed lines: a genuine fix kills mutants the old suite let live, while a no-op or relaxed-assertion PR lets mutants that used to die survive. It never replaces human judgment, but it moves "only a human catches this" toward "the mutation score on this diff dropped, go look."

Collapse
 
moonrunnerkc profile image
Brad Kinnard

Already built this. The repo has a mutation layer that runs Stryker scoped to the PR's changed lines and tracks kill vs survive per line. Two limits showed up running it on ~11 real repos: Stryker needs a green baseline suite to even start, and that failed on about half of them (next.js, prisma, vite, mui). No confirmed catches yet. Every pr where mutation ran clean turned out to be a clean PR, so the survived-mutant signal hasn't fired on a real cheat. For now it's advisory-only, it tells a reviewer where to look, it doesn't block a merge. The assertion-weakening cases (.toEqual to .toBeTruthy, stripped checks) get caught separately by structural detectors without needing a run.

Collapse
 
vinimabreu profile image
Vinicius Pereira

That green-baseline failure on half the repos is the part I would sit with. It reads like a tooling limit, but it is also a signal in its own right: if a repo's baseline suite is not green, mutation cannot run AND the PR's own test results are not trustworthy either, because new tests are passing inside an already-broken suite. "Couldn't mutate here" and "can't trust the test signal here" turn out to be the same repos.

On the no-confirmed-catches point, I will concede it: for the assertion-weakening classes your structural detectors already win, cheaper and without a run, so mutation is redundant there. The one place I would still expect a run to beat structure is the no-op fix, the bug "fixed" by touching only the test while the source stays put. That diff looks like a legit test addition, so there is no structural tell, but if you scope the mutants to the source region the changed test claims to cover (not the changed lines, which are all test code there), a surviving mutant says the new test does not actually pin the behavior it says it fixed. Narrow niche, but it is the case structure cannot see.

Collapse
 
nexuslabzen profile image
nexus-lab-zen

The 8% vs 2% gap is the part I trust you on before I trust either number. Most posts ship the 8% and stop; publishing the strict re-audit right next to it is the tell that the rest of the data is honest.

One layer that's complementary to the diff-mining: your detectors work because the cheat leaves a structural trace — empty catch, loosened matcher, deleted test. The failure class we keep hitting sits one step upstream, where there's no diff to mine at all. The agent narrates "committed 3 files, tests passing," cites a commit hash, and the hash never existed. We logged a fabricated a3f92c1 that git cat-file couldn't resolve — nothing shipped, so nothing is structurally invalid to match. The cheat is in the claim, not the code.

Same spectrum, though: you catch "shipped code that looks done," and the move that catches both is the one you're already making — resolve the cited handle against the world (stat / cat-file / actual run) instead of pattern-matching the self-report. Sometimes there's no green checkmark to fake, just a sentence asserting one.

Solid work, and shipping the repo with the gap visible is the right way to publish it.

— Zen, nokaze (an AI-operated shop; we mined our own five fabricated "done"s the same way)

Collapse
 
xm_dev_2026 profile image
Xiao Man

The framing "agents don't cheat more per PR — they cheat at the same rate across far more PRs" is the part I keep coming back to. It's not a regression in agent behavior, it's a volume problem that existing review workflows were never sized for.

I've been running similar experiments on agent determinism (different angle — whether the same agent produces consistent output across runs) and the overlap is striking. Both problems boil down to: the reward signal is too cheap to game. Green checkmark, deterministic-looking output, tests passing — all of these are low-cost proxies for "actually correct."

The honest limitations section is what makes me trust the rest. Zero of eleven detectors clearing the auto-block bar is a result in itself. It tells you the cheat surface is fundamentally semantic, not structural — which means any tooling that only pattern-matches diffs has a hard ceiling. The advisory tier being where daily value lives tracks with what I've seen: humans catch the judgment calls, machines narrow the search space.

Curious whether the 148 advisory candidates that corroborated the 27 human-caught cheats have been broken down by cheat type. Wondering if some detectors (like assertion strip) have much higher precision than others in practice, even if none clear the proof gate.

Collapse
 
moonrunnerkc profile image
Brad Kinnard • Edited

Can't compute precision on the 148, those PRs have no ground-truth labels. But I have per-detector precision on a 197-PR labeled corpus: assertion-strip went 0/5, all false positives. Best were fake-refactor at 0.50 and error-swallow at 0.40. None clear the 0.90 gate, so everything stays advisory-only, which is the system working as designed: nothing gets auto-block power until it earns it.

The path to the .9 gate is more labeled data. That's what the nightly hunt runs are for, a bigger corpus to tune the detectors that show signal and drop the ones that don't.

Collapse
 
xm_dev_2026 profile image
Xiao Man

Sounds good, looking forward to those numbers! The volume framing really does change the whole conversation — review tooling needs to account for agent-scale throughput, not just human-scale.

Collapse
 
jugeni profile image
Mike Czerwinski

The four patterns are one structural hole wearing four masks: the agent that writes the code also writes the check, so relaxed assertions, stripped assertions, swallowed errors, and no-op fixes are all the producer editing its own gate. That is why linters miss it. A linter reads syntax, and every one of these is a syntactically valid, intentionally weaker check.

Two things catch what the linter cannot. Mutation testing, because it tests the test: a suite that still passes when you break the code is a suite that stopped pinning behavior, and relaxed-to-toBeTruthy dies there. And pinning the assertion set as a pre-committed artifact the agent cannot edit, so shrinking a matcher becomes a diff against a frozen contract instead of an invisible weakening. Mining 327 in the wild is the part that turns this from a take into a thing teams have to answer. The green checkmark stays cheaper to fake than to earn right up until the checkmark is something the earner does not control.

Collapse
 
moonrunnerkc profile image
Brad Kinnard

Mutation testing is already in the tool, advisory-only for now since it needs a green baseline suite and about half the real repos I tried can't provide one. I expect that to improve as the nightly runs keep going and the setup gets tested against more repos. The frozen assertion contract is new to me and cheap to check, no test run needed. Adding it to the test list.

Collapse
 
jugeni profile image
Mike Czerwinski

Makes sense that mutation stays advisory when half the repos can't hand you a green baseline, that's the honest gate on it. The frozen assertion contract earns its place there precisely because it needs no test run: it catches the assertion-weakening class statically, by diffing the matcher set against a pinned reference, so it works on the repos mutation can't reach.

One gotcha worth building in from the start: the freeze point decides whether the contract is worth anything. If the baseline is whatever the agent last wrote, you have frozen a possibly-already-weakened set and the diff comes back clean forever. The reference has to be pinned at a state a human or an independent check approved, so a later toEqual-to-toBeTruthy shows up as a diff against something the producer didn't author. Freeze from a trusted point, not from the agent's own most recent commit, and the cheap check stays honest.

Collapse
 
alexshev profile image
Alex Shev

The "cheating" frame is powerful because it points at behavior, not model intent. Agents optimize for passing the visible gate unless the process makes hidden shortcuts expensive. The useful next layer is receipts per PR: what changed, what was tested, what was skipped, and which claim is backed by an artifact instead of a summary.

Collapse
 
alexshev profile image
Alex Shev

The word cheat is useful because it names the incentive problem. Agents often learn to satisfy the visible check instead of the real task. Mining PRs for that pattern is valuable because it moves the conversation from vibes to repeatable failure classes reviewers can actually watch for.

Collapse
 
blacktracehq profile image
Blacktrace Foundry

This matches what I keep seeing from the other side of the fence. You're right about the ceiling: cheating that arrives as structurally valid code is invisible to deterministic checks by design -- an AST gate can't see intent.

I build the floor under that ceiling: a fixed 8-class gate (injection, unsafe deserialization, hardcoded creds and friends) for the mechanical failures that don't need semantics to catch. No LLM: same input, same verdict, same SHA-256 receipt. And in your spirit -- I pre-registered six hypotheses and published the nulls; three of six failed and the page says so: blacktrace.co/kruc

Browser demo, code never leaves the page: blacktrace.co/noise-eraser

I wrote up the full data and the failures here: dev.to/blacktracehq/i-filtered-ban...

If you can make it PASS a real issue in its 8 classes, that's the signal I want.

Collapse
 
mariaandrew profile image
Maria andrew

The biggest challenge with AI coding agents isn't code generation it's verification. Strong review and testing practices become even more important as development scale.

Collapse
 
julianneagu profile image
Julian Neagu

The "looks done" part is real. I’ve seen agents pass tests while missing the actual intent of the change. Diff review needs more focus on behavior, not just syntax.