Skip to main content
benchspec

Benchmark what your agent does, not what it says.

CI PyPI Python License: MIT

benchspec runs an agent (Claude Code, Codex, or OpenCode) against a task in a fresh sandbox, checks what it actually did in the workspace, and reports the result as a comparison: with your skill versus without, one model versus another, one harness versus another.

Animated terminal output: benchspec run prints a benchmark matrix with evals as rows, arms as columns, color-coded rates, and percentage-point deltas

End-of-run summary for the in-repo hello suite: the baseline and trial columns of a real three-sample run. Rows are evals, columns are arms (baseline ran the agent bare, trial installed the skill), and every non-baseline cell shows its assertion pass rate plus the delta against the baseline in percentage points. The scoped Skill `hello` invoked line is in neither column. The same matrix lands in benchmark.md, with machine-readable artifacts alongside.

Teams pick harnesses, models, and prompts by anecdote: run it once, eyeball the transcript, trust the vibe. benchspec turns that guess into a measurement. Write the goal once, run it across the configurations you care about, and read off — in percentage points — how good each one actually is at accomplishing it.

When to use benchspec. If the only question is whether one plugin helps Claude Code, claude plugin eval ships inside Claude Code and answers it on one credential, no container. The comparison below routes the other cases.

What you need

Platform Any OS with a Docker daemon (default). Apple Silicon or Linux with /dev/kvm for the microsandbox opt-in. Python 3.11+.
Agent CLI claude, codex, or opencode on PATH, with its credential (for Claude Code, CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY).
Binder credential The binder is a fixed model call that classifies each assertion, required for every analyze and run: GEMINI_API_KEY by default, or OPENROUTER_API_KEY with [tool.benchspec.binder] provider = "openrouter".
Judge credential The judge runs on the host through an agent CLI, using either its env credential or the CLI's own login (claude login, codex login); the default is claude-code with sonnet. Prefer a different vendor from the arms (this repo's own suite judges Claude arms with Codex).

Credentials can live in a repo-root .env. A graded run can touch up to three vendors: the agent's, the binder's, and the judge's. Or exactly one: set provider = "openrouter" on the binder, the judge, and the arms, and the whole run needs only OPENROUTER_API_KEY (see configuration.md). lint is free; analyze and run spend API calls, and run also boots sandboxes. Preflight lists every missing piece and exits before anything is spent.

Getting Started

pip install benchspec

For the microsandbox isolation opt-in, add its extra:

pip install "benchspec[microsandbox]"

Pre-1.0. The eval format and the artifact schemas are the surfaces most likely to change.

An eval is one Markdown file: a prompt, then a checklist of plain-prose claims about the workspace after the agent is done. There is no checker syntax to learn; the wording is the spec. evals/hello/greets-by-name.eval.md:

---
---

## Prompt

You are working in a workspace rooted at your current working directory.
Greet Alice by name.

## Assertions

- [ ] ./Greetings/Alice.md contains the exact line 'Hello, Alice!'
- [ ] Skill `hello` invoked
  - if: {BENCHSPEC_ARM} != {BENCHSPEC_BASELINE}
- [ ] The greeting feels warm and personable, not curt or robotic

The benchmark is a block in pyproject.toml. Arms are the report columns; the baseline is what the others are measured against:

[tool.benchspec]
default-set = "default"

[tool.benchspec.sets.default]
harness  = "claude-code"
model    = "sonnet"
baseline = "baseline"
arms = [
  { name = "baseline" },   # installs nothing
  { name = "trial" },      # setup.sh installs the skill
]

Then:

benchspec lint      # static checks on the assertions
benchspec analyze   # which assertions grade deterministically, which go to the judge
benchspec run       # every (eval × arm) in its own sandbox, graded, reported

The Skill `hello` invoked line needs a skill for the trial arm to install and a setup.sh that installs it; the quickstart writes both and takes an empty directory to that first graded report. The - if: sub-bullet keeps that line out of the baseline's rate, so the delta measures the skill (scoping).

