Claude Code plugin evalでスキルは効いているか—Δの測り方

CASE               WITH  W/OUT Δ      RUNS COST    NOTES
ignores-unrelated  1.00  1.00  0.00   6    $0.22
writes-commit      0.67  0.00  +0.67  6    $0.62   exit 1: Reached maximum number of turns (5)

2 case(s) · mean Δ +0.33 · 119s · $0.84

コミットメッセージを書くだけのスキル1本を、Claude Code v2.1.270 の claude plugin eval で採点した集計表です。12 run で119秒、費用は $0.84。WITH が 0.67 で止まった原因はスキルではなく、ケースに書いた max_turns: 5 でした。

目次

claude plugin eval が採点するのは発火と出力

claude plugin evalv2.1.269 で入ったコマンドです。プラグインに同梱したテストケースを実際のモデル呼び出しで回し、点を付けます。Anthropic は公式ドキュメントに “Test plugins with evals” という独立したページを用意していました。

1ケースが6 runになる計算

ケースは、ユーザーが打ちそうなプロンプトと grader(採点ルール)の組です。Claude Code は1ケースを既定で3回、プラグインあり(with)となし(without)の両方で回します。3回×2系統で、1ケースあたり 6 run

各 run は使い捨ての home と作業ディレクトリで claude -p の子プロセスとして動き、1セッション分のモデル呼び出しになります。

スコアは通過した grader の割合の平均

run のスコアは、通過した grader の割合で決まります。weight: 2 の grader は2本分として計算に入り、ケースのスコアは3 runの平均。表の 0.67 は、0点の run が1本と 1.00 が2本という内訳でした。

regextool_usedtool_orderfile_exists の4種類は、トランスクリプトとファイルから計算するので無料です。llmbaseline は判定用のモデルを別に呼び、その分の費用が run に乗ります。今回は無料の grader だけで組んだため、全 run の judgeCostUsd は 0 でした。

skill-creator の evals.json とは別の書式

検索サジェストには「claude code skill creator evals」や「evals.json」も並びます。skill-creator プラグインもスキルを with/without で比べますが、ケースの書き方が違います。Skills のドキュメントは両者について “The two formats aren’t interchangeable.” と書いていました。

