Why AI Coding Assistants Silence Errors Instead of Fixing Them
Reward hacking isn't a lab curiosity. It's the try/except your assistant added yesterday. Here's how to catch it in review and in CI.

TL;DR: When you give an AI a failing test or a stack trace, the cheapest way to satisfy you is to remove the signal, not the cause. Researchers have measured this directly. You can't prompt it away with "don't cheat," but you can make it structurally harder and much easier to spot.
A benchmark nobody can pass honestly
In October 2025, Ziqian Zhong, Aditi Raghunathan and Nicholas Carlini published ImpossibleBench. They took real coding tasks and edited the unit tests so they contradicted the task description. An honest solution cannot pass. Any pass is a shortcut by construction.
On the version built from real GitHub issues (SWE-bench tasks with conflicting tests):
- GPT-5 passed 54% of the impossible tasks.
- Claude Opus 4.1 passed 50%.
- Claude models mostly did it by editing the tests. OpenAI models spread it across test edits, special-casing inputs, operator overloading, and recording hidden state between calls.
It isn't one paper. In March 2025, OpenAI reported agents in training rewriting a verify function to always return true and adding a conftest.py hook that skipped every test. In June 2025, METR reported o3 replacing an evaluator with a stub that marked everything correct, and patching the timer on a performance task.
These are stress tests, not a measure of everyday frequency. But they show which way the pull goes: when the real fix is hard and the check is easy, the check gets satisfied.

Why it happens in a normal chat, too
You might assume this is an agent-only problem. Three things push the same way when you just paste an error into a chat:
- The model only sees the alarm. A stack trace says where it hurt, not why. With no view of your data or callers, the error message is the only thing it can act on.
- Coding models are partly trained on checks. Rewarding work that passes automated tests is part of how modern coding models get good. The studies above show that, under that pressure, some of what's learned is how to make checks pass.
- "Works now, thanks" grades the symptom. You confirm the error disappeared. Nobody confirms the cause did.
Retries make it worse. ImpossibleBench found that allowing multiple submissions against the tests raised average cheating from 33% to 38%. The fifth "still failing, try again" is the one to watch.
The seven silencers, in code

1. Swallow
try:
send_welcome(user)
except Exception:
pass
try { await sendWelcome(user) } catch {}
2. Default
email = user.get("email", "") # KeyError gone, empty emails queued
const items = response.items ?? [] // "no data" and "request failed" now look the same
3. Skip
@pytest.mark.skip(reason="flaky") # was it flaky, or was it right?
def test_refund_rounding(): ...
4. Loosen
assert order_total(cart) == 42.50 # before
assert order_total(cart) is not None # after
5. Special-case
def parse_date(s):
if s == "2024-02-30": # the exact input the failing test uses
return None
...
6. Mute the tool
// @ts-ignore
const id: number = row.id
7. Rewrite the checker
def verify(result):
return True
Declared vs undeclared silence
Not every one of these is wrong. A cache miss should be quiet. A default is the spec when the spec says "missing nickname means use first name." Sometimes the test is the bug.
The line that matters is whether the silence was decided. A declared silencer carries its reason: a comment, a linked ticket, a line in the PR. An undeclared silencer is one that nobody chose; it was just the shortest path to green.
A silencer isn't the bug. An undeclared silencer is.
So the review question becomes: did anyone decide this?
Why "please don't cheat" doesn't work
METR appended instructions to o3's prompt on the task it gamed most reliably, 20 runs each:
| Prompt | Runs that gamed the score |
|---|---|
| Original | 80% |
| + "Please do not cheat." | 80% |
| + "Please do not reward hack." | 70% |
| + "use only methods the task's designer likely intended" | 95% |
OpenAI's paper adds a darker note: penalizing agents for visibly reasoning about cheating reduced some of it, but most of what remained was no longer visible in the reasoning.
The model doesn't believe it's cheating. It believes it's fixing your error. Moral instructions miss; structural ones land.

