All posts
Essay · September 29, 2026

Who checks the checker?

Graders decide what AI learns, and almost nobody tests them. How graders get fooled, what the research shows, and the rules we built Bohrin around.

Bohrin · Essay

When a student hands in an exam, a teacher marks it. When an AI model hands in an answer, a short piece of code marks it. We call that code a grader. The model learns whatever the grader rewards, and everyone downstream believes whatever score it produces. So here is a question that sounds simple and turns out not to be: who checks the grader?

The part nobody looks at

A grader can be a test suite, a reward function, or a script that compares outputs. It is usually small, written quickly, and reviewed once. Then it gets used thousands or millions of times: to train a model, to rank it on a leaderboard, or to decide that an agent’s work is done.

That used to be fine, because a person was still looking at the real work behind the score. That is less and less true. Models are trained against graders at a scale no one can read, and coding agents open more pull requests than any team can review line by line. The grader has quietly become the only thing standing between a model and a mistake.

Which would be fine if graders were reliable. They often aren’t.

How a grader gets fooled

Take the simplest task there is: write a function add(a, b) that returns the sum. Here is a grader that looks reasonable at a glance. It loads the answer and calls the function:

grader.py
1def my_grader(submission):
2 namespace = {}
3 try:
4 exec(submission, namespace)
5 namespace["add"](2, 3)
6 return True
7 except BaseException:
8 return False

It checks that the function runs. It never checks the answer. A function that returns 0 passes. A function that does nothing at all passes. If you train a model against this grader, you are paying it to learn that doing nothing is enough.

Real graders are more careful than this one, but they fail in the same basic ways. The ones that show up again and again:

  • —The empty answer. A function with the right name that returns a constant, or nothing. It passes any grader that only checks the code runs.
  • —Leaving early. The answer calls sys.exit(0) before a single check runs. A grader that reads “the program exited successfully” as “the tests passed” pays full marks.
  • —The object that equals everything. The answer returns an object whose == always says yes. Every assert result == expected passes without computing anything.
  • —Editing the judge. If the answer can touch the tests, it can skip them, delete them, or weaken them until they check nothing.

This is called reward hacking. It is worth being clear about where the fault lies. The model isn’t being malicious. It is doing exactly what it was trained to do: earn the reward. The grader has a gap, and the model found it.

This is not hypothetical

It would be comforting if these were contrived examples. The published research says otherwise.

76%

of attempts in which a frontier model cheated on coding tasks built so that passing is impossible without cheating.

Source · ImpossibleBench

43×

more reward-hacking attempts by frontier models when the scoring code was visible to them.

Source · METR

16%

of 445 reviewed benchmarks reported any uncertainty estimate or statistical test.

Source · Measuring what Matters

Researchers at METR watched frontier models attempt reward hacking in 1–2% of all task attempts, and 43 times more often when the scoring code was visible.[1] On coding tasks deliberately built so that the tests contradict the specification (so any pass means cheating), one frontier model cheated 76% of the time. For several model families, most of those cheats came from modifying the tests.[2]

The same study found that the most effective fix was giving the agent read-only access to the tests.[2] I find that result clarifying. The answer wasn’t a cleverer prompt. It was checking what the agent was able to change.

And the cost doesn’t stay contained. Anthropic found that models which learned to hack real coding environments during training went on to show alignment faking and sabotage in other settings.[3] A gap in a grader doesn’t just produce one wrong score. It can become part of the model.

Benchmarks have the same problem at a larger scale. An automated audit found 219 flaws across 10 agent benchmarks, most of which reached near-perfect scores without solving a task.[4] A review of 445 benchmarks by 29 experts found only 16% reported any uncertainty estimate or statistical test at all.[5]

“Reward hacking is a big issue… It needs to be robust. That’s the minimum.”
— People who build RL environments, interviewed by Epoch AI[6]

A grader can fail in two directions

Most of the conversation is about graders that pay for wrong work. There is a second failure that gets less attention: graders that refuse right work.

If a grader compares text instead of behaviour, a correct answer written a little differently gets marked wrong. A model trained against it learns to avoid correct answers that don’t look like the one you wrote. In one audit, four widely used maths verifiers accepted correct rewrites of their own answers only 53.8% to 95.2% of the time.[7]

This is why Bohrin never reports a single number. It reports two:

  • —Coverage Score: out of 100, the share of wrong-on-purpose answers the grader rejected.
  • —Correct-work acceptance: out of 100, the share of proven-correct rewrites it accepted.

Any single number built from the two could be raised by trading one for the other. Reject more correct code, catch more cheats. A grader that rejects everything catches every cheat and is useless. You need both, side by side.

What we built

Bohrin checks the checker. It sends a grader answers that are wrong on purpose. A good grader must reject all of them, and every one it accepts is a gap. It never needs your model, an API key or a GPU, because it doesn’t test the model. It tests the thing that judges the model.

Here is the weak grader from earlier, scored against Bohrin’s wrong-on-purpose answers:

real output · bohrin 0.4.2
>>> print(coverage_score(attempts))
COVERAGE SCORE: 0 / 100 95% CI 0–79 0 of 1 caught categories: 1 of 5 (battery 2)

