GUESTBOOK.EXEcontact.exe
CTXBENCH

</ctxbench>

CTXBENCH

A/B benchmark for AI-agent context layers. Its first null was my own product.

+4.5pp

completion delta, 95% CI [−13.6, +13.6] — no detectable effect

3 of 24

tasks flipped when the control arm re-ran against itself — 2 excluding a harness timeout

945 tokens

the real MCP schema tax — the experiment was designed around ~9,800

SPECIFICATIONS

ROLESOLO BUILD
YEAR2026
TYPEBENCHMARK HARNESS
STATUSPUBLISHED
STACKtypescript · node.js · vitest · zod · mcp · claude code
LINKS[live ↗][github ↗]
AVAILABILITYnpm · ctxbench

Every vendor selling a context layer for AI coding agents — a memory MCP, a shared-context service, a CLAUDE.md convention — claims it makes the agent better.

ctxbench is a public benchmark harness that A/B-tests whether an AI coding agent's context layer changes what it actually finishes — two arms, one file of difference, graded only by whether the target repository's own test suite goes green.6].

README.TXT: CTXBENCH (14 KB)[full readme →]

== WHAT IS THIS ==

────────────────────────────────────────

ctxbench is a public benchmark harness that A/B-tests whether an AI coding agent's context layer changes what it actually finishes — two arms, one file of difference, graded only by whether the target repository's own test suite goes green. Its first experiment ran against Aethereum, the context product its author also builds, and returned a null: +4.5pp completion delta, 95% CI [−13.6, +13.6]. The control arm was then re-run against itself and disagreed with itself on 3 of 24 tasks, a noise floor at least as large as the signal. Every published figure regenerates from committed raw artifacts on a stranger's machine.

== </the problem> ==

────────────────────────────────────────

Every vendor selling a context layer for AI coding agents — a memory MCP, a shared-context service, a CLAUDE.md convention — claims it makes the agent better. Almost none of them have measured it, and the people with the strongest incentive to measure are the ones a reader should trust least to do it. I was building one of those products, and the question I could not answer honestly was whether it changed what an agent actually finished, as opposed to whether it felt like it did.

role & context

Sole author. I designed the experiment, built the harness, wrote the defects and the fixtures, ran the batches, and published the result — which went against the context product I also build. That conflict is why the analysis was pre-registered before the harness existed, and why the limitations section of the README is generated from artifacts rather than typed.

== </my approach> ==

────────────────────────────────────────

Two arms, everything else held constant: the agent alone, and the same agent plus one context provider, on the same hand-authored defects, at the same pinned model, graded only by whether the target repository's own test suite goes green. The property the whole design turns on is that the only difference between the arms is one file — both arms invoke the same binary with the same flags, and only the MCP config path differs. A unit test asserts exactly that, and a runtime assert re-checks the agent's own startup event on every rollout. Because the author of the benchmark also builds the product under test, the analysis was pre-registered before the harness existed: arms, metrics, statistical method, the minimum detectable effect, and the exact wording used to interpret a result, all fixed before a single number was seen.

== </the story> ==

────────────────────────────────────────

I was building a context layer for AI coding agents and could not answer, honestly, whether it worked. Every product in that category claims the agent gets better with it. Almost none of them have measured it, and the ones with the most to gain from measuring are exactly the ones a reader should trust least. ctxbench is that measurement, built by precisely the person you should not trust to build it, and arranged so that a favourable result would have been hard to manufacture.

The design has one property that everything else serves: the only difference between the two arms is one file. Both arms shell out to the same agent binary with the same flags, and only the MCP config path differs. A unit test pins that. A runtime assert re-checks the agent's own startup event on every single rollout — expected MCP servers all connected, zero leaked skills, the pinned model — and a rollout that fails the check is thrown away and logged rather than quietly retried until it passes.

The plan was wrong about its own founding assumption. Twelve findings were probed first-hand before any code was written, and three of them killed planned approaches: the flag the entire purity design rested on pins the agent to three tools and cannot be widened, Docker was not on the machine, and both planned task sources were unusable. The largest correction came later. The experiment had been designed to control for an MCP schema tax of roughly 9,800 tokens. Measured on an identical no-op prompt, three replicates identical to the token, it is 945. The agent registers all 30 tools and never puts their schemas in context. The premise the control was built for did not exist.

