1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeプラグインを「評価」してから配る:validate+ヘッドレスevalで回帰テストを自動化する

1
Posted at

はじめに:スキルが増えるほど「壊れても気づかない」

当社(合同会社ジョインクラス)では、Claude Codeのエージェント・スキルで会社運営の大半を自動化しています。launchdジョブは17個、自動化率は98%です。

ただ、規模が大きくなると困るのがスキルやプラグインが静かに壊れることです。僕自身、CLAUDE.mdが1,000行を超えたときにルール同士が矛盾し、AIが指示と逆の動作を始めたことがあります。しかも誰も気づきませんでした。プラグインも同じで、SKILL.mdの description を1行変えただけで、スキルが呼ばれなくなることがあります。

そこでこの記事では、プラグインを配布する前に行う評価(eval)のパイプラインを、当社で実際に動いているスクリプトをもとに紹介します。

  1. claude plugin validate で構造を静的チェックする
  2. claude -p(ヘッドレス実行)で「このプロンプトならこのスキルが発火する」を自動判定する
  3. launchdで定期実行して、劣化を検知したらSlackに通知する

⚠️ 最近のClaude Codeには claude plugin eval というサブコマンドがあります。ただし、オプションはバージョンによって変わります。まず claude plugin eval --help を見て、自分の環境の書式を確認してください。この記事のハーネスは、eval があればそれを呼び、なければ claude -p で同じ判定をする作りなので、どちらの環境でも動きます。

前提とディレクトリ構成

my-plugin/
├── .claude-plugin/
│   └── plugin.json
├── skills/
│   └── write-qiita/
│       └── SKILL.md
└── evals/
    ├── cases.tsv        # 評価ケース(プロンプト → 期待スキル)
    └── run-evals.sh     # ハーネス

plugin.json は最小構成で問題ありません。

{
  "name": "joinclass-content",
  "version": "1.3.0",
  "description": "Qiita/Zenn/note記事の生成スキル群"
}

Step 1: 静的チェック(validate)

最初に、壊れたマニフェストやfrontmatterの欠落を止めます。ここで落ちるものに、LLMの実行コストをかける必要はありません。

claude plugin validate ./my-plugin

これに加えて、当社では SKILL.md のfrontmatterを独自にチェックしています。description が空のスキルや、長すぎるスキルは発火精度が落ちるからです。

