abloh overview
abloh is the verification layer for testing AI-written code. When AI writes both the code and its tests, both can repeat the same mistaken assumption. Passing tests show the suite is green, and coverage shows code ran. Neither shows whether the tests would detect incorrect behaviour.
On every pull request, abloh runs two checks inside your CI: it measures which changed lines your tests execute, then plants controlled faults and counts how many your tests detect. An AI review separates real gaps from noise — clearly labelled, never changing the raw result. And on a schedule you choose, a deep audit takes the whole repository through three further engines overnight: patch revert, realistic mutants and invariance.
Why abloh exists.
AI made code and tests faster to produce, but a green suite can still miss required behaviour. Two findings shaped what abloh measures—and why its mutation engine reaches beyond operator flips into the patches and error paths where real faults live.
Regressions
…faults concentrate where faults were fixed: a small set of recently-corrected files predicted the large majority of the next ones. The patch that fixed a bug is executable evidence of what must stay protected.
Error-handling gaps
…according to an analysis of 470 open-source pull requests. AI-coauthored changes contained almost twice as many error-handling and exception-path issues.
of mutants initially reported at Google were judged unproductive. Feedback-driven filtering later raised the productive share from 15% to 89%—why abloh triages results before developers see them.
Diff coverage
Diff coverage emerged from a recurring contradiction in CI: the test suite could be green, repository coverage could look healthy, and the behaviour introduced in the pull request could still remain untouched. The aggregate number was not false; it was answering a question too broad for code review. Years of established coverage could absorb an untested patch without visibly changing the result.
Narrowing the measurement to the diff was less about creating another score than placing uncertainty at the correct scale. An uncovered changed line gives a firm answer: the tests never entered it. A covered line gives a more limited one: execution reached it, but may never have observed or asserted its effect. Research across 31,000 test suites found only a limited relationship between coverage and fault detection (Inozemtseva and Holmes, 2014).
That distinction became a guiding principle: a measurement is only useful while its meaning remains bounded. Diff coverage was never allowed to become a verdict on test strength. It shows which new behaviour the suite had an opportunity to judge, and which it certainly did not, without claiming that execution alone proves the tests were effective.
Mutation engine
Mutation testing rests on a more demanding idea of test quality: tests should earn confidence by reacting when the program is made wrong. A green suite describes one successful execution. Deliberately altering the implementation reveals which behavioural distinctions the suite actually protects.
That shift makes test quality observable, but it introduces a tension. Reproducible mutations are often simple; realistic mistakes are contextual; changes taken from the patch are relevant but unique. Treating all three as one interchangeable score would conceal those differences. The mutation engine therefore preserves three separate views of wrongness rather than forcing one mechanism to represent every defect.
Classic mutation anchors the system in reproducibility. Its transformations are intentionally mechanical: comparisons are reversed, boundaries shifted and conditions removed. The vocabulary cannot describe every real bug, but every intervention can be reconstructed and measured consistently.
That trade-off is deliberate. A test-quality system needs a stable reference before it can safely introduce judgement. Research across 357 real faults found that suites detecting more mutants were also more effective at detecting real defects (Just et al., 2014).
Survivors remain evidence of an undetected difference, not automatic proof of a missing test. AI triage is placed after execution for this reason. It may interpret the evidence, but a probabilistic model is never allowed to rewrite the deterministic result that produced it.
Reverse-patch mutation follows a different line of thought: the submitted patch is itself evidence about which distinctions mattered to the developer. Removing a coherent part of that patch asks whether the test suite has made the new behaviour observable.
This preserves the shape of the actual change—an added guard, a removed path or several edits that only make sense together—rather than translating it into isolated operators. Relevance is gained at the cost of uniformity, so reverse-patch findings remain distinct from the classic score.
The interpretation is intentionally conservative. If the tests remain green, the change was not distinguished under the measured conditions. That does not make the change unnecessary. If the reverted program cannot build or execute, the experiment has not reached the point where the tests can answer at all.
Fixed operators cannot express every mistake that a developer could plausibly make. The wrong field may have the correct type; a requirement may be implemented on one path but omitted from another. These errors derive their plausibility from intent and context.
Language models make that contextual search possible, but their role is deliberately limited. LLMorpheus found that model-generated mutations could resemble defects outside conventional operator sets (Lemieux et al., 2024). That supports using models to broaden the hypotheses under consideration, not allowing them to certify their own suggestions.
The governing separation is simple: probabilistic reasoning may design the experiment, but only execution supplies the evidence. Because the generated population changes with the model and its context, realistic-mutant findings remain outside the reproducible classic score rather than creating a false impression of comparability.
LLM invariant testing
An omitted requirement creates a different problem: there may be no condition, branch or function corresponding to the missing behaviour. The evidence has to begin outside the implementation, with a rule the system was expected to preserve.
This is why requirements, acceptance criteria, schemas and protocols matter more than properties inferred from code alone. Asking the implementation to define its own invariants would make the investigation circular; the same omission could appear in both the program and the rule used to judge it.
Language models provide a bridge from written intent to executable properties. Property-based testing then explores those properties across generated inputs and reduces failures into stable counterexamples, following the tradition established by QuickCheck (Claessen and Hughes, 2000).
The division of authority mirrors the wider design: the model interprets and proposes; execution challenges and records. A reproducible counterexample is meaningful evidence against the stated rule. An unsuccessful search is not proof of correctness—it records only the territory explored. The aim is not to manufacture certainty, but to expand what can be investigated without claiming more than the experiment established.
What abloh cannot catch.
abloh can change existing code and prove whether your tests notice. It cannot inject a fault into behaviour that was never implemented.
To find missing cases, abloh needs a clue from the ticket, types, invariants or surrounding code. If the behaviour is described nowhere, abloh cannot know it was intended.
abloh also cannot reproduce failures caused solely by production infrastructure, unavailable external services or unsupported concurrency schedules.
Documentation
Set up in one command.
- 01Install the GitHub App. Sign in at app.abloh.dev/connect and choose the repositories abloh may see. Installation happens on GitHub; you pick the repositories there.
- 02Run npx abloh init in the repository. It writes two files: the workflow that runs abloh on every pull request — your build steps stay above it, the action pinned to a full commit SHA, GitHub OIDC in place of any long-lived token — and abloh.yml, the policy. Commit both.
- 03Open a pull request. The check appears on it, and every pull request after it, with nothing further to configure.
The CLI.
Or run every command through npx abloh with nothing installed. Node 20.6 or newer.
Writes the workflow and abloh.yml, then warms the toolchain cache so the first run is not cold. --no-prepare skips the warm.
The cache warm on its own: resolves the repository’s toolchain and stores it for later runs. --cache-dir moves where it lives.
The measurement. Plants deliberate bugs in the diff, counts what your tests notice, triages survivors and writes attest-results.json and attest-summary.md.
Exit codes are boring on purpose: 0 for success, 2 for a usage error. The verdict lives in the artifacts and the check, never the exit status.
The policy file.
abloh.yml is read from the pull request’s merge base, so changing it is reviewed like any other code. npx abloh init writes it with these keys and guidance beside each; its own advice on enforcement: leave it false for the first week, look at what it flags, then pick the threshold you believe. The keys you will actually touch:
The catch rate a run must reach to pass. abloh plants deliberate bugs in your diff and counts how many your tests notice; 70 means seven in ten.
Whether a failing gate blocks the merge. false posts the verdict and lets you merge anyway; true turns it into a red X, which blocks only if the check is also required in branch protection.
Run it locally.
The same measurement, on your machine: it diffs base to HEAD, plants its bugs and writes the full report to disk. A dirty working tree is refused — changed lines come from the commits while coverage reads the files, and measuring two different versions at once would be a lie — so for work in progress add --uncommitted, which moves both halves to the working tree together. Local runs never reach the control plane: no check, no upload, no allowance spent. They are unlimited on every plan.
The range being measured: base is required, head defaults to HEAD. CI passes the pull request’s own base and head.
Measure the working tree instead of the last commit — changed lines and coverage move together, so the measurement stays coherent. Untracked files are still refused.
Point the run at one package in a monorepo. Without it, the package is auto-selected when every changed file falls inside exactly one workspace package.
Which abloh.yml to read. Defaults to the one in the repository root, or the built-in defaults when none exists.
Raw score only: skip the AI review of survivors. No model is called.
Measure and review, but propose no test fixes.
npx abloh run --help lists the rest — seeded replays, triage concurrency, time budgets.
The MCP server.
Serves the tests abloh has already proved — each one executed against the planted fault and failing on it, passing on your code — to any client that speaks the Model Context Protocol. Three read-only tools; none of them touches your working tree, and applying a test stays your agent’s act, not the server’s.
Lists the tests abloh has already proved will catch a gap — each was executed against the planted bug and failed on it, passed on your code. One entry per proposal, with the key to fetch it by.
Reads one proposal in full: the body, the file it belongs in, the name it runs under and the gap it closes. Meant to be applied as written — what was proved is that exact test.
The narrow list: only the proven tests somebody on your team read in Slack and deliberately staged for a coding agent, bodies included. An empty answer means nobody has staged anything for that run, not that the run proposed nothing.
Generate a key in workspace settings. Both values below come from there, and both files carry your key — keep them out of commits.
Claude Code, as .mcp.json at the repository root — and Cursor, as .cursor/mcp.json beside it, or ~/.cursor/mcp.json for every project:
{
"mcpServers": {
"abloh": {
"command": "npx",
"args": ["-y", "@abloh/mcp"],
"env": { "ABLOH_API_KEY": "…", "ABLOH_ORG_ID": "…" }
}
}
}Codex, in ~/.codex/config.toml:
[mcp_servers.abloh]
command = "npx"
args = ["-y", "@abloh/mcp"]
env = { ABLOH_API_KEY = "…", ABLOH_ORG_ID = "…" }