How a run works

flowchart LR
    E["greets-by-name.eval.md<br/>prompt + assertions"] --> A1["arm: baseline<br/>fresh sandbox,<br/>setup.sh installs nothing"]
    E --> A2["arm: trial<br/>fresh sandbox,<br/>setup.sh installs the hello skill"]
    A1 --> F1["facts: files, SHAs,<br/>final message, tool calls"]
    A2 --> F2["facts"]
    F1 --> G["binder: deterministic checkers<br/>everything else: LLM judge"]
    F2 --> G
    G --> R["benchmark.md + benchmark.json<br/>meta.json + index.jsonl"]

Each (eval × arm) pair is one parametrized pytest test. A cell:

  1. Boots a sandbox — a Docker container by default, or a microsandbox microVM for the opt-in — from a cached snapshot with the agent CLI already installed. The first run builds the snapshot (about a minute); later runs reuse it. benchspec sandbox:build pays that cost up front, benchspec sandbox:clean reclaims the disk.
  2. Seeds the clean room — the eval's optional workspace/ files land in a fresh directory mounted at /workspace, the agent's working directory.
  3. Runs setup.sh, where arms diverge: it sees $BENCHSPEC_ARM, so the baseline branch exits early and the trial branch copies the skill into place.
  4. Invokes the agent on the eval's prompt.
  5. Collects the facts — file tree, contents, SHA-256s, the final message, the tool calls.
  6. Grades — the binder maps each assertion to a deterministic checker where it can do so without risk; the judge grades everything else from the collected evidence alone.