Guardrails that actually move the number
ImpossibleBench tested structural interventions:
- Hidden tests: cheating near zero, but honest performance dropped too.
- Read-only tests: blocked test edits without that cost. Special-casing and operator tricks still got through.
- An explicit way out (the model may stop and flag): GPT-5 fell from 54% to 9%, o3 from 49% to 12%.
Here's how to turn that into your workflow.
1. A prompt contract with an exit
Before changing any code, explain the cause of this error:
what input or state produces it, and where that comes from.
Treat tests, assertions, error handling and type/lint config
as read-only. If a fix seems to need a try/except, a default
value, a skipped or loosened test, or a type ignore, stop and
tell me why instead. Stopping is a good answer here.
2. Make tests read-only for agents
Prompts ask; permissions enforce. If your agent tool supports per-path permissions, deny writes to test directories during fix tasks. At the repo level, a CODEOWNERS entry forces a human review on any test change:
# .github/CODEOWNERS
/tests/ @your-team/reviewers
**/*.test.ts @your-team/reviewers
**/test_*.py @your-team/reviewers
3. Flag new silencers in CI
This doesn't block merges. It makes undeclared silence visible so a human asks "did anyone decide this?"
name: silencer-check
on: pull_request
jobs:
silencers:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Flag newly added silencers
env:
PATTERN: 'except.*: *pass|catch *\([^)]*\) *\{ *\}|\.skip\(|mark\.skip|xit\(|type: *ignore|@ts-(ignore|expect-error)|eslint-disable'
run: |
git diff "origin/${{ github.base_ref }}...HEAD" -U0 \
| grep '^+' | grep -nE "$PATTERN" > silencers.txt || true
if [ -s silencers.txt ]; then
echo "::warning::New silencers added. Each needs a reason in a comment or the PR description."
cat silencers.txt
fi
4. The Wrong-Answer Test
When an AI change turns a red test green, break the expectation on purpose:
assert order_total(cart) == 42.50 # passes after the fix
assert order_total(cart) == 999.99 # must FAIL; if it passes, the test checks nothing
That's mutation testing by hand. If you want it automated, tools like mutmut (Python) and Stryker (JavaScript/TypeScript) do this across a whole suite.
For runtime errors, feed the fixed code the exact input that used to crash it and follow that input one step further. If the empty email now lands in the mail queue, the fire moved rooms.
Key Takeaways
- An error is an alarm. AI can put out the fire or unplug the alarm, and both look like "fixed."
- Measured: on ImpossibleBench, frontier models passed about half of deliberately impossible tasks, mostly by editing tests or special-casing.
- Seven silencers to scan for: swallow, default, skip, loosen, special-case, mute the tool, rewrite the checker.
- Silence is fine when declared. Review for undeclared silence.
- "Don't cheat" barely moved the needle. Read-only tests and an explicit "stop and flag" option did.
- Run the Wrong-Answer Test before merging any AI fix that turned a test green.
FAQ
What is reward hacking in AI coding assistants?
Reward hacking is when a model satisfies the check it's measured by instead of the goal behind it. In coding, that means making a failing test pass or an error disappear by editing the test, catching and ignoring the exception, or hard-coding the expected output, rather than fixing the underlying bug.
Why does AI modify my tests to make them pass?
Because a test is a check, and editing the check is often the cheapest way to turn it green. ImpossibleBench found Claude models used test modification in most of their shortcut solutions. Making test files read-only for the agent, or requiring human review on test changes, removes the easiest path.
Does telling the AI "don't cheat" or "fix the root cause" work?
Not reliably. In METR's experiment, appending "Please do not cheat." left o3's rate of gaming the score unchanged at 80%. Structural changes worked better, especially explicitly allowing the model to stop and report that a fix requires changing a test.
Is it always wrong when AI adds a try/except or a default value?
No. Some exceptions should be handled quietly and some defaults are the specification. The problem is undeclared silence: a handler or default with no stated reason, added because it made the error go away. Require a comment or PR note for each one.
How can I tell if an AI fix actually fixed the bug?
Ask it to explain the cause before it edits anything, then check what now happens to the input that used to fail. For test fixes, run the Wrong-Answer Test: change the expected value to something wrong and confirm the test fails.
Do AI coding agents reward hack more than chat assistants?
The measured cases mostly come from autonomous agents running long loops against tests, where there's more pressure and more access. But the same pull exists in chat: you show only the symptom and approve when the symptom is gone. Repeated "try again" retries increase it in both settings.
Related reading
- AI Writes Code That Looks Right. That's Exactly the Danger.
- Which Developer Skills AI Replaces Next: Sort by the Cost of the Check
- Why AI Feels Smarter About Topics You Don't Know
- How to Actually Work With AI Coding Agents (A Practical Guide for 2026)
The bottom line
A test the AI can edit isn't verification; it's a suggestion. Give the model a legitimate way to say "I can't fix this without changing the check," lock the checks it shouldn't touch, and review every AI fix with one question before any other: what did it touch?
The error message was never the problem. It was the only part of the problem that could talk.
New here? Start Here: A Plain-English Map to Understanding AI Get one of these in your inbox each week: subscribe to the Simply Explained newsletter
Adam Jaber is a software engineer who writes Simply Explained: complex topics, made simple. No jargon, no hype.
Sources: OpenAI, "Detecting misbehavior in frontier reasoning models" (March 2025); METR, "Recent Frontier Models Are Reward Hacking" (June 2025); Zhong, Raghunathan and Carlini, "ImpossibleBench" (arXiv 2510.20270, October 2025).
About the author
- I'm a senior software engineer. I write Simply Explained, a plain-English breakdown of how AI and software actually work, and the quiet ways they break: AI fixes that hide bugs, agents that pick the wrong tool, health checks that take down healthy servers. One symptom, one analogy, one fix per piece. New here? Start with the pinned Start Here guide, or get one breakdown a week by email.
Comments
Join the discussionNo comments yet. Be the first to comment.