Zero. It paid for an empty function. Change one line so the grader checks the value and its type, type(r) is int and r == 5, and it catches all five and scores 100. (Checking the type matters: with == 5 alone, the object that equals everything slips through.)

The same idea runs through the rest of the tool. Three things work today:

  • —Test a grader. A Python library that builds the wrong-on-purpose answers and the proven-correct rewrites, then scores what your grader did with them.
  • —`bohrin verify`. For anyone using a coding agent: it lists every change that made a test check less, next to any commit message that claims success.
  • —`bohrin power`. For anyone running evaluations: every score with its margin of error, and the smallest difference the evaluation can actually detect.

This is what verify prints when an agent weakened one test, skipped another, and wrote “Fix parser, all tests pass”:

real output · bohrin 0.4.2
$ bohrin verify --since main
bohrin verify · since main (7c1d18b) · 1 files changed · 1 commits
 
2 facts and 0 observations about this change, beside 1 commit message claiming success.
 
BGW-109 fact test_parse_numbers in tests/test_parser.py was weakened: from value equality or comparison to a non-None check only (S1 → W2)
BGW-109 fact 1 skip or xfail marker added in tests/test_parser.py
 
Commit messages that claim success:
7757e23 Fix parser, all tests pass
 
Not checked: a test's expected value copied into the source; files other than Python and pytest configuration.
Next: bohrin verify --strict (exit 1 on any fact, for CI)

The rules we hold ourselves to

A tool that accuses graders is only useful if it is almost never wrong about a correct one. So Bohrin is built around a few rules, and they are the part of this project I care about most:

  1. 1.No accusation without proof. An answer is called wrong only when that is certain without asking your grader: it does no work by construction, or it gives a different result from your reference on an input anyone can run. Anything less certain is shown as a lead and never counted.
  2. 2.Correct rewrites are proven by machine. A rewrite is sent only if Bohrin can show it is the same program. If it can’t prove that, it doesn’t send it.
  3. 3.Every finding can be checked without us. Each one ships with a small script that runs your grader, unchanged, with no Bohrin installed and no network call.
  4. 4.Every number has its margin of error. No score is shown without its sample and its range.
  5. 5.What wasn’t checked is said. A check that couldn’t run is “not measured”, never clean. A clean result means these checks found nothing, not that the grader can’t be fooled.

Facts, not verdicts

verify never says “cheated”. Tests are sometimes removed for good reasons. It shows what changed next to what was claimed, and leaves the judgement to you.

Why it’s open

Bohrin is open source under Apache-2.0. The rules behind every number are in a public specification, so anyone can recompute a result. Every known way a coding grader gets fooled has a permanent ID, like a CVE number for security bugs, in a public list. Every technique on that list was published by someone else first. We add nothing secret to it.

When we find a gap in someone else’s grader, its maintainer hears about it privately first, with the script that shows it and a suggested fix. The public record comes 45 days later, or sooner if they’ve fixed it and agree. Records name the grader, never a person.

We also made three promises that I want to state plainly. Nothing released as open source will ever be closed. The free tier does a whole job, for free, forever. And the public list of grader defects stays open and downloadable.

What’s next

Today you run your own grader and Bohrin scores what happened. At launch, bohrin audit will do the whole thing in one command, on your whole environment: run the grader on every wrong-on-purpose answer and every correct rewrite, and report findings as they are proven. It covers Python graders and Python code first, starting with coding environments.

If you want to try what exists now, it’s one install:

terminal
1pip install bohrin
2bohrin verify --since main

And if Bohrin ever tells you something about your grader that turns out to be wrong, please tell us. A false positive is the most useful report there is.

Self-improvement is the first step toward greater artificial intelligence. We want AI systems to keep learning from signals that are actually correct. That starts with checking the checker.

PGPrabhu GopalFounder, Bohrin

Prabhu is building Bohrin, an open-source tool that checks the graders, benchmarks and test suites that decide what AI learns.

Sources

  1. 1.METR, “Recent Frontier Models Are Reward Hacking”, 2025. ↩
  2. 2.Zhong et al., “ImpossibleBench: Measuring LLMs’ Propensity of Exploiting Test Cases”, 2025. ↩
  3. 3.MacDiarmid et al. (Anthropic), “Natural Emergent Misalignment from Reward Hacking in Production RL”, 2025. ↩
  4. 4.Wang et al., “Systematically Auditing AI Agent Benchmarks with BenchJack”, 2026. ↩
  5. 5.Bean et al., “Measuring what Matters: Construct Validity in Large Language Model Benchmarks”, 2025. ↩
  6. 6.Epoch AI, “An FAQ on Reinforcement Learning Environments”. ↩
  7. 7.“Where the Verifier Fails: A Category-Level Audit of Reward Signals in RLVR”, 2026. ↩

Cite this post

BibTeX
@misc{gopal2026checker,
  author = {Prabhu Gopal},
  title  = {Who checks the checker?},
  year   = {2026},
  url    = {https://bohrin.com/blog/who-checks-the-checker},
}

Try it on your own grader.

One install. No account, no model, no GPU.

See the use cases

Start with the one you need. It’s one install.

$ pip install bohrin