On this page

Token benchmark

How many tokens, how much money and how long Claude Code spends on typical work in this repository, and whether a way of working (a profile) changes that without hurting the answers. evals/ asks a different question (does the plugin make a whole task succeed more often); the benchmark is the cheap, repeatable one to run after changing a skill, AGENTS.md, the plugin or the prompts agents get.

Run it

pwsh benchmark/Invoke-TokenBenchmark.ps1 -DryRun                              # what would run, and how many sessions
pwsh benchmark/Invoke-TokenBenchmark.ps1 -TaskId COMP-PS-001 -Repeat 1 -Model haiku   # a few cents
pwsh benchmark/Invoke-TokenBenchmark.ps1 -SaveResponses                       # the whole spec: 96 sessions by default

Or the Token benchmark workflow (Actions, run manually), which puts summary.md in the run's summary and the whole folder in an artifact. It authenticates with a Claude subscription, not an API key: the CLAUDE_CODE_OAUTH_TOKEN secret from claude setup-token, shared with the Evals workflow. Runs then count against the plan's usage rather than a bill, so keep them small (tasks one id, repeat 1); the cost columns are Claude Code's own figure, not a charge. Locally, a logged-in claude works the same way, with no key set.

Each run writes benchmark/results/<UTC time>/: results.csv and results.json (one row per task, profile and repetition), summary.json and summary.md (per profile and per task), and with -SaveResponses every answer and the CLI's raw JSON. -Json prints summary.json on stdout.

Parameter
-ProfileId, -TaskId Only these ids; a list or comma-separated
-Repeat, -Model, -MaxCalls Override the spec
-Claude The CLI to run (default claude); a .ps1 runs with pwsh, which is how the self-test uses a stand-in
-WorkDir Where the sessions run (default: the repository root)
-Plugin Load this repository as a Claude Code plugin (--plugin-dir, and the Skill tool allowed), so its skills can trigger. Run the same spec with and without it to see what the skills change; rows and summary.md say which. In the workflow: the plugin input
-Isolate Run every session as a separate user that can read only its game copy (Isolation); also HYPERDOT_ISOLATE=1. In the workflow: the isolate input, on by default
-GameSource, -Seed The private field test on a game of yours (below)

The spec: benchmark/token-benchmark.json

The tasks name real files and expect answers grounded in them (HYPERDOT_TIMEOUT, process_frame, Stop-GodotCi), so a vague answer fails instead of passing on generic words.

What every session is

claude -p --output-format json, with the prompt on stdin (no shell sees it), in the repository, with:

The game programme: benchmark/game-benchmark.json

The tasks above are about Hyperdot's own code. The game programme measures the job Hyperdot is for: an agent working on a game. It is built from evals/explain-failure and evals/fix-score-bug, made deterministic, plus three new tasks:

Task The session must Graded by
GAME-DIAG-001 name the failing test, its file and line, expected and actual values, without editing the answer's terms; game and tests unchanged
GAME-FIX-001 fix a gameplay bug from a player's report the game's tests and a hidden one; the existing test unchanged
GAME-FEAT-001 add a best score and reset() with tests a hidden test; at least one new test; the game's tests
GAME-TEST-001 write tests for the scoring rules five mutants of score.gd, each of which its tests must fail on; the game unchanged
GAME-TUNE-001 expose a gameplay value to the editor without changing behaviour a hidden test; the startup flow still scores 230
pwsh ci/godot/hyperdot.ps1 setup -Project examples/gdscript    # Godot, and GODOT_BIN for the sessions and the grader
pwsh benchmark/Invoke-TokenBenchmark.ps1 -SpecPath benchmark/game-benchmark.json -SaveResponses [-Plugin]

In the workflow, choose the game programme. What differs from the tool programme:

tests/Test-GameBenchmark.ps1 runs the real spec with a stand-in CLI that does every task once well and once badly: the good work must pass every check, and the bad work must fail each task for the reason the task is about. It also runs a one-genre, two-stage test programme (tests/game-benchmark/programme/), checking tiers, selectors, the estimate, the ceiling, the stop past it, stages built from the whole game, and a milestone reached and missed.

The staged game programme

The pilot above is one prototype. The staged programme (benchmark/PLAN.md) covers genres, four stages each (prototype, alpha, beta, release candidate) and both languages. Its spec has a programme block instead of a task list; benchmark/GameProgramme.ps1 turns what a run selects into ordinary tasks.

