Regression testing for
Claude Code agents.
Change a prompt, a skill, or an MCP tool β then let statistics,
not vibes, tell you whether your agent got worse.
Β Β Β Β
You spent weeks tuning your agent β prompts, skills, MCP tools, CLAUDE.md. Now every
change to that setup is a gamble: agents are nondeterministic, so one good run after a
prompt tweak proves nothing, and one bad run proves nothing either. Catacomb settles
the question with statistics. catacomb bench runs the same tasks repeatedly under
the old setup and the new one, recording each run locally as secret-redacted evidence;
catacomb regress compares the two groups and maps the verdict to an exit code, so CI
blocks the regression before it merges. No daemon, no service, no network β the whole
loop is plain local files.
β¨ Features
A gate, not a vibe check. Repeated runs per variant, compared with statistics built for small samples (why this matters); the verdict maps to the exit code your CI already understands.
Plain local files. Evidence directories plus an optional SQLite store for baselines β nothing listens, nothing phones home.
Evidence you can share. Recorded transcripts pass through best-effort secret redaction before they ever touch disk (what is and isn't caught).
Comparisons survive prompt rewrites. The agent can name phases of its own run (checkpoints), giving regress a stable axis even when prompt churn re-keys every step (concepts).
Longitudinal memory. Pin golden groups as named baselines; every recorded comparison accumulates into a history that trends replays (workflows).
Checks the answer, not just the path. Declare a per-task verifier and its pass/fail verdict rides the same statistical gate (verifying task outcomes).
π§° Requirements
Claude Code installed and claude on your
PATH. Catacomb itself is a single static binary β no runtime, no dependencies, no
config file.
go install github.com/realkarych/catacomb/cmd/catacomb@latest
# make sure $GOBIN (default ~/go/bin) is on your PATH
Or clone the repo and make build.
Windows / other distros
Download the pre-built archive from the
Releases page, unpack it, and
add the binary to your PATH. On Windows, you may need Unblock-File .\catacomb.exe
before first run.
Upgrading from Homebrew: brew upgrade --cask catacomb. Migrating from the old
formula: brew uninstall catacomb && brew install --cask catacomb.
π Tutorial
Ten minutes, two small files, one caught regression. The scenario: someone on your
team wants to add a chain-of-thought instruction to a shared prompt, and you want CI
to tell you what that does to the agent's behavior β before it merges.
1. Create it
Make an empty folder with two files. agent.sh is the agent command under test β
any command works as long as it emits stream-json, which is how catacomb finds the
session transcript:
#!/usr/bin/env bash
# agent.sh β the agent under test. bench runs this once per cell.
set -euo pipefail
exec claude -p "${PROMPT}" \
--model claude-haiku-4-5 \
--output-format stream-json \
--verbose \
--strict-mcp-config \
--setting-sources project
(The last two flags keep your user-scope plugins and hooks out of the benchmark, so
runs are comparable across machines.)
demo.yaml declares the experiment β a basket: the matrix of tasks Γ variants Γ
reps. Each combination is one cell, run as a plain local process:
# demo.yaml β the same question, with and without a chain-of-thought
# instruction someone wants to add to the shared prompt.
basket: demo
reps: 5
tasks:
- id: answer
cmd: ["./agent.sh"]
dir: .
variants:
- id: main
env:
PROMPT: "In one short sentence, what is a hash function?"
- id: candidate
env:
PROMPT: "In one short sentence, what is a hash function? Think step by step and write out your full reasoning before you answer."
chmod +x agent.sh
2. Run it
catacomb bench demo.yaml --runs-dir runs
Ten cells run sequentially β 2 variants Γ 5 reps, about a minute and a few cents on
haiku. Each cell streams the agent's raw stream-json output to your terminal; the
block below shows only the tail:
(If your Claude Code is newer than the versions catacomb has been tested against, a
few harmless warning: transcript β¦ newer than tested β¦ lines may precede the
table.)
baseline runs 5 candidate runs 5
coverage steps 1.00 phases 1.00 steps_trusted true overall regression
sensitivity: gate cannot fire at this support (paired gate needs k>=5 tasks)
VERDICT SCOPE KEY NAME METRIC BASELINE CANDIDATE BAND DETAIL
regression total - - cost_usd 0.00 0.01 [0.00, 0.00] -
regression total - - duration_ms 5059.00 7165.00 [3755.50, 6362.50] -
ok total - - error_rate 0.00 0.00 [0.00, 0.35] -
ok total - - nodes 4.00 4.00 [3.00, 5.00] -
ok total - - tokens_in 10.00 10.00 [7.50, 12.50] -
regression total - - tokens_out 147.00 465.00 [91.50, 202.50] -
insufficient paired - - cost_usd 0.00 0.00 - matched 1 task below paired min 5
insufficient paired - - duration_ms 0.00 0.00 - matched 1 task below paired min 5
insufficient paired - - tokens_in 0.00 0.00 - matched 1 task below paired min 5
insufficient paired - - tokens_out 0.00 0.00 - matched 1 task below paired min 5
regression phase eb1d7eb24fc38d7838cf7b81664c90e6 task:answer cost_usd 0.00 0.01 [0.00, 0.00] -
regression phase eb1d7eb24fc38d7838cf7b81664c90e6 task:answer duration_ms 5059.00 7165.00 [3755.50, 6362.50] -
regression phase eb1d7eb24fc38d7838cf7b81664c90e6 task:answer tokens_out 147.00 465.00 [91.50, 202.50] -
audit: baseline run bench-demo-answer-main-r1 (task answer) cost_usd 0.010954549999999999 vs group median 0.0033167 (band 0.00165835)
The chain-of-thought instruction made every run roughly 3Γ chattier (median
tokens_out 147 β 465) and about 40% slower (median duration 5.1 s β 7.2 s), and
even at haiku pennies the cost crossed its noise band β the overall verdict is
regression and the exit code is 1. The sensitivity: line refers only to the
paired per-task axis, which needs five tasks where this demo has one β the run-total
metrics are what fired the gate. That exit code is the gate:
Two runs of an agent are two samples from a distribution β a single side-by-side diff
cannot tell a real regression from sampling noise, which is why every comparison here
is group-vs-group (methodology).
4. Upgrade it
Pin a baseline and keep history. Label selectors churn; names don't. Pin the
golden group once, then --record every CI comparison and replay the drift later:
Let the agent mark its own phases. Prompt rewrites re-key steps, which degrades
step-level alignment. Checkpoints survive that: give the agent the shipped MCP marker
tool, tell it when to mark, and declare what you expect:
Pass that file via claude --mcp-config, add one line to your CLAUDE.md β e.g. "call
mcp__catacomb__mark with name: plan before planning and after tests pass" β and
declare checkpoints: [plan, tests.pass] on the task. Declared phases become stable
comparison rows in the regress table, robust to prompt churn
(concepts).
Verify the answer itself. Deterministic observables catch behavioral drift; whether
the agent produced the right answer is task-specific. Declare the files a task
produces and a command that scores them:
The verifier reads the captured artifacts and emits one pass/fail line β two calls
with the shipped Python SDK:
import os
from catacomb_verifier import Cell, emit, compare_tables
cell = Cell.from_env()
res = compare_tables(cell.artifact("out/result.csv"), os.environ["GOLDEN"], ordered=False)
emit(passed=res.equal, tool="verify_sql", tool_version="1")
bench runs it after every cell, and the resulting verifier.pass rate gates through
regressby default β same Wilson bounds as everything else
(verifying task outcomes).
Recap
You now have: a declarative basket, redacted local evidence for every run, a
statistical gate wired to CI exit codes, a named baseline accumulating history, and
two quality axes (checkpoints, verifier) that survive prompt rewrites. Recipes for
all of it β sensitivity at small k, the paired sign test, external scores, the
accuracy-vs-cost Pareto table β live in the
workflows guide.
π§ How it works
Catacomb reduces each session's transcripts (the main session plus every subagent
sub-transcript) into one deterministic execution graph:
Cross-run identity rides two keys: a step key hashes each call's redacted, salient
input, so "the same logical step" aligns across runs even though every node ID
differs; a phase key names checkpoint windows, so comparisons survive even when
prompt churn re-keys the steps. Groups of runs are then aggregated and compared per
ADR-0022: Wilson bounds
for rates, IQR noise bands for metrics, an exact sign test for paired per-task drift.
The full pipeline β bench runner, redacted capture, reducer, keys, gate, baselines,
history β is implemented end-to-end under a 100%-test-coverage TDD gate; catacomb
deliberately ships no live viewer
(ADR-0026). The deeper story
is in concepts.
π¬ Methodology
The gate's design follows the published eval literature, not house heuristics:
Repeated, isolated trials; tasks from real failures; outcome-over-path scoring β Anthropic, Demystifying evals for AI agents (2026).
Wilson bounds and the exact sign test are the right small-n tools β naive CLT-based intervals undercover below a few hundred datapoints: Bowyer et al., Don't use the CLT for LLM evals (ICML 2025).
pass^k reporting and deterministic final-state verification β Ο-bench (ICLR 2025), which catacomb's reliability block and verifier model follow.
The harness and its transcripts are a first-class reliability concern β transcript inspection catches shortcuts that pass outcome verifiers: Holistic Agent Leaderboard (ICLR 2026).
Verifiers must themselves be validated β annotators flagged 61.1% of sampled SWE-bench tasks for unit tests that may unfairly reject valid solutions (SWE-bench Verified), and vetting reduces but does not eliminate the bias (SWE-Bench+). Hence catacomb's verifier contract keeps comparators total, offline-re-runnable, and out of the agent's reach.
LLM judges do not replace deterministic checks β even expert-designed graders trail human inter-rater agreement (GDPval); judge protocol discipline per OpenAI's evaluation best practices. Catacomb gates on deterministic observables and lets external scorers ride the same mechanism instead of baking a judge in.
Catacomb runs no daemon and opens no sockets β everything is local files. Evidence
transcripts pass through secret redaction on the write path (API keys, tokens, private
keys, connection strings, high-entropy values β typed markers). It is best-effort, not
a guarantee β the classes it deliberately does not catch are listed under
known residuals. Graphs and
step keys hash only post-redaction content; the SQLite store holds baselines and
reports, never transcripts or payloads.
π Documentation & Development
Reading order:
This README β install, tutorial, the mental model