On either backend, nothing in the guest can write back to your checkout: setup.sh reaches the skill under test through a read-only staged copy of your repo at /project (what a git clone would contain — never .env, .git, or earlier runs' artifacts). Credential exposure differs — microsandbox injects each credential at the network boundary, Docker as a plain container environment variable the agent can read. sandbox.md has the tradeoff.

benchspec run is pytest underneath, and everything after -- goes to pytest verbatim. The normal run is benchspec run -- --count 3: three samples per cell (pytest-repeat), so every delta carries a noise band; a one-sample run is flagged in the report. -n 8 fans cells across eight sandboxes to keep it fast, and -k greets-by-name (equivalently pytest -k greets-by-name) runs one eval. The repo's own make e2e defaults to three samples and six workers through the COUNT and WORKERS variables; make e2e COUNT=1 WORKERS=1 is the quick sequential pass.

benchspec vs. the alternatives

Where each alternative is the better answer:

Reach for it when What benchspec adds
claude plugin eval The question is whether one plugin helps Claude Code. It ships inside Claude Code, needs one credential and no container, and interviews you to write the suite. A second harness, arms beyond with-and-without, a workspace seeded and hashed before the run, and a judge that need not share the arms' provider.
Inspect AI You write Python, and you want off-the-shelf evals, remote execution at scale, or pass@k reducers. The eval is a Markdown file of prose claims rather than a scorer you implement, and the report is an arms-versus-baseline delta without assembling one.
Harbor / Terminal-Bench You want to rank agents on a standard published benchmark, with a long list of agents already integrated. Your tasks, your baseline, your delta.
Coder Eval You want typed YAML criteria with weights and fractional credit, or its GitHub Action. Prose assertions instead of a criterion schema, and pre-run hashes behind left unchanged.
promptfoo What you are grading is a prompt and the reply it produced. Grading of the workspace the agent left behind, not the response it wrote about it.

benchspec's own cost is the top of this README: a Docker daemon, and up to three vendors' credentials. Every run pays that back in artifacts — meta.json, index.jsonl, benchmark.json — that another tool can aggregate without knowing the directory layout.

docs/research/2026-09-13-alternatives-landscape.md has the long version: unit of evaluation, isolation, and statistics per tool.

Documentation

docs/quickstart.md Empty directory to a graded two-arm run.
docs/concepts.md The vocabulary: eval, arm, set, baseline, binder, judge.
docs/writing-evals.md The eval format, workspaces, setup.sh, and how grading decides what binds.
docs/configuration.md Sets, arms, the judge, every CLI flag, exit codes.
docs/sandbox.md Snapshots, mounts, credentials, host requirements.
docs/results.md Reading benchmark.md and the machine-readable artifacts.
docs/harnesses.md claude-code, codex, opencode, and adding your own.

Development

Clone to a graded run of the in-repo suite in four steps. Steps 1 and 2 cost nothing; step 3 is the first paid call.

git clone https://github.com/theycallmeswift/benchspec && cd benchspec
  1. Install. uv manages the venv; make install runs uv sync. Then the free checks:

    curl -LsSf https://astral.sh/uv/install.sh | sh   # if you don't have uv
    make install
    make test     # unit suite: no credentials, no sandbox
    
  2. Confirm the suite collects. Needs no credentials and no Docker:

    make e2e EVAL_ARGS="--collect-only -q"   # 36 cells: 4 evals × 3 arms × 3 samples, then 48 through OpenRouter
    
  3. Set up the credentials. make e2e runs evals/e2e/hello/ twice: four evals across three Claude Code arms, judged by Codex; then the same evals across Claude Code, Codex, and OpenCode arms with the binder, the judge, and every arm on OpenRouter. It needs claude and codex on PATH, a running Docker daemon, and four credentials in .env:

    cp .env.example .env
    
    Variable For
    CLAUDE_CODE_OAUTH_TOKEN (from claude setup-token) or ANTHROPIC_API_KEY the arms
    GEMINI_API_KEY the binder
    OPENAI_API_KEY the Codex judge
    OPENROUTER_API_KEY the second run: binder, judge, and arms through OpenRouter

    Preflight lists every missing piece in one message and exits before anything is spent.

  4. Run it. The cheapest real run is one eval, one sandbox:

    make e2e COUNT=1 WORKERS=1 EVAL_ARGS="-k greets-by-name"   # one eval, one sample, sequential
    make e2e                                                  # the whole suite, three samples, six sandboxes
    

    The first run builds the sandbox snapshot (about a minute); with the snapshot cached, the whole suite takes about a minute on six workers. The report lands in tmp/evals/iteration_01/benchmark.md.

Then make lint (ruff, ty, houserules; needs GEMINI_API_KEY) before a pull request. On Claude Code on the web, the environment's setup script does steps 1 and 3 once for every session; see sandbox.md.

Issues and pull requests are welcome at github.com/theycallmeswift/benchspec. Until CONTRIBUTING.md lands, docs/style/development.md is the code style, and make test plus make lint are the bar.

License

MIT.

Release files for benchspec 0.0.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for benchspec 0.0.5
File Size Uploaded
benchspec-0.0.5.tar.gz 393.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for benchspec 0.0.5
File Interpreter ABI Platform
benchspec-0.0.5-py3-none-any.whl Python 3 none any Details

Total release size: 583.7 kB

Release files / benchspec-0.0.5.tar.gz

Download URL benchspec-0.0.5.tar.gz
Size 393.7 kB
Tags Source
SHA-256 checksum
How to use checksums
77e29b47a6ecad4e512f75cddd4693818857c6fb2a5f8cebd7a9f9e6d659648f
BLAKE2b-256 checksum
How to use checksums
d9f09c88fc7f4e2a55e44484af74a2b3238b5ee59b5b117499987f37a79fffc9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / benchspec-0.0.5-py3-none-any.whl

Download URL benchspec-0.0.5-py3-none-any.whl
Size 190.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e62229d132f715c4122c15c8dd4c0c3023e2a05845d1302a9d6ea84e335a3395
BLAKE2b-256 checksum
How to use checksums
82ed073e3fc233e006e21c1bd65d33ea5ff667d11eebfa6ed1de5df4f4eb779e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.0.5 This release

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page