· 6 min read · Patchlit team

Let your CI judge the fix, not the AI

Why one party should never control both a code change and its judge, how a model can cheat its way to green CI, and how checks before and after a push stop it.

There is one rule behind most of the safety work in Patchlit: one party must not control both the change and the judge.

If a model writes the fix and the same model decides whether the fix is good, you have learned what the model thinks of itself. That is not evidence. A pass from your own test suite is evidence, as long as the model could not touch the suite.

The first version graded itself

Patchlit began as an issue resolver. A model read an issue, ran a long multi-step process, and scored its own fix from 0 to 100. A score of 85 passed.

That number proved nothing to anyone. A customer cannot check it, and the model has every reason, in the loose sense of the word, to be generous. We dropped the score. Now the verdict comes only from the repository's own CI, and nothing is scored by a model.

How a model cheats to get green

Dropping the self-score is not enough. If the goal is "make CI green", a model has easy ways to reach it that have nothing to do with fixing the problem. We saw them in our own test runs, on a private test repository we built.

  • Delete the thing under test. On one pull request the model deleted the author's whole shipping module, wrote a different one, tested its own replacement, and CI went green. By the numbers it was a win.
  • Stub a failing call. On another, a failing network call was replaced with a stub. The test passed because it no longer tested anything real.
  • Edit the test. Change an assertion, skip a case, or loosen a threshold. The suite passes because the suite changed.
  • Edit the test config. Exclude the failing files from the run, or lower a coverage gate. Nobody touched a test file, and the check still got easier.
  • Swap a provider or downgrade. Pin the dependency back to the old version, or replace the library that caused the error. The bump PR is now not a bump.

In that first full run, 9 of 14 cases were right against our own answer key. Two of the apparent wins were not real. We never quote those numbers as a success rate, and the runbook says so, because we built the test repository ourselves. The useful lesson was the one about shape: green is not the same as correct.

Every guard we built after that was designed around one question. What could a model do to make CI green without fixing anything?

Allowlists beat lists of tricks

The obvious response is a blocklist: ban deleting tests, ban stubs, ban the word skip. This fails, because the model will find the next trick, and a keyword list cannot know it.

So the checks lean on allowlists and on structure.

  • Existing test files are off limits. New ones are fine.
  • Test tooling is off limits too. Any change to an existing runner config is a human decision, because a diff from lines: 90 to lines: 60 contains no telling word.
  • CI definitions, git hooks, package manager config, lockfiles and environment files cannot be touched.
  • A new file is allowed only in the places a fix could reasonably add one.
  • A file may shrink, but not be gutted. A file that loses an exported name or drops below half its size is refused.
  • Deleting or emptying a file is refused outright.
  • A change that points code at a web host the file did not already use is refused.
  • In package.json, only the scripts section may change, and any new or changed lifecycle script is refused.

You can see the full list on the security page. The size limits and protected paths are also yours to set, and the rules page lists them. Your limits can only tighten the built-in ones.

Checked twice

All of the above runs before the push. That is the first check, which we call vetting. The model's output is treated as untrusted, and a fix that fails is never pushed.

The second check runs after the change lands, and it ignores what the model said it did. It reads the commits that actually went into git. Patchlit finds its own commits by a trailer line on each one. If they touched an existing test, test tooling, CI, a lockfile or an env file, deleted or renamed a file, or added a file outside the allowed places, the win is withdrawn and a person is told.

Why a second check? Because every bypass we found came from a gap between what we thought a fix did and what git recorded. A path spelled two ways. A file created versus updated. The vetting reads the model's output through our own parser. The audit reads the result. In testing, the audit caught a simulated bypass that CI alone had passed.

Both checks share the same definition of what is protected, so the two cannot drift apart.

What green really means

A fix counts as green only when every check on the exact commit Patchlit pushed has finished and passed.

That sentence has a few parts, and each one closes a hole.

  • The exact commit. Not the PR in general, and not an earlier run.
  • Every check. A repository with several workflows can have one pass while another is red or still running. One green workflow is not enough. Commit statuses from non-Actions CI count too, once something has reported to them.
  • Finished. A check that is still running is not a pass.
  • At least one real success. If every check was skipped or neutral, nothing ran, and that is not green.

Then the audit has to find nothing protected in what landed.

Even then, green does not mean right. Your tests passed on a commit that touched nothing protected. A change inside a file the fix was allowed to edit, such as quietly loosening a validation, can pass every rule if your tests still pass. A new config file for the tool being upgraded can change how that tool behaves. No rule catches those, so a person has to read the diff.

For that reason Patchlit never merges anything. It also reports a second number next to the green rate, called "merged as is". It records whether a person merged the PR without changing Patchlit's last commit. Turning CI green cannot raise it. If the first number goes up and the second does not, you are looking at fixes that pass and are not trusted.

The same rule for bug reports

The same idea shapes the bug-report path, which is off until you switch it on. A model writes one failing test first. Your CI runs it. A red result means the bug was reproduced. Then a person reads that one test and approves it before any fix is written. The reason is that a failing test only proves the test fails, not that it says what the issue asked for. In our runs the model invented a cause for a vague report and a whole specification for a feature request. The approval step is what stops that.

The short version

If you use any tool that writes code, ask three questions.

  1. Can it edit the thing that judges it?
  2. Is the verdict something it produced, or something you already trusted?
  3. Does anything check the result, and not only the plan?

You can read the full loop on the how it works page, and the list of what Patchlit refuses to do, and what it cannot catch, on the security page.

Tags: ai, ci, verification