項目claude plugin evalskill-creator
ケースの置き場evals/<case>/prompt.mdgraders/*.mdスキル内の evals/evals.json
run の隔離run ごとに使い捨て環境で claude -p を起動テストケースごとにサブエージェントを起動
採点6種類の graderアサーションを判定して grading.json に記録
集計aggregate-result.jsonreport.htmlbenchmark.json と HTML のビューア
向く場面CI での合否判定(--threshold と終了コード)会話の中で1本のスキルを調整する

検証用プラグインにケースを2つ書く

$ find . -type f -not -path './evals/results/*' | sort
./.claude-plugin/plugin.json
./evals/ignores-unrelated/graders/answer.md
./evals/ignores-unrelated/graders/no-skill.md
./evals/ignores-unrelated/prompt.md
./evals/writes-commit/graders/format.md
./evals/writes-commit/graders/skill-fired.md
./evals/writes-commit/prompt.md
./skills/commit-ja/SKILL.md

スキルは、1行目を [chg] で始める社内形式のコミットメッセージを書かせるだけのものです。プラグインの骨組みは Claude Codeプラグインの作り方 で書いた構成と同じです。claude plugin validate . は author 未記入の警告1件だけで通りました。

init –bare が書くのは TODO 入りの2ファイル

公式が勧めるのは、Claude が質問しながらケースを組む claude plugin eval init のほう。ファイルの中身を先に見たかったので、白紙のテンプレートを出す --bare を使いました。

$ claude plugin eval init --bare writes-commit
Created evals/writes-commit/prompt.md and evals/writes-commit/graders/criteria.md

init --bare が書いた prompt.md の frontmatter には、max_turns: 10allowed_tools: [Read, Glob, Grep, Skill] が入っていました。本文は TODO: describe what the agent should do の1行だけです。grader の criteria.mdtype: llm で、判定のたびにモデルを呼ぶため regex と tool_used に書き換えました。

発火すべきケースは結果と経路の2本立て

ケースの本体が evals/writes-commit/prompt.md です。短い依頼なので、テンプレートの 10 を 5 に下げていました。

---
max_turns: 5
timeout_seconds: 120
allowed_tools: [Skill]
---

getUser を fetchUser にリネームして、呼び出し元3箇所も直しました。コミットメッセージを書いてください。

grader は公式の “Choose graders that give a stable signal” に倣い、結果を見るものと経路を見るものに分けました。返答の書式を見るのが graders/format.md

---
type: regex
pattern: '^\[chg\] '
flags: m
weight: 2
---

Skill が呼ばれたかを見るのが graders/skill-fired.md です。

---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:[\w-]+:)?commit-ja"'
---

1 run・with 側だけのパイロットで配線を確かめました。

$ claude plugin eval . --case writes-commit --runs 1 --ablation none \
    --model claude-sonnet-5 --max-cost-usd 1 --trust-plugin --no-publish
  writes-commit run 1/1: score 1.00  $0.13
    ✓ format (weight 2): matched ^\[chg\]
    ✓ skill-fired (weight 1): Skill called 1x (expected 1..∞)

CASE           SCORE PASS% RUNS COST    NOTES
writes-commit  1.00  100%  1    $0.13

1 case(s) · 47s · $0.13

with 側の返答は、コミットメッセージをコードブロックに入れて返してきます。1本は前に英語の説明文まで付いていました。flags: m がないと ^ は文字列の先頭にしか一致しないので、どちらの形でも format は通りません。

input_match(?:[\w-]+:)? も削れない部分です。後の再実行では、3回中1回だけ Skill の入力が "skill": "commit-ja:commit-ja" と名前空間付きになりました。残り2回は "skill": "commit-ja" です。

発火してはいけないケースは min: 0 と arm: both

もう1つは、関係ない依頼でスキルが割り込まないことを確かめるケースです。プロンプトは「Pythonのリスト内包表記で、1から10までの偶数の2乗を作る1行を書いてください。」にしました。

---
type: tool_used
tool: Skill
min: 0
max: 0
arm: both
---

min: 0max: 0 で、Skill の呼び出し0回を合格条件にしています。arm: both が要る理由は、後の「Δ 0.00 のケースを消さない理由」の節で扱います。

  ignores-unrelated run 1/3 [with]: score 1.00  $0.04
    ✓ answer (weight 1): matched range\(\s*2\s*,\s*11\s*,\s*2\s*\)|% ?2 ?== ?0
    ✓ no-skill (weight 1): Skill called 0x (expected 0..0)
✓ ignores-unrelated  with 1.00  without 1.00  Δ 0.00  (6 runs)  $0.22

6 run すべて1ターンで終わり、1 run あたり $0.037、所要は2〜10秒でした。

WITH 0.67 の run はシェルを探して止まった

writes-commit の with 側3 runのうち、1本が exit 1: Reached maximum number of turns (5) で 0 点でした。skill-fired は通っていて、スキル自体は発火しています。

空の作業ディレクトリで差分を探しに行く

失敗した run の trace.jsonl から、ツール呼び出しと最後の結果だけを抜き出しました。

tool_use  Skill       {"skill": "commit-ja"}
tool_use  ToolSearch  {"query": "bash shell run command"}
tool_use  ToolSearch  {"query": "git diff status"}
tool_use  ToolSearch  {"query": "execute shell command powershell"}
text      No shell tool is available here, so I'll locate the rename via search instead.
tool_use  Grep        {"pattern": "fetchUser"}
tool_use  Grep        {"pattern": "getUser"}
result    error_max_turns  Reached maximum number of turns (5)

Skill で書式を読んだ直後から、Claude は差分を確かめるためにシェルを探していました。eval の run は空の作業ディレクトリで始まり、--allow-tools で渡していない Bash は Claude Code がセッションから外します。プロンプトに「3箇所も直しました」とあるので、Claude はリネーム箇所を検索で探しに行きました。

max_turns を既定の10に戻すと3 runとも1.00

公式ドキュメントは prompt.md frontmatter の表で、max_turns についてこう書いています。

Hitting it is recorded as a run error and usually lowers the score, so set it generously

変えたのは1行だけ。プロンプトに差分を貼る手もありますが、原因を切り分けるため max_turns 以外は触っていません。

# NG: 短い依頼だからと削った
max_turns: 5
# OK: テンプレートの既定のまま
max_turns: 10
CASE           WITH  W/OUT Δ      RUNS COST    NOTES
writes-commit  1.00  0.00  +1.00  6    $0.46

1 case(s) · mean Δ +1.00 · 60s · $0.46

Δ は +0.67 から +1.00 に上がり、終了コードも 1 から 0 に変わりました。修正後の with 側の JSON を見ると、turns は 5・6・7 と run ごとに散っています。SKILL.md は1文字も変えていません。

プラグインなしでは英語の Conventional Commits が返る

without 側は3 runとも format で落ちました。--keep-temp で残した trace を開くと、2本は次のメッセージを返しています。

refactor: rename getUser to fetchUser

Update function name and its 3 call sites for clarity.

残り1本はメッセージを書かず、「変更を加えたファイルは保存されていますか」と聞き返して終わりました。費用は without 側のほうが高く、with 側の1 run $0.06〜$0.07 に対して $0.07〜$0.10。without 側の1本はツールを13回呼び、空の作業ディレクトリを Glob で6回なめていました。

Δ 0.00 のケースを消さない理由

“How an eval run works” の “The no-plugin baseline” には、次の一文があります。

If a case scores 1.0 both with and without the plugin, the plugin isn’t what made it pass.

割り込まないことを見るケースは Δ 0 で合格

ignores-unrelated は with も without も 1.00 で、Δ 0.00 でした。引用だけ読むと、削ってよいケースに見えます。ただしこのケースが確かめているのは「関係ない依頼で Skill を呼ばない」こと。プラグインを入れても振る舞いが変わらないのが合格条件で、Δ 0 は意図どおりでした。

HTML レポートの冒頭には Plugin effect: ↑ +33.3 pts vs baselineimproved 1 · flat 1 · regressed 0 of 2 cases が並びました。flat の1件がこのケースで、mean Δ が +0.33 まで下がって見えるのも同じ理由です。

tool_used: Skill は採点から外れて indicator になる

writes-commit の skill-fired は、2系統の実行で表示が変わりました。

    ✓ skill-fired [with-only, not scored]: Skill called 1x (expected 1..∞)
{"name": "skill-fired", "passed": true, "weight": 1, "explanation": "Skill called 1x (expected 1..∞)", "withOnly": true, "scored": false}

公式の “Score against the no-plugin baseline” によると、Skill の呼び出しを見る grader は without 側では通りようがありません。点に入れると without 側が0点に寄り、Δ が膨らみます。Claude Code はこの grader を両方の系統で採点から外し、レポートでは plugin-fired indicator のバッジを付けます。WITH の 0.67 も、format だけで計算した値でした。

no-skill に arm: both を付けたのはこの除外を止めるためで、外すと「呼ばなかった」ことが点に入りません。逆に Δ が0付近で skill-fired が落ちているなら、description が自然な言い回しで発火していません。公式はこれを、eval を始めて真っ先に出やすい結果として挙げていました。直し方は Claude Code Skillの作り方 に書いた description の設計と同じです。

–ablation none では同じ grader が点に入る

パイロットは --ablation none で回したので、skill-fired は "scored": true として点に入っていました。表の列も WITH・W/OUT・Δ ではなく、SCORE と PASS% に変わります。パイロットで 1.00 でも、本番の WITH は format だけで決まります。

CI に載せる前に終了コードを確かめる

今回の実行で出た終了コードを、公式の表と並べました。

終了コード公式の条件(抜粋)今回出した場面
0全ケースが --threshold 以上max_turns を10にした再実行
1閾値未満のケース、未信頼のディレクトリ、不正なオプションWITH 0.67 の本番、--trust-plugin なしの非対話実行、--json .
2--max-cost-usd に達した partial--max-cost-usd 0.01

既定の –threshold 1.0 は1本の失敗で exit 1

トラブルシューティングの “The run exits 1 but the results look fine” に、そのまま書いてありました。

The default --threshold is 1.0, so the command exits 1 when any case scores below perfect.

3 run のうち1本でも落ちれば 1.0 を割ります。公式の CI 例は --threshold 0.8 です。ただし採点対象の grader が1本のケースだと、3 run 中1本の失敗で 0.67 になり、0.8 でも通りません。

–max-cost-usd は run を始める前にだけ見る

$ claude plugin eval . --case writes-commit --runs 3 --ablation none \
    --model claude-sonnet-5 --max-cost-usd 0.01 \
    --json /tmp/eval-lab/ceiling.json --trust-plugin --no-publish
Wrote /tmp/eval-lab/ceiling.json
Report: /private/tmp/eval-lab/commit-ja/evals/results/2026-09-13T11-07-09-762Z/report.html
$ echo $?
2
{"partial": true, "partialReason": "cost_ceiling", "costUsd": 0.05051420000000001, "durationSeconds": 7}

使用額 $0 の時点で1本目が始まり、$0.05 を使ったところで残り2本は起動しませんでした。Claude Code は各 run を起動する直前に、使用額と上限を比べています。-j 4 で並列にすると、走っている4本分まで上限を超える余地が残ります。

–json の後ろに対象を置くとパス扱いになる

# NG
$ claude plugin eval --json .
Error: --json output path must end in .json (got '.'). If that is your eval target, put it before --json.
# OK
$ claude plugin eval . --json results.json

--json は出力先のパスを任意で受け取るので、CLI は直後の . を出力先として読みました。--tag--allow-tools も値を複数取るため、対象は先頭に置きます。OK の形で回したときの標準出力は、上の例と同じく Wrote と Report の行だけでした。

run のあとに残るファイル

成功した run の trace は消える

パイロットの aggregate-result.json は、tracePath/private/tmp/e-ME3IqD/out/trace.jsonl を記録していました。実行後に開くと、ディレクトリごと消えています。中身を読むなら --keep-temp を付けて回し直します。

失敗した run は sealed に封印されて残る

max_turns に達した run だけは、--keep-temp なしでも kept temp (run failed) として残りました。

⚠ kept /private/tmp/e-jXiOYg: home/ and tmp/ in it were written by the plugin under test and are sealed in /private/tmp/e-jXiOYg/sealed (mode 000; the kept directory is read-only)

out/trace.jsonl はそのまま読めます。sealed/ を開くには chmod 700 が要り、CLI は中で git を実行しないよう警告も出していました。

手元の CLAUDE.md も MCP サーバーも読まれない

“How runs are isolated” の最初の項目は “Nothing personal or project-level loads.” です。ユーザー設定、hooks、CLAUDE.md、MCP サーバー、ほかのプラグインは run に入りません。Bash を渡した run は OS のサンドボックスで動き、ホームディレクトリも読めなくなります。サンドボックス側の前提は Claude Code Sandboxの設定 にまとめました。

claude.ai のサブスクリプションでログインしていると、Claude Code がレポートを非公開の Artifact として公開します。手元だけに置くなら --no-publish。Artifact の共有範囲は Claude Code Artifacts入門 で扱っています。

まとめ

  • 1ケースは既定で with/without × 3回の6 run。2ケースで 119秒・$0.84 かかった
  • max_turns は削らない。5 にしただけで WITH が 0.67 に落ち、10 に戻すと 1.00 になった
  • 発火してはいけないケースは Δ 0.00 で合格。tool_used: Skill は2系統の実行では点に入らない
  • CI では --threshold を明示し、exit 2 の partial な結果は推移の記録から外す
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次