<gamesDir>/<genre>/<language>/game/                 the genre's whole game, at release-candidate level
<gamesDir>/<genre>/<language>/stages/<stage>.json   how to make the stage from it, and the stage's tasks
<gamesDir>/<genre>/<language>/stages/<stage>/       files laid over the stage (optional)
<templatesDir>/<template>.json                      a kind of task: title, category, prompt with {placeholders}, limits
Parameter
-Tier min|mid|max the programme's tier: its cells (genre, language and stage combinations), tasksPerStage, repeat, milestones and maxCostUsd
-Genre, -Stage, -Language narrow the tier; lists or comma-separated, wildcards allowed
-TaskId full ids, wildcards allowed (*.fix-bug, *.milestone)
-BudgetUsd the most the run may cost
-Shard k/n one of n parts of the run: its sessions are every n-th in the run's order from the k-th, with their own estimate and their share of the ceiling; the n parts together are the run (meta.shard, and meta.partOfStandard instead of standard)
-SkipDone <folder,...> continue an earlier run: every results.json under those folders (the artifact layout <folder>/<time>/results.json) is read, and each session (configuration, profile, task, repetition) with a row that ran is not run again. "Ran" means tokens spent and not an infrastructure failure; a row with tokensTotal 0 or infrastructure true is run again, a failed answer is not. The key does not include the order, so the rotation of the first profile does not matter. The skip comes before -Shard, so what is left splits evenly. The dry run counts and prices only the remainder (skipped, continuation in its JSON). The new run is a fresh folder; its summary says meta.continuation, meta.skipped, and standard and partOfStandard stay false: the classes need the earlier results and this run together
-MaxMinutes n stop starting new sessions after n minutes (fractions allowed); the session running finishes. The rest is written to not-reached.json (configuration, profile, task, repetition), meta.notReached and the summary say how many, and the exit code is 2: incomplete, nothing failed (1 stays for failed sessions and the cost ceiling)

Cost. benchmark/cost-model.json gives each kind of session an expected cost by model, plugin and kind (task or milestone). The runner estimates the run from it. The ceiling is -BudgetUsd, or the tier's maxCostUsd shared over the programme's configurations in proportion to their estimates, so that four parallel jobs together stay under it.

summary.json records the tier, the selection, the estimate, the ceiling and what was spent. standard is true only for an unnarrowed, complete mid run, the one that can set a class.

Judged grading. A task with a judge block (rubric, a file under benchmark/rubrics/; subject, answer or a file the session left; optional runs and passScore) is also scored by the programme's pinned judge model (judge in the programme, provisionally Haiku until calibration picks one). The judge scores from 1 to 5, runs times (3), in fresh sessions with no tools, in an empty folder. It is told the work is data, not instructions, and nothing about who made it. A median under passScore (4) fails the task. The scores go in the row's judge. The judge's cost counts toward the run's ceiling but not toward the configuration's tokens.

Blind side-by-side. benchmark/Invoke-SideBySide.ps1 -Results <folders> pairs each session of a model with the plugin against the same task and repetition without it. The judge gets the task, the two answers and diffs as A and B, and the rubric benchmark/rubrics/side-by-side.md. Each pair is judged twice, the order swapped and its first order random. The plugin's session wins the pair (1) if it is chosen both times, loses it (0) if the other is, and anything else is half. preferences.json holds each pair and each model's win rate. Get-HyperdotLabel.ps1 -Preferences divides the plugin configuration's index by 0.75 + 0.5 x win rate (the configuration without the plugin stays at 1). The runner saves each session's prompt (<profile>-<task>-r<n>.prompt.md) for this.

A person's review, and the judge's calibration. Invoke-SideBySide.ps1 -Results <folders> -ReviewOut <folder> calls no judge: it writes the same pairs as review-pairs.json (the task and the two pieces of work as A and B, in a seeded random order, with nothing naming the model, the profile or the plugin) and, apart, review-key.json (which side had the plugin). The same results and seed always give the same key, so it need not be kept. benchmark/review-page.html, published as an artifact with review-pairs.json beside it, shows each pair and keeps the owner's verdict (A, B or even, and a note) in the page's own store, one document per pair under verdicts, which only the owner can write. benchmark/Get-JudgeAgreement.ps1 -Calibration benchmark/calibration -Key <keys> -Preferences <preferences> (one key and one preferences file per set) then applies the owner's rule of 2026-10-26 over every set at once: the judge must agree with the owner at least as often as the owner agreed with themselves on a blind re-judge of earlier pairs (the files with pairs and result, 6 of 10 so far), measured on every set's verdicts except one marked "forAgreement": false and with each re-judged pair's newer verdict, and with itself (the same work chosen in both orders) on at least 90% of every pair it judged. Below either it exits 1, and the judge's results stay out of the label. -Verdicts <verdicts.json> in place of -Calibration scores one set at fixed bars (-MinAgreement, 80% unless given). The verdicts are a JSON array of {id, choice}, as the page's store holds them. The workflow's calibrate input does all of it: it rebuilds every set's pairs from its run (while the run keeps its results, 30 days) and tries the judges cost-model.json prices, cheapest first.