Then three bugs, each of which would have published a false result. A macOS path bug meant no test file ever matched its grading entry, so every rollout in both arms graded unresolved — a perfect false null, caught only because a trajectory was read by hand. A design flaw left each defect as an uncommitted change, so the agent's first orienting command printed the exact inverse of the fix. And the reporter selected run directories lexicographically, printing superseded numbers under a correct-looking heading. Four more were caught by gates rather than by luck, including a fixture that spelled out the fix for its own task verbatim, and live bearer tokens committed under a dot-directory the analysis code skipped.

The result was a null. Aggregate paired delta +4.5pp, 95% CI [−13.6, +13.6], McNemar p = 1.000, with only 3 of 22 tasks discriminating between the arms against a pre-registered floor of six. And then the accident that is the most interesting thing in the repository: a third arm's batch re-ran the control arm with an identical flag set on the identical task set — an unplanned partial replicate, and exactly what a single-repeat design is supposed to be blind to. It disagreed with itself on 3 of 24 tasks, against a between-arm difference of 1 task net. The noise floor was at least as large as the signal. It also dismantled the one efficiency story that looked like a finding: three control rollouts that took 1128s, 551s and 1354s finished in 82s, 97s and 68s on the replicate. "The context layer makes the agent 68% faster" would have been arithmetically true and substantively false.

The README leads with the null, gives the self-disagreement more room than the headline, and states the conflict of interest down to the npm account the package ships from.

== </architecture> ==

────────────────────────────────────────

A CLI drives a batch runner — randomised interleave, resumable, with a cost guard against a locked budget ceiling — which drives one rollout at a time. Each rollout crosses two seams.

Sandbox prepares a pristine workspace: clone at the pinned commit, install from the committed lockfile, apply the defect patch, and record the exact toolchain into every artifact. Provider returns an AgentConfig, and that is the only channel a context layer has. A provider may influence the agent through AgentConfig plus the context files it declares, and never through the prompt, the sandbox or the grading. Declared files are diffed against git status and the rollout aborts if any other tracked file changed, so a provider owns its context and cannot touch the code under test. That restriction is what keeps the single-difference property true no matter who writes the provider.

The agent runs headless with streaming JSON output, and the first thing the harness does with that stream is assert on the startup event. Pass, and grading runs: resolved means every fail_to_pass test flips and every pass_to_pass test holds, in the target repository's own suite, with no model anywhere in the loop. Fail, and the rollout is marked invalid_environment, excluded from both arms symmetrically, and logged.

Everything lands on disk as a trajectory, a run record and a report per rollout, plus a run summary. The statistics layer — paired deltas, McNemar, BCa bootstrap, medians beside means — reads only those files, and the reporter injects its output into the README between markers. Providers are loaded by dynamic import only, with a test asserting on the source that no static provider import exists, so "runs with zero vendor code installed" is machine-enforced rather than intended. LocalSandbox is the v1 default because Docker was absent; DockerSandbox ships implemented, auto-selects when a daemon answers, and is labelled unexercised.

== </key features> ==

────────────────────────────────────────

One file of difference, machine-enforced

Both arms invoke the same agent binary with the same flags; only the MCP config path differs. A unit test asserts the two argv arrays differ in exactly that element, and every rollout re-checks the agent's own startup event for the expected MCP servers, zero leaked skills and the pinned model. A rollout that fails the check is discarded and logged rather than retried into the data.

A provider interface, with the control arm as a provider

Context layers plug in behind a small interface, and the control arm implements it too — so the control path runs the same code as the treatment path and a harness asymmetry cannot masquerade as an effect. Three reference providers ship (none, a hosted MCP, and a static CLAUDE.md) and all three pass the same parameterised conformance suite, which is the artifact you run against your own layer.

Tasks that cannot have been memorised

Rather than SWE-bench, the defects are hand-authored and injected into pinned commits of two real TypeScript repositories, so the bug never existed in any training corpus. Each task must prove itself against an oracle gate — broken in exactly the declared places, and fully fixed by reversing its own defect patch — or it is deleted rather than patched around.