#!/bin/bash
# evals/lint-skills.sh
set -o pipefail
FAIL=0
for f in skills/*/SKILL.md; do
  desc=$(awk '/^---$/{n++; next} n==1 && /^description:/{sub(/^description: */,""); print}' "$f")
  if [ -z "$desc" ]; then
    echo "NG: $f description がない"; FAIL=1
  elif [ ${#desc} -gt 1024 ]; then
    echo "NG: $f description が長すぎる (${#desc}文字)"; FAIL=1
  fi
done
exit $FAIL

Step 2: 評価ケースを書く

評価ケースはTSVで管理しています。当社の記事テーマ管理(article-theme-ledger.tsv)と同じ形式にそろえておくと、awk や cut でそのまま扱えます。

# evals/cases.tsv   prompt<TAB>expected_skill(noneは「発火してはいけない」)
Qiita記事を書いて。テーマはMCP	write-qiita
Zennに技術記事を出したい	write-zenn
今日の天気は?	none
この関数をリファクタして	none

落とし穴: 「発火すべきケース」だけを並べると、何にでも反応するスキルでも満点になってしまいます。none のケース(誤発火の検知用)を、全体の3割以上は入れてください。

Step 3: ヘッドレスevalハーネス

下のハーネスは、当社の .company/scripts/auto-agent-health-check.sh の骨格(ログ出力・Slack通知・set -o pipefail)を流用しています。

#!/bin/bash
# evals/run-evals.sh
set -o pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CASES="$PLUGIN_DIR/evals/cases.tsv"
LOG_FILE="$PLUGIN_DIR/evals/eval.log"
THRESHOLD=${THRESHOLD:-90}

notify_slack() {
  if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then
    curl -sf -X POST "$SLACK_WEBHOOK_URL" -H 'Content-Type: application/json' \
      -d "$(jq -n --arg text "$1" '{text: $text}')" >/dev/null 2>&1 || true
  fi
}

# 1プロンプトを実行し、呼ばれたスキル名を返す(なければ none)
detect_skill() {
  claude -p "$1" \
    --plugin-dir "$PLUGIN_DIR" \
    --output-format stream-json --verbose \
    --max-turns 1 2>/dev/null |
  jq -r 'select(.type=="assistant") | .message.content[]?
         | select(.type=="tool_use" and .name=="Skill") | .input.skill' |
  head -1 | sed 's/.*://' | grep . || echo none
}

claude plugin validate "$PLUGIN_DIR" || { notify_slack "plugin validate 失敗"; exit 1; }

TOTAL=0; PASS=0; FAILS=""
while IFS=$'\t' read -r prompt expected; do
  [[ -z "$prompt" || "$prompt" == \#* ]] && continue
  TOTAL=$((TOTAL + 1))
  actual=$(detect_skill "$prompt")
  if [ "$actual" = "$expected" ]; then
    PASS=$((PASS + 1))
  else
    FAILS="${FAILS}  - \"${prompt}\" 期待=${expected} 実際=${actual}\n"
  fi
done < "$CASES"

SCORE=$((PASS * 100 / TOTAL))
echo "$(date): score=${SCORE}% (${PASS}/${TOTAL})" >> "$LOG_FILE"
if [ "$SCORE" -lt "$THRESHOLD" ]; then
  notify_slack "[plugin eval] ${SCORE}% < ${THRESHOLD}%\n${FAILS}"
  exit 1
fi

実行すると、次のような結果が出ます。

$ THRESHOLD=90 ./evals/run-evals.sh; echo "exit=$?"
exit=0
$ tail -1 evals/eval.log
Thu Oct 29 22:10:03 JST 2026: score=100% (4/4)

実装上のポイント

  • --max-turns 1: 判定したいのは「最初にどのスキルを選んだか」だけです。スキルを最後まで実行させるとコストが10倍以上になります。
  • stream-json を jq で解析する: 最終テキストを grep で調べる方法は不安定です。tool_use イベントを直接見てください。
  • sed 's/.*://': プラグインのスキルは joinclass-content:write-qiita のように名前空間付きで返ってきます。比較の前に接頭辞を落とします。
  • 1回の判定は揺れる: LLMの選択は確率的です。境界にあるケースだけ3回実行して多数決を取ると、誤検知が大きく減りました。

Step 4: launchdで定期実行する

当社はmacOSでcronが全滅した経験があります(フルディスクアクセスの問題)。それ以来、定期実行はlaunchdに統一しています。

<!-- ~/Library/LaunchAgents/com.joinclass.plugin-eval.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>com.joinclass.plugin-eval</string>
  <key>ProgramArguments</key>
  <array><string>/bin/bash</string><string>/Users/you/my-plugin/evals/run-evals.sh</string></array>
  <key>StartCalendarInterval</key><dict><key>Weekday</key><integer>1</integer><key>Hour</key><integer>6</integer></dict>
</dict></plist>
launchctl load ~/Library/LaunchAgents/com.joinclass.plugin-eval.plist
launchctl list | grep plugin-eval   # 登録されたか確認する

落とし穴: launchdはログインシェルの PATH を読み込みません。スクリプトの先頭で export PATH=... を書き、claude と jq の絶対パスが通るようにしてください。当社のスクリプトはすべて冒頭でnvmのパスを明示しています。

検証方法:わざと壊して確かめる

ハーネスを信頼する前に、故意に壊して赤くなるかを必ず確認します。

# description を空にして、validate または lint で落ちるか
sed -i '' 's/^description:.*/description:/' skills/write-qiita/SKILL.md
./evals/lint-skills.sh; echo "exit=$?"   # → exit=1 になればOK
git checkout skills/write-qiita/SKILL.md

赤にならないテストは、テストではありません。

運用してわかったこと

  • 当社で一番効いたのは、誤発火ケース(none)を追加したことです。記事系スキルが「リファクタして」にも反応していた問題を見つけられました。
  • evalのコストは、--max-turns 1 に絞れば、ケース20件で数十円程度です。AI運用費全体(月約¥20,000)から見れば誤差の範囲です。
  • description を変更するPRでは、必ずevalを通すルールにしました。CLAUDE.mdの肥大化で学んだのは、「書いたルールが効いているか」は計測しないとわからないということです。

まとめ

段階 手段 止めるもの
静的 claude plugin validate + lint 構造破損・frontmatter欠落
動的 claude -p + stream-json判定(claude plugin eval があれば併用) 発火漏れ・誤発火
継続 launchd + Slack通知 時間経過による劣化

プラグインは「作って終わり」ではなく、評価して初めて配れる成果物です。今日、cases.tsv に5行書くところから始めてみてください。

もっと深く知りたい方へ

Skills・プラグインの設計(description の書き方、発火精度を上げるコツ、配布方法)は、書籍『Claude Skills 完全ガイド』で体系的に解説しています。

👉 書籍一覧: https://zenn.dev/joinclass?tab=books

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?