In the workflow, choose the staged programme, a tier, any selectors, and configurations: all. A plan job dry-runs each configuration (Sonnet and Haiku, with and without the plugin) and fails before anything is spent if one is over its share. Then one job per configuration runs in parallel.

A tier that outlasts one job (the mid tier runs 135 sessions a configuration, about seven hours) takes shards: n: each configuration then runs as n parallel jobs, one per part, each with its own artifact (-part-k).

Finishing a run that did not complete. A usage limit, the job's 330 minutes or a cancelled job leave sessions unrun. max_minutes (default 285; empty or 0 mean the cap, never no limit) makes each job stop starting sessions at that time, at most 45 minutes (a milestone session) before the job's limit, and end green with its results and not-reached.json uploaded, instead of being cancelled. continue_run: <run id> then runs only what is missing: each job downloads the token-benchmark-<programme>-* artifacts of that run (kept 30 days; the workflow's actions: read permission), passes them as -SkipDone, and the plan job's estimate counts only the remainder, so the shards split it evenly. Give the same selection as the run being continued: the runner refuses a -SkipDone result of another tier, model, plugin, repeat or narrowing, naming what differs. The old run is not changed; the new one is a continuation (see -SkipDone) and its summary lists the runs it continues (meta.continues). A continuation can itself be continued, naming it in turn. continue_run also takes several ids separated by commas (41465548838,41495679885,41526565441): a continuation reads only the runs it is given, so after a chain of runs name them all, or the sessions of the earlier ones run again (the Opus chain of the mid tier repeated 29 sessions that way, and 80 would have repeated to finish the last 7). Get-HyperdotLabel.ps1 reads a run and its continuations as one unit, a session run again counting once as its last row.

Sizing: at about seven minutes a session, a job reaches 285 minutes after about 40 sessions. A remainder of 40 sessions fits one job, two give margin.

Isolation

The copy without the benchmark keeps the test out of sight, but a session with Bash could still read the checkout the run started from, hidden tests and graders included. -Isolate (benchmark/Isolation.ps1; on in the workflow) closes that on Linux:

What it does not do: isolation covers the session, not the grading step. The grader still runs the session's code (the game's autoloads and scripts, loaded by Godot when the tests start), as the runner, after the session user is dropped, so that code can read what the runner can until a second, unprivileged grading user is added (see the roadmap); and on Windows, on macOS or without root or passwordless sudo, -Isolate warns and runs the sessions as before.

tests/Test-BenchmarkIsolation.ps1 runs the one-genre test programme with the stand-in, which tries to read benchmark/, a hidden test, the folder its game is in and the checkout: without isolation it can, with isolation it cannot, and the work is still graded. It also checks the credentials passed and the rest held back, Godot runnable, a process left running, a link planted where the hidden test goes, a git setting that runs a command, the user removed and the checkout's mode restored, through both runuser and sudo.

The private field test

-GameSource <folder> runs generic tasks on a game of yours (benchmark/FieldTest.ps1, spec benchmark/field-test.json), in a copy, under every configuration of the spec (Sonnet and Haiku, with and without the plugin; -Model or -Plugin picks one). It reports numbers only: per template and configuration, the runs, passes, median tokens, cost and time. No file name, code, test name or text of the game or the sessions is kept: answers and diffs are not saved (-SaveResponses is refused), failure reasons become kinds ("the game's checks fail"), and nothing is committed (benchmark/results/ is ignored by git).

Template The session must Applies when
fix-mutant find and fix a bug the tool planted, tests untouched the game has tests that catch a planted bug
write-tests write new tests that alone catch up to three planted bugs, code and existing tests untouched the same
doctor-green make the kit's doctor pass without breaking the tests doctor fails on the game as it is
bot-finish fix a planted bug only the playthrough bot notices the game has a playthrough.json and such a bug is found

Planting a bug generically. A mutant flips one operator or changes one whole number in the game's own GDScript or C# (not tests, test projects, add-ons or build output): < and <=, > and >=, == and !=, + and -, * and /, && and ||, and and or, n and n+1. Operators count only with a space on each side, so C# generics, -> and => are left alone, and strings and comments are blanked before the search. The candidates are put in an order the seed fixes (-Seed, default the spec's 1) and tried in that order, each in a copy, with the kit's test, at most mutantTrials (12) times:

