action/, tools/, .github/workflows/config-drift-checker.yml into the repo (or, once
published, uses: jameskomo/config-drift-checker@v0).ANTHROPIC_API_KEY (an API key from console.anthropic.com — not a consumer
subscription), optional SLACK_WEBHOOK_URL.<plugin>/evals/ — start with three: one positive skill case, one
negative-trigger case, one hook case with a case.yaml scaffold. If the repo ignores .claude/
(git check-ignore -v .claude/evals/x), keep the plugin files under agent-config/ instead and
set experimental.evals in the manifest. Real-code cases: scaffold copies the real source and
CLAUDE.md into the workspace ($EVAL_PLUGIN_ROOT is set during scaffold); ask for a feature
that does not exist yet.claude plugin validate <plugin> then
node tools/eval-shim.mjs <plugin> --case '<one>' --runs 1 --ablation none --scaffold.workflow_dispatch) once → records the baseline on eval-results.<plugin>/evals/results/<timestamp>/
aggregate-result.json # the data - what eval-diff, --regrade and the Action read
report.html # the same data as a self-contained page, generated from the JSON
The shim prints both paths at the end of every run, including --regrade runs. The report is
derived, never authoritative: regenerate it any time, with or without a baseline for the diff columns
(node tools/eval-report.mjs <results>/aggregate-result.json [--baseline <other>/aggregate-result.json]).
In CI the Action generates it with the baseline diff and uploads it as the eval-report workflow
artifact (90-day retention); the results branch stores only the JSON, so the HTML is always
reproducible. One file, no server, opens from disk (Google Fonts if online, system fonts if not).
The official claude plugin eval writes an HTML report too, so the habit carries over when the
account is enabled.
Nothing. The watch job polls npm every 6 h and runs the suite only when Claude Code shipped.
Push/PR to the plugin path runs it too. Read the job summary; regressions also arrive on Slack.
eval-report.html:
every run, every grader with its reason, tool calls, and the full response. Locally the same file
is written as report.html next to each aggregate-result.json.claude-code-version to the last good version in the workflow while you adapt the setup;
write it up (this is content).promote-baseline: true.runs for that case; check the
scaffold creates the state the prompt assumes; check graders match code, not prose.threshold to make red go away; add runs.workflow_dispatch with promote-baseline: true after an intentional change, or after the first
run. Baselines are per results branch; history keeps every run regardless.
| Lever | Effect |
|---|---|
ablation: none on scheduled runs |
halves cost; keep with-without for one-off proof |
runs per case |
3 default; 5 for flaky/critical hook cases; 1 for smoke on PRs |
model: haiku for PR smoke, sonnet for release runs |
5–10× cheaper smoke |
--case / --tag filters on PRs |
run only what the PR touched |
| watch interval 6 h (not every version) | coalesces bursts (9 versions in a week observed) |
Measured: $0.045–0.075 per short Sonnet run; $1.00 per 18-run suite.
node tools/eval-shim.mjs komo-stack --scaffold # full, with/without, 3 runs
node tools/eval-shim.mjs komo-stack --case 'guard*' --runs 1 --ablation none --scaffold
node tools/eval-diff.mjs baseline.json current.json --threshold 0.15 --md diff.md
node tools/eval-shim.mjs <plugin> --regrade <results>/aggregate-result.json # fixed a grader? re-score without re-running
node tools/release-watch.mjs --state .claude-code-version --update
claude plugin eval ./komo-stack --allow-tools Bash --scaffold --json out.json # when enabled
| Symptom | Cause | Fix |
|---|---|---|
“plugin eval is currently in early access” |
account not enabled | Action falls back to the shim automatically; request access from Anthropic (/bug in Claude Code, or a GitHub issue on anthropics/claude-code) |
shim run isError: true, stderrTail mentions auth |
no ANTHROPIC_API_KEY in CI / no credentials locally |
set the secret; locally the shim copies ~/.claude/.credentials.json |
| every case 0 tool calls, 1 turn | prompt asks for something the model refuses in an empty dir | add case.yaml scaffold; choose a command the model will run |
store step fails on first run |
orphan branch creation quirk | create eval-results manually once: git checkout --orphan eval-results && git rm -rf . && git commit --allow-empty -m init && git push -u origin eval-results |
| scores differ from the official runner | LLM grader votes (1 vs 2-of-3), unsupported grader types | expected; the official runner is authoritative when available |