Numbers that regenerate on a stranger's machine

The raw trajectory, run and report files for every rollout are committed, and the report command rewrites the README's results tables from them, between markers that make any hand-edit revert on the next run. The "where it does not help" section is a build output, not prose, so it cannot drift into optimism by being edited.

The conflict of interest written down, not left to be found

The README leads with the null, gives the control arm's disagreement with itself more room than the headline result, and states that the npm package ships from the same account that ships the product under test. The package carries no provenance attestation, and that is disclosed rather than implied away.

== </key decisions> ==

────────────────────────────────────────

DECISION 01

Drop the flag the design was built on, and buy arm purity with an explicit flag set plus a runtime assert. `--bare` caps the agent at three tools and demands an API key, which would have floored completion in both arms and compressed the very effect under measurement. The replacement flag set was verified to produce zero MCP servers and zero skills, a unit test asserts the two arms' argv arrays differ only in the MCP config path, and every rollout re-checks the agent's own startup event. A rollout that fails the assert is discarded and logged, never silently retried into the data.

DECISION 02

Grade by test execution only — no LLM-as-judge. A benchmark of an AI product graded by an AI is a claim nobody has to believe. Resolved means every fail_to_pass test flips and every pass_to_pass test holds, in the target repository's own suite. It costs real sensitivity, since a partially correct fix scores zero, and that is part of why completion sat near ceiling. It buys a result a skeptic can re-run.

DECISION 03

Make the gold patch the defect patch reversed, not a separate file. Two files drift. A gold patch that no longer inverted its defect would silently break the one mechanism guaranteeing every task is solvable, and nothing would fail loudly — the benchmark would just start quietly measuring impossible tasks. The oracle gate proves solvability by actually reversing the patch and running the suite.

DECISION 04

Make Sandbox a seam rather than a hard Docker dependency. Docker was absent on the build machine, and a hard container requirement would have made v1 unrunnable rather than merely imperfect. LocalSandbox pins by commit, lockfile and recorded toolchain; DockerSandbox ships and auto-selects when a daemon answers. The trade is that the host toolchain becomes a variable — disclosed, and mitigated by always running both arms of a task on the same machine in the same session, which makes it a shared confound rather than a differential one.

DECISION 05

Pre-register the analysis, and generate the results and limitations sections from artifacts rather than typing them. The author of the benchmark also builds the product under test. Fixing the metrics, the statistical method, the minimum detectable effect and the exact interpretation wording before any result existed — and putting the honest section between markers that the next report run overwrites — removes the places where a favourable result could be manufactured after the fact.

DECISION 06

Report medians beside means, and label a tail-driven effect as one. The first efficiency pass showed −68% wall-clock and −31% output tokens with intervals excluding zero, and both were carried by a handful of very long control rollouts while the medians were flat or pointed the other way. Publishing the mean alone would have been true of the arithmetic and false about the experience.

== </what i learned> ==

────────────────────────────────────────
>

A false null is the easiest result in the world to publish. A path bug meant no test file ever matched its grading entry, so every rollout in both arms came back unresolved — plausible, quiet and completely wrong. Nothing in the pipeline would have caught it; it was caught by reading one trajectory by hand and noticing the agent had visibly fixed the bug it was being marked down for.

>

Probe the tool, do not read about it. Three planned approaches died to measurements that took an afternoon, and the experiment's founding assumption — a ~9,800-token MCP schema tax — was wrong by an order of magnitude when actually measured at 945.

>

A single-run design cannot see its own noise floor, and the accident that let this one see it was worth more than the planned result. Re-running the control arm against itself flipped 3 of 24 tasks with nothing changed at all.

>

Means without medians will sell you a finding. A 68% wall-clock "improvement" was four long rollouts; re-running three of them identically produced 82s, 97s and 68s instead of 1128s, 551s and 1354s.

>

Pre-registration is what makes publishing an unflattering number cheap. By the time the null arrived, the wording for it was already fixed, so there was no moment where a more generous reading was available to reach for.

>

Shipping the code is not the same as shipping the tool. The measurement survived a clean clone on a stranger's machine; the npm package did not survive its own bin path.

next:

Repeats, so pass@k and pass^k can answer the reliability question this result raised — they are exactly what the locked budget could not buy. A second agent CLI through the Agent seam, which exists but is unexercised. A rolling task refresh as a contamination hedge. Before any of that, the two distribution defects: the published package's bin path does not resolve, and the task definitions hardcode absolute paths to two repositories on my laptop, so nobody else can generate new rollouts without editing them first.

typescript · node.js · vitest · zod · mcp · claude code

BUILD.TXT: CTXBENCH (7 KB)[the build record →]

== </what the audit found> ==

────────────────────────────────────────

Twelve findings (F1–F12 in the build log) were measured first-hand before a line of harness code existed, and three of them killed planned approaches outright. `--bare`, the flag the entire arm-purity design was built on, turned out to pin the agent to exactly [Bash, Edit, Read] with no way to widen it — which would have floored completion in both arms and compressed the very effect under measurement. Docker was absent from the build machine. And both planned task sources were unusable: SWE-smith is Ubuntu-and-Python only, and PR-mining was impossible because the candidate repositories have zero merge commits between them.

The biggest finding arrived later and overturned the premise the experiment was designed to control for. The whole project was planned around an MCP schema tax of roughly 9,800 tokens — a byte count over the server's own tools/list independently predicts ~9,400 — and the measured figure, on an identical no-op prompt across three replicates identical to the token, is 945 (+3.1%). Claude Code registers all 30 tools and never puts their schemas in context; ~31 tokens per tool is a name and a one-line description. It is written up as an overturned premise rather than quietly corrected.

== </what was repaired> ==

────────────────────────────────────────

Three bugs would each have published a false result. The first was a macOS path bug — vitest reports `/private/var`, the harness stripped `/var` — so no test file ever matched its grading entry and every rollout in both arms graded unresolved. That is the most dangerous kind of wrong answer: a plausible, publishable, entirely false null. Nothing in the pipeline flagged it. It was caught because one trajectory was read by hand and the agent had visibly fixed the bug it was being marked down for. The second: defects were applied as uncommitted changes, so the agent's first orienting command — `git diff` — printed the exact inverse of the fix. Ten of twelve pilot rollouts solved, four of them in under 30 seconds. Defects are now folded into the pinned commit. The third: the reporter selected run directories lexicographically and printed superseded numbers under a correct-looking heading. It now selects by newest summary and always prints its source — and the same failure class returned during the documentation pass, when the documented report command, with no run pinned, resolved to the wrong batch and silently swapped the README's headline result. The fix was to pin the run in the script rather than let the reporter guess.

Four more were caught by gates rather than by luck. The oracle gate rejected a snapshot test embedding a relative timestamp, which could have manufactured a between-arm difference out of wall-clock drift alone. A shared-context fixture spelled out, verbatim, the fix for one task; leakage is now a mechanical symbol diff of every fixture against its own defect patch. The clean-clone gate found live room bearer tokens committed under a dot-directory that the analysis code skipped — deleted, and purged from git history. And a multi-turn ordering bug ran turn 2 before turn 1, so the treatment arm carried context forward from a task it had not done yet.

== </what was measured> ==

────────────────────────────────────────

The MCP schema tax is 945 tokens (+3.1%), not the ~9,800 the project was planned around. `--setting-sources ""`, the flag that guarantees the control arm is clean, also suppresses CLAUDE.md and AGENTS.md discovery entirely — verified three ways, and the consequence is that the third arm has to differ in two flags rather than one, which is stated rather than buried. The useful side effect is that the control arm's cleanliness became verified rather than assumed: the target repo's own checked-in CLAUDE.md was not ambient context for either arm.

The most valuable measurement was an accident. The third arm's batch re-ran the control arm with an identical flag set on the identical task set — an unplanned partial replicate, and precisely the thing a single-repeat design is supposed to be unable to see. It disagreed with itself on 3 of 24 tasks against a between-arm difference of 1 task net, and it dissolved the one efficiency story that looked like a finding: three tasks that took 1128s, 551s and 1354s in the first run took 82s, 97s and 68s in the second. Separately, a third arm delivering the same content as a plain CLAUDE.md moved nothing at all — both arms solved every task, in every category.

