TL;DR
When you ask an LLM to review buggy code, it often mistakes the bug for intended behavior and writes checks that confirm the bug instead of catching it. The fix: don't show the model the potentially broken artifact directly. Instead, first ask it to describe in words what the correct result should look like, and only then have it check the implementation against that description.
Researchers found that if you give an LLM a string-comparison function with a bug where "abc" and StringBuilder("abc") are incorrectly treated as different objects, the model doesn't catch the bug — it writes a test confirming they really are different. In other words, the model looks at the code and thinks "well, if it's written this way, that must be the intent" — then generates a check that matches that flawed logic. Across all tested models, the share of these "bug-approving" tests rises noticeably specifically when the prompt contains buggy code rather than working code.
The method is a two-step process. Step 1: ask the LLM to describe the intent of the method/function in words (as a docstring), looking only at the name and context — not the implementation. Step 2: remove the code from the prompt entirely and replace it with that description — tests are now written against the stated intent, not against a potentially broken implementation.
Method Blueprint
STEP 1 (separate request): LLM writes a specification (docstring) of the code's intent
→ input: function name + context (signature, neighboring methods)
→ output: a text description of "what should happen," with no implementation details
STEP 2 (separate request): The specification COMPLETELY REPLACES the code in the prompt
→ input: only the specification text from step 1
→ output: tests/checks built on intent, not on implementation
Key point: the code isn't supplemented with a description — it's removed and replaced by it. If you leave the code sitting next to the specification, the effect is weaker — the model still peeks at the implementation.
Example in Action
Task: A startup founder sends you a unit-economics breakdown exported from Google Sheets: formulas for CAC, LTV, and margin, with a description of the logic. You suspect there's an error somewhere in the calculation chain (say, taxes weren't factored into cost of goods sold), but you're not sure where.
Prompt (step 1):
You are a financial analyst. Without looking at the specific calculation formulas
below, describe what the CORRECT methodology for calculating unit economics
should be for an e-commerce store: how CAC, LTV, and margin should be calculated,
and what should be included in cost of goods sold. Describe it in general terms,
without regard to how it's actually done in this particular file.
Prompt (step 2, as a separate message):
Here is a reference description of the unit-economics calculation methodology:
{insert the answer from step 1}
Now check the following actual calculation against this methodology
and find all discrepancies:
{insert the actual calculation with formulas}
Result: In step 1, the model produces a general description of "how it should be" — without any regard to the client's specific numbers. In step 2, comparing the actual calculation against this reference, the model is much more likely to spot the missing taxes or unaccounted-for costs than if you'd handed it the calculation right away and asked "check this for errors" — in that case, it tends to accept the existing calculation logic as correct.
Why It Works
LLM weakness: when it sees a finished artifact (code, a calculation, a plan), the model builds an explanation around what it sees rather than critically arguing with the logic. It reads the implementation and thinks "the author decided to do it this way — so that must be right," especially when the bug looks "natural" at first glance.
LLM strength: it's good at reconstructing the general purpose of something from its name and context, as long as it isn't distracted by the details of a specific implementation. Ask "what should the function equals() do" and the model will confidently describe standard comparison behavior, with no knowledge of the bug lurking inside.
The method splits these two modes across separate requests: first, "reconstruct the intent in a vacuum," then, "compare against that intent." That way the model doesn't get a chance to "catch" the bug from the implementation before it's already stated how things should correctly work.
A control lever: the paper includes an "advanced" version of step 1 — ask the model to explicitly flag logical inconsistencies and robustness gaps (edge cases, null checks) before writing the specification. This works better on models without built-in "reasoning" (non-reasoning models), which need to be explicitly told to analyze rather than just describe.
Prompt Template
STEP 1:
You are an expert in {field}. Without looking at the implementation/calculation
details below, describe what the correct result/logic SHOULD be for {object}
based on its name and context: {context}.
First, note any possible logical errors and weak points that could arise
in a task like this, then describe the reference methodology/behavior
without regard to the current implementation.
STEP 2 (as a separate message, without the original material):
Here is a reference description of how things should be:
{answer from step 1}
Check the following material against this reference
and flag all discrepancies:
{original code/text/calculation}
What to fill in: {field} — the domain (finance, code, legal text), {object} — what exactly you're checking, {context} — the name/purpose without implementation details, {original code/text/calculation} — the actual material you suspect contains an error.
🚀 Quick Start — paste this into the chat:
Here's a template for the "specification instead of code" method for checking
material for errors. Adapt it to my task: {your task}.
Ask me questions to fill in the fields.
[insert template above]
The LLM will ask what exactly you're checking and what counts as "correct behavior" for you — because without that, it can't put together a reference description in step 1.
Limitations
⚠️ Two requests instead of one: the method requires an extra step and extra tokens — overkill for a quick, casual check.
⚠️ The specification can inherit the bug: if the error is deeply baked into the logic (say, a misleading method name that sends the model down the wrong path), the model can get it wrong right at the intent-reconstruction step — and then the whole method won't save you.
⚠️ Tested on code, not on text: the study tested the method on unit tests for Java code. Extending it to business documents, texts, and calculations is a reasonable extrapolation of the principle, but the authors didn't verify it directly.
How the Research Was Done
The team used Defects4J — a database of real bugs from open-source Java projects — and selected 318 methods with a known bug alongside its fixed version. 11 different LLMs (from GPT and Claude to DeepSeek and Qwen) generated tests for both the buggy version of the code and the fixed one, and the results were compared.
Key finding: among tests that "pass" on buggy code, over 90% are actually normal tests that would have passed on the fixed code too — meaning previous methods of measuring this "self-deception effect" in other papers were imprecise. The authors came up with a more honest metric: only count a test as "fooled" if it passes on the buggy version but fails on the fixed version — that is, if it literally confirms the bug.
They also looked "under the hood" of the model and calculated a sequence score — how strongly the model prefers one test text over another depending on which version of the code it saw. It turned out that buggy code genuinely shifts the model's internal preferences toward bug-confirming tests — this isn't random generation noise, it's a systematic bias.