🚀 はじめに
Claude Code の Skill(スキル)やプラグインを自作していると、必ずこの疑問にぶつかります。
このスキル、本当に効いてる?
description を書き換えてみたけれど呼ばれているのか分からない。プロンプトを調整したけれど、良くなったのか運が良かっただけなのか分からない。モデルが上がったときに壊れていないかも分からない。
Claude Code v2.1.269 で追加された claude plugin eval は、この「分からない」をスコアに変えるコマンドです。
# プラグイン(スキル)ルートで実行するだけ
claude plugin eval .
CASE WITH W/OUT Δ RUNS COST NOTES
first-case 1.00 0.33 +0.67 6 $0.41
1 case(s) · mean Δ +0.67 · 74s · $0.41
Report: /Users/you/my-plugin/evals/results/2026-09-10T17-02-11-482Z/report.html
WITH がプラグインあり、W/OUT がプラグインなしのスコア、そして Δ がプラグインの貢献分です。この Δ こそが、この機能の一番おいしいところです。
この記事では、実際に自作スキルのリポジトリを題材に、eval スイートの作り方から CI でのゲート化、コスト設計までを一通り追いかけます。
本記事のスコアテーブルやレポート例は公式ドキュメントの例です。eval の実行とジャッジグレーダーは実際のモデル呼び出しで課金が発生するため、この記事では課金なしで完結する範囲(マニフェスト検証・スイートの雛形生成)を手元の v2.1.269 で実機確認し、それ以外は公式ドキュメントに基づいて記述しています。
📝 3行まとめ
-
v2.1.269 で
claude plugin evalが追加。プラグインやスキルをテストケース群にかけて、スコア付き・再現可能な結果(JSON + HTML レポート)を得られる -
目玉は「プラグインなしベースライン」との差分
Δ。スキルが無くても同じ結果なら、そのスキルはスコアに貢献していない、という事実が可視化される -
--thresholdの終了コードで CI ゲート化できる。ただし実行はすべて実モデル呼び出しなので、--ablation noneや無料グレーダー中心の設計でコストを抑えるのが前提
🤔 なぜスキルに eval が必要なのか
スキルは実態としては Markdown で書かれたプロンプトです。つまり、普通のコードのようにユニットテストで守れません。にもかかわらず、次のような壊れ方を普通にします。
| 壊れ方 | 症状 |
|---|---|
| そもそも呼ばれない |
description が曖昧で、ユーザーの自然な言い回しでトリガーされない |
| 呼ばれるが従わない | 手順を書いているのに、出力形式やファイル出力先が守られない |
| 回帰する | スキルを少し直したら、別のケースが壊れる |
| モデル更新で変わる | 新しいモデルで挙動が変わり、以前の前提が崩れる |
| そもそも要らなかった | スキルが無くても Claude が同じ品質で答えていた |
最後の「そもそも要らなかった」は特に厄介で、スキル無しの比較対象がないと永遠に気づけません。claude plugin eval はこれを標準で測ります。
⚙️ eval 実行の仕組み
構造はシンプルです。
- eval スイートはプラグイン内の
evals/ディレクトリに置く -
1 ケース = 1 ディレクトリで、
prompt.md(送るプロンプト)とgraders/(採点方法)を持つ - 各実行では、プラグインだけをロードした隔離された非対話セッションが新規に立ち上がり、空のワークスペースでプロンプトを処理する
- 実行後、各グレーダーが「最終応答・トランスクリプト・生成されたファイル」を見て PASS / FAIL を判定する
そして重要なのがここです。
- エージェントの 1 回の実行はノイズが大きいため、各ケースはデフォルトで 3 回実行される
- 実行のスコアは「(重み付き)合格グレーダーの割合」、ケースのスコアはその平均
- ケースは、そのスコアが
--threshold(デフォルト 1.0)を満たせば合格
さらにデフォルトでは with アーム(プラグインあり)と without アーム(プラグインなし)の 2 本立てで走ります。つまり 1 ケース = 3 実行 × 2 アーム = 6 実行です。ここはコストに直結するので後述します。
ケース定義はエージェントから隠されています。実行中の Claude は eval ディレクトリを読めないため、プロンプト・グレーダー・兄弟ケースを覗いてカンニングすることはできません。
🧪 自作スキルを eval にかけてみる
ここからは、筆者が公開している qiita-claude-skills のような「.claude/skills/ にスキルを並べただけのリポジトリ」を題材にします。
qiita-claude-skills/
└── .claude/
└── skills/
├── qiita/
│ └── SKILL.md
└── qiita-claude-code-release-notes/
└── SKILL.md
1. eval の対象は「プラグイン」である
claude plugin eval のターゲットは パス / プラグイン名 / plugin@marketplace の ID です。そして対象になるのは、plugin.json または .claude-plugin/plugin.json のマニフェストを持つプラグインディレクトリ(または skills-directory プラグイン)です。
つまり SKILL.md が 1 枚あるだけのディレクトリは、そのままでは eval のターゲットになりません。マニフェストを 1 枚置くのが最初の一歩です。
プラグインルートに SKILL.md があり、skills/ サブディレクトリも skills マニフェストフィールドも無い場合は「単一スキルプラグイン」として扱われるので、マニフェストは最小限で済みます。
mkdir -p .claude/skills/qiita/.claude-plugin
cat > .claude/skills/qiita/.claude-plugin/plugin.json <<'EOF'
{
"name": "qiita",
"description": "Qiita投稿用の記事を作成するスキル",
"version": "0.1.0"
}
EOF
構文とスキーマは claude plugin validate で確認できます(こちらはモデルを呼ばないので無料です)。
claude plugin validate .claude/skills/qiita
Validating plugin manifest: /path/to/.claude/skills/qiita/.claude-plugin/plugin.json
‼ Found 1 warning:
> author: No author information provided. Consider adding author details for plugin attribution
√ Validation passed with warnings
author を足せば警告も消えます。挙動ではなく構文を見るのが validate、挙動を見るのが eval という住み分けです。
2. スイートを作る
推奨は対話型の init です。Claude がプラグインを読み、「良い結果とは何か」を質問し、トリガーすべき/すべきでないプロンプトを提案し、グレーダーを設計し、1 回パイロット実行してからファイルを書いてくれます。
cd .claude/skills/qiita
claude plugin eval init
初回は Trust this plugin directory? と聞かれるので y で答えます。
一方「まず中身を自分の目で確認したい」「CI で雛形だけ欲しい」という場合は --bare で空のテンプレートを作れます。こちらはモデルを呼ばず、ファイルを置くだけです。実際に手元で叩いた結果がこちら。
claude plugin eval init --bare writes-article
Created evals/writes-article/prompt.md and evals/writes-article/graders/criteria.md
evals/writes-article/
├── prompt.md
└── graders/
└── criteria.md
生成された prompt.md:
---
max_turns: 10
allowed_tools: [Read, Glob, Grep, Skill]
---
TODO: describe what the agent should do
生成された graders/criteria.md:
---
type: llm
weight: 1
---
TODO: describe what a successful response looks like
この TODO を埋めるのが作業の中心です。
3. プロンプトを書く
prompt.md の本文は「ユーザーが実際に打ちそうな言い回し」にします。ここを「qiita スキルを使って記事を書いて」のようにスキル名を明示してしまうと、トリガー精度のテストにならない点に注意してください。
---
max_turns: 15
timeout_seconds: 600
allowed_tools: [Read, Glob, Grep, Write, Skill]
tags: [smoke]
---
Claude Code の Hooks について、Qiita に投稿する記事を書いてください。
各実行は空のワークスペースから始まるので、タスクに必要な材料はプロンプト自体に入れるか、後述の case.yaml の context でワークスペースを用意します。本文中の @path はファイル添付には展開されないので、ファイルを読ませたいなら allowed_tools でツールを渡します。
4. グレーダーを書く
「答えが正しかったか」と「そこへスキル経由で到達したか」の 2 系統を必ず入れるのがコツです。
① スキルが発火したか(tool_used)
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:[\w-]+:)?qiita"'
---
plugin-name:skill-name という名前空間付きの形式にもマッチします。
② 記事ファイルが生成されたか(file_exists)
---
type: file_exists
path: "**/*_qiita_*.md"
---
実行中に作成されたファイルだけがカウントされます。
③ 構成ルールを守っているか(regex)
SKILL.md に「3行まとめ」を含めよと書いてあるなら、それを機械的に検査できます。
---
type: regex
pattern: "3行まとめ"
target: files
---
④ 記事として成立しているか(llm)
---
type: llm
weight: 2
---
PASS if 記事が日本語で書かれており、「はじめに」「3行まとめ」「まとめ」「参考リンク」に相当する節をすべて含み、コードブロックが 1 つ以上ある。
FAIL if 英語で書かれている、節が欠けている、または本文が箇条書きの羅列だけで解説文がない。
llm グレーダーはジャッジモデルが 3 回中 2 回 PASS に投票すれば合格という多数決方式です。
📊 グレーダーは 6 種類
カスタムコードグレーダーはありません。用意された 6 種から選びます。
| タイプ | 主なオプション | 合格条件 | 課金 |
|---|---|---|---|
regex |
pattern / flags / match / target
|
JavaScript 正規表現がターゲットにマッチ(match: not_contains で不在、match: "count:N" で回数指定) |
無料 |
tool_used |
tool / input_match / min / max
|
対象ツールの呼び出し回数が min〜max の範囲。min: 0, max: 0 で「呼ばれていないこと」を主張 |
無料 |
tool_order |
before / after
|
両方のツールが呼ばれ、before が after より先 |
無料 |
file_exists |
path / exists
|
実行中に作成されたファイルが glob にマッチ(exists: false で不在) |
無料 |
llm |
criteria / focus
|
ジャッジモデルが 3 回中 2 回 PASS に投票 | 課金 |
baseline |
baseline_file / criteria
|
参照トランスクリプトと同等に基準を満たしていると判定 | 課金 |
安定したスコアを得るためのポイントは以下です。
- 生成ファイルのような長い出力は
regexで検査する。毎回同じ判定になる -
llmは短い出力に使い、ルーブリックは「PASS if / FAIL if」の具体条件で書く - 各ケースに「結果に対するグレーダー」と「到達経路に対するグレーダー(
tool_used/tool_order)」を 1 つずつ -
llmのジャッジはデフォルトで小さく速いモデル。ニュアンスのある判定が必要なら--judge-model sonnet
⚖️ 最重要:Δ(アブレーション)の読み方
単独のスコアが高いだけでは、プラグインが役に立った証拠になりません。プラグイン無しでも Claude が同じ結果を出せるかもしれないからです。
そこで各ケースはデフォルトで with / without の 2 アームで走り、その差が Δ として出ます。
-
WITH 1.00/W/OUT 1.00/Δ 0.00→ スキルが理由で合格したわけではない -
WITH 1.00/W/OUT 0.33/Δ +0.67→ スキルが効いている -
Δがマイナス → スキルが邪魔をしている可能性(あるいはジャッジがフォーマットに引きずられている)
ここで公平性のための仕掛けがあります。「スキルが呼ばれたか」のようなチェックはプラグイン無しでは絶対に合格できないため、そのままカウントすると without 側が 0 に寄って Δ が水増しされます。そのため、
-
tool: Skillのtool_usedグレーダー -
arm: with-onlyを指定したグレーダー
は両アームのスコアから除外され、with アームでは「plugin-fired indicator(発火したかどうかの指標)」として報告されます。レポート上もバッジが付きます。
そして、最初に遭遇しがちな典型パターンがこれです。
tool_used: Skillが FAIL していてΔがほぼゼロ → Claude が自然な言い回しでスキルを選んでいない
この場合に直すべきは本文ではなく、SKILL.md の description です。直して claude plugin eval . を回し直して比較する、というのが基本の改善ループになります。
なお、グレーダーを調整している段階など Δ が要らない場面では --ablation none を付けると with アームのみになり、コストが半分になります(テーブルは WITH / W/OUT / Δ ではなく SCORE / PASS% 表示に変わります)。
# 1 ケースだけ、1 実行、片アームで素早く回す
claude plugin eval . --case writes-article --runs 1 --ablation none
1 実行はノイズが大きいので、最終判断はデフォルトの 3 実行に戻してから行いましょう。また --ablation none では除外処理が働かないため、同じスイートでもモードによって絶対スコアが変わります。
🗂 フィクスチャとモック
「空のワークスペースから始まる」制約は、case.yaml の context ブロックで補えます。
schema_version: "1.1"
name: changelog-from-diff
tags: [smoke]
context:
scaffold_script: fixture.sh # 事前セットアップ用の Bash(--scaffold 指定時のみ実行)
add_dirs: [resources] # 実行中に読ませたいフィクスチャ
# history_file: prev.jsonl # 途中からの会話を再現したい場合
MCP を呼ぶスキルの場合は、実サーバーを起動せずに 1 ツール = 1 Markdown のモックで答えさせられます。
---
expect:
title: string
priority: [low, medium, high]
---
Created issue #4821: {{input.title}}
evals/mocks/<server>/<tool>.md に置けばスイート全体、ケース配下の mocks/ に置けばそのケース専用です。expect: に違反する呼び出しはスコア 0 で中断されるため、「プラグインがサーバーに何を渡したか」まで主張できます。
🤖 CI でスコアをゲートする
--threshold を下回ると終了コード 1 で落ちるので、そのまま CI のゲートになります。
name: plugin-eval
on:
pull_request:
paths:
- '.claude/skills/**'
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm install -g @anthropic-ai/claude-code
- name: Run plugin evals
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude plugin eval .claude/skills/qiita \
--trust-plugin \
--json results.json \
--threshold 0.8 \
--model claude-sonnet-5 \
--judge-model claude-haiku-4-5 \
--no-publish \
--max-cost-usd 5
- uses: actions/upload-artifact@v4
if: always()
with:
name: eval-results
path: results.json
ポイントは 4 つです。
-
--trust-plugin: 初回の信頼プロンプトで CI が止まらないようにする(--jsonを付けた時点で対話できないため、これが無いと終了コード 1 で拒否されます) -
--modelと--judge-modelを固定: スコアを時系列で比較可能にする -
--no-publish: HTML レポートをローカルに留める -
--max-cost-usd: 予算の上限。各実行の起動前にチェックされる
終了コードの意味は以下の通りです。
| 終了コード | 意味 |
|---|---|
0 |
全ケースが --threshold 以上。全ケースファイルがロード成功 |
1 |
閾値未満、ケースのロード失敗、ケースが見つからない、実行を開始できない、未信頼ディレクトリで --trust-plugin 無し、オプション不正 |
2 |
部分実行(--max-cost-usd 到達、または認証情報の拒否)。results.json に partial: true と理由が書かれる |
130 |
中断(部分結果は書き出される) |
143 |
終了(CI タイムアウトなど) |
HTML レポートの書き込みや公開の失敗は終了コードを変えません。なお claude plugin eval init は質問のためにターミナルが必要なので、CI では --bare を使います。
evals/results/ には実行のたびに成果物が書き出されるので、.gitignore に追加しておきましょう。
evals/results/
💰 コスト設計
**eval 実行もジャッジグレーダーも、あなたのアカウントでの実モデル呼び出しです。**プランの使用量または API 請求にカウントされます。
ざっくりした規模感は以下です。
エージェント実行数 ≒ ケース数 × runs × 2(with / without)
ジャッジ呼び出し数 ≒ 実行数 × (llm / baseline グレーダー数) × 3
抑えるための定石:
- 毎コミットで回す軽量スイートは、ジャッジを呼ばないグレーダー(
regex/tool_used/tool_order/file_exists)だけで構成する -
Δが要らない場面は--ablation noneで半額 - イテレーション中は
--runs 1 --case <name> -
--max-cost-usdでハードリミット。上限に当たると有料グレーダーはスキップされ、無料グレーダーだけで採点されます(partial: trueが付いた結果はトレンド集計から除外するのが安全) - 並列化は
-j/--concurrency(1〜8)。ただし各実行は自分の認証情報で動く本物の子プロセスなので、同じレート制限を共有します
🔐 セキュリティ上の注意
claude plugin eval は プラグインをあなたの権限で、あなたのマシン上で実行します。実行時のサンドボックスは悪意あるプラグインができることを制限しますが、保証ではありません。信頼できるプラグインだけを評価してください。
特に、次のフラグは「明示的に危険側へ倒す」ものです。
| フラグ | 何が起きるか |
|---|---|
--trust-plugin |
初回の信頼プロンプトをスキップ(CI 向け。--dangerously-skip-permissions と同種の判断) |
--scaffold |
ケースの scaffold_script(作者提供の Bash)をあなたとして実行 |
--allow-tools |
Bash / Write / Edit / WebFetch / mcp__* など、読み取り専用セットを超えるツールを解禁 |
--allow-real-servers / --mocks off
|
プラグインの実 MCP サーバーを起動。OS サンドボックスの外で動く |
フックや実サーバーはエージェントのサンドボックス外で動き、グレーダーが読むファイルに触れる可能性があります。コンテナや CI ランナーなど隔離環境で回さない限り、そのスコアは参考値として扱うのが安全です。また「バンドルされたスイートが合格すること」はセキュリティ検証ではありません。
🧯 ハマりどころ
| 症状 | 対処 |
|---|---|
plugin eval is currently in early access |
ビルドが古い。claude update してから新しいセッションで再実行 |
plugin eval is currently unavailable |
サーバー側でオフにされている。手元では戻せないので claude update して後で再試行 |
is not a trusted plugin directory, and this run cannot stop to ask you about it |
初回実行かつ非対話(--json 指定含む)。一度ターミナルで実行して承認するか --trust-plugin
|
No eval cases found |
eval ディレクトリ配下に <case>/prompt.md か case.yaml が無い。あるいは --case / --tag のフィルタ不一致 |
| スコアが落ちる原因が見えない |
--json を外してローカル実行すると、実行ごとの進捗行とグレーダー行が出る |
evals/ が別ツールで使われている場合は、plugin.json に "experimental": { "evals": "quality/evals" } を書くか、--eval-dir quality/evals を渡して置き場所を変えられます。
✅ まとめ
claude plugin eval は、これまで「なんとなく良くなった気がする」で運用されていたスキル/プラグイン開発に、スコアという共通言語を持ち込むコマンドです。
特に価値があるのは、単なる合否ではなく プラグインなしベースラインとの差分 Δ を標準で測ってくれる点です。「スキルを消しても結果が変わらない」という耳の痛い事実まで含めて可視化されます。
今日からやれるアクションは次の 3 つです。
-
claude --versionで v2.1.269 以降であることを確認する(古ければclaude update) - 自作スキルに
.claude-plugin/plugin.jsonを置き、claude plugin validateを通す(ここまで無料) -
claude plugin eval initで 1 ケースだけ作り、--runs 1 --ablation noneで安く回して感触を掴む
そのうえで、tool_used: Skill が落ちていないか、Δ が正になっているかを見ながら description を磨いていく。この 1 周を経験すると、スキル開発の解像度がかなり変わります。