== </decisions> ==

────────────────────────────────────────

DECISION 01

Drop `--bare`; buy arm purity with an explicit flag set plus a runtime assert on every rollout

because `--bare` caps the agent at three tools and demands an API key, which would have floored completion in both arms and compressed the effect being measured. The replacement flag set was verified to produce zero MCP servers and zero skills, a unit test asserts the two arms' argv arrays differ only in the `--mcp-config` path, and every rollout re-checks the agent's own startup event. A rollout that fails the check is discarded and logged, never silently retried into the data.

DECISION 02

Grading is test execution only — no LLM-as-judge

because A benchmark of an AI product graded by an AI is a claim nobody has to believe. Resolved means every fail_to_pass test flips and every pass_to_pass test holds, in the target repository's own suite. It costs real sensitivity — a partially correct fix scores zero, which is part of why completion sat near ceiling — and buys a result a skeptic can re-run.

DECISION 03

The gold patch is the defect patch reversed, not a separate file

because Two files drift. A gold patch that no longer inverted its defect would silently break the one mechanism guaranteeing every task is solvable, and nothing would fail loudly — the benchmark would just quietly start measuring impossible tasks. The oracle gate proves solvability by actually reversing the patch and running the suite.

DECISION 04

A Sandbox seam instead of a hard Docker dependency

because Docker was absent on the build machine, and a hard container requirement would have made v1 unrunnable rather than imperfect. LocalSandbox pins by commit, lockfile and recorded toolchain; DockerSandbox ships implemented and auto-selects when a daemon answers. The trade is that the host toolchain becomes a variable — disclosed rather than hidden, and mitigated by always running both arms of a task on the same machine in the same session, which makes it a shared confound rather than a differential one.

DECISION 05

Pre-register the analysis, and generate the results and limitations sections from artifacts rather than typing them

because The author of the benchmark also builds the product under test. Fixing the metrics, the statistical method, the minimum detectable effect and the exact interpretation wording before any result existed — and putting the honest section between markers that the next report run overwrites — removes the places where a favourable result could be manufactured after the fact. It cannot be edited into optimism, because the next run reverts it.

DECISION 06

Report medians beside means, and label a tail-driven effect as one

because The first efficiency pass showed −68% wall-clock and −31% output tokens with intervals excluding zero, and both were carried by a handful of very long control rollouts while the medians were flat or pointed the other way. Publishing the mean alone would have been arithmetically true and substantively false. The analysis now emits both and flags the shape, with unit tests pinned against the real distributions.

LIMITS.TXT: CTXBENCH (2 KB)[where it doesn't work →]

== </where it doesn’t work> ==

────────────────────────────────────────

The benchmark could not have detected the effect it went looking for. Completion sat near ceiling — only 3 of 22 tasks discriminated between the arms at all, against a pre-registered floor of six — and run-to-run noise within a single arm exceeded the difference between the two, so the honest reading is that a near-ceiling benchmark reporting "no effect" is largely reporting its own insensitivity. The two efficiency numbers whose confidence intervals excluded zero turned out to be tail artefacts that a partial replicate dissolved. It is a single-repeat, single-model, single-agent study of a product whose value proposition is multi-agent coordination, so it measures at most half of what that product claims, and the same person wrote the harness, the tasks, the fixtures and the product under test. Two rollouts errored and are disclosed rather than buried: a 20-minute harness timeout that is one of the three self-disagreement flips, and an api_error that is the sole discordant pair favouring control and was kept rather than excluded — a judgement call applied inconsistently against the pre-registered rule. Distribution is worse than the measurement: `npm i -g ctxbench` does not give you a working command, because the published tarball's bin points at dist/cli.js while the compiled entry sits at dist/src/cli.js, and the task set is not in the package's file list at all, so the clone is the only install that works. And re-running the experiment is not turnkey for anyone else — all 24 task definitions hardcode absolute paths to two repositories on my laptop, which are handed straight to git clone, so a stranger can regenerate every published number from the committed artifacts but cannot generate new ones without editing the task files first.

PUBLISHED · npmjs.com