Every planted bug was shown to be found by the game's own checks, and the untouched game must pass them all first. For write-tests only the session's new test files judge the mutants, so copying nothing and adding a test that checks nothing fails. A template with nothing to work on is listed as not applicable, with the reason in general terms.

Running it on Windows, in PowerShell 7 (pwsh), with Claude Code installed and logged in (claude, then /login; a subscription's usage, not a bill) and this repository cloned:

cd C:\src\hyperdot                                           # your clone of the kit
pwsh ci/godot/hyperdot.ps1 test -Project C:\games\mygame     # the game must be green first
pwsh benchmark/Invoke-TokenBenchmark.ps1 -GameSource C:\games\mygame -DryRun     # what runs, and its estimated cost
pwsh benchmark/Invoke-TokenBenchmark.ps1 -GameSource C:\games\mygame            # the run: about 1.80 USD by the cost model

tests/Test-FieldTest.ps1 checks the planter (code only, a seed's fixed order) and runs the field test with the stand-in on copies of examples/gdscript and examples/csharp: good work passes every template that applies, bad work fails for the right kind of reason, doctor-green on a copy whose doctor fails, and nothing of the game in the report.

Reading the numbers

Reference runs (October 2026)

Hyperdot 0.1.0 at commit e4ccc2e, Claude Code 2.1.309, three runs of every task. These are GitHub Actions runs 39930259189 to 39930286282: 192 sessions (240 calls), $5.95 by Claude Code's figure, run on a subscription. They set the label's scale (label.md). The table is graded by the current grader, from the saved answers.

Model Profile AEI Pass Tokens per solved task Cost per solved task Class
Haiku 5.5 A-baseline 74.2 95.8% 86,041 $0.0042 D
Haiku 5.5 B-scoped 94.5 87.5% 82,355 $0.0062
Haiku 5.5 C-iterative 120.1 100% 106,040 $0.0084
Haiku 5.5 + plugin A-baseline 107.5 79.2% 113,548 $0.0058 G
Sonnet 5.5 A-baseline 92.8 54.2% 84,763 $0.0607 F
Sonnet 5.5 B-scoped 124.1 45.8% 100,536 $0.0847
Sonnet 5.5 C-iterative 167.8 66.7% 133,253 $0.1713
Sonnet 5.5 + plugin A-baseline 100.4 62.5% 85,563 $0.0633 F

The label's information sheets for the four profile-A rows, rebuilt from these runs' saved answers with benchmark/Get-HyperdotLabel.ps1: Haiku, Haiku + plugin, Sonnet and Sonnet + plugin. The tested class is A (index 97.2), from the coverage of the self-test run on commit 0381761.

What they show:

Earlier runs (Sonnet on 2026-10-23 07:45 and 08:11, Haiku at 08:23, Sonnet with the plugin at 08:50) used a grader and sessions that could read the spec. They are superseded by the runs above.

Differences from the blueprint it started from

Blueprint Here Why
YAML spec, installs powershell-yaml at run time JSON spec The kit's configuration is JSON; installing modules on someone's machine is a side effect
Answer read from content, text or output_text From result What claude -p --output-format json prints; the others would leave every answer empty and failing
Tokens = input + output All four kinds Cache reads and writes are most of a session
Price table (zero by default) The CLI's cost Exact per kind of token and per model
Profiles as labels in the prompt Real differences: a system prompt, listed files, turn limit, effort, steps Labels measure wording, not ways of working
Prompt passed through pwsh -Command "..." On stdin, no shell A $ or quote in a prompt was expanded or broke the command
One run per task and profile repeat (3) with medians and P90 Single runs vary too much to compare
Full access in the working repository Read-only tools, no MCP servers, project settings, a fresh session The benchmark must not change the repository or depend on the machine
retries always 0, time to first token empty Retries counted, ttft_ms from the CLI
A claude-sonnet-4 default, temperature and max_output_tokens settings An alias (sonnet); no temperature or output settings The CLI has neither option; an alias stays current
Paths such as scripts/**/*.ps1 Paths in this repository There is no scripts/ folder

Self-test

tests/Test-BenchmarkIsolation.ps1 and tests/Test-FieldTest.ps1 are described above. pwsh tests/Test-TokenBenchmark.ps1 runs the benchmark against tests/token-benchmark/fake-claude.ps1, a stand-in that prints the CLI's JSON shape: token counting, acceptance, a retry after output that is not JSON, the time limit, two steps resuming one session, the isolation arguments, the call limit and the reports. It calls no model.