はじめに
前回の記事「Skillsを50個運用して気づいた — 増やすほど生産性が下がるパラドックス」では、Agent Skillsが増えるほど管理コストが膨らむ問題を提起しました。同じ課題を抱えている方が想像以上に多いことがわかりました。
ただ、前回は「問題提起」で終わっていました。「じゃあどうすればいいのか」が宙ぶらりんのままでした。
あれから数ヶ月、50個以上のSkillを運用する自分の環境で試行錯誤を重ねました。本記事では、その結果たどり着いたCI/テスト/品質管理の設計を、実際の障害事例やコードとともに共有します。
本記事では .claude/skills/ を標準パスとして使います。旧来の .claude/commands/ もClaude Code上は同じ動作ですが、Agent Skills標準仕様(agentskills.io)に合わせて .claude/skills/ に統一しています。
Skillsが壊れる4パターン — 実際のエラーと調査過程
まず、Skillが壊れる典型パターンを整理します。テスト戦略はこの分類から逆算して設計しました。抽象的な話ではなく、自分が実際に遭遇した障害を具体的に書きます。
パターン1: Claude Codeのアップデートで動かなくなる
これが一番厄介です。Skill自体のコードは一切変えていないのに、ある日突然動かなくなります。
自分が遭遇した実例を挙げます。Claude Codeのアップデートで、PreToolUse/PostToolUse HookがWrite/Edit/Readツールに渡すfile_pathの形式が変わりました。以前は相対パスが渡されるケースがありましたが、あるバージョンから絶対パスに統一されました。
自分のHookスクリプトでは、file_pathの先頭が./で始まる前提で文字列処理をしていました。絶対パスに変わった結果、パスの判定が常にfalseになり、Hookが実質的に何もしなくなりました。
症状はこうです。
# Hookのログに何もエラーは出ない
# ただ、期待する処理が実行されていない
# Skillを手動で呼んでも「完了しました」と返る
# でも出力ファイルが生成されていない
厄介なのは、エラーが出ないことです。Hookは正常に発火しているけれど、条件分岐で弾かれて処理がスキップされていました。気づいたのは3日後、自分で使って「あれ、ファイルが出力されてない」と違和感を覚えたときです。
原因の特定には半日かかりました。Hookスクリプトにechoでデバッグログを仕込み、file_pathの中身を確認してようやく判明しました。
Claude Codeのアップデート後は、Hook経由の変数(file_path等)が想定通りの形式かを必ず確認してください。公式のChangelogにはHookの挙動変更が記載されることがあります。
パターン2: 依存するMCPサーバーの仕様変更
MCPサーバーのツール名やパラメータが変わると、それを参照しているSkillが壊れます。
自分の場合、Google Workspace系のMCPサーバーで認証フローが変更されました。update-financeスキルはGoogle Sheetsからデータを取得するため、google_sheets_readというツールを呼んでいました。MCPサーバーのアップデート後、ツール名がgoogle_sheets_get_valuesに変わりました。
# Skillのログ(Claude Codeの出力)
Tool not found: google_sheets_read
Available tools: google_sheets_get_values, google_sheets_update_values, ...
このパターンはエラーメッセージが明確なので、原因特定は比較的簡単です。ただし、気づくのが遅れる問題は残ります。update-financeは月に数回しか使わないSkillなので、壊れてから2週間気づきませんでした。
パターン3: Skill間の競合
同じトリガーワードを複数のSkillが奪い合うケースです。
trend-checkとtrend-digestがまさにこの問題を起こしました。両方ともdescriptionに「トレンド」というキーワードを含んでいて、「トレンドは?」と聞いたときにどちらが発火するか不安定でした。
# trend-check のdescription
description: トレンド情報を収集し output/trends/ に保存
# trend-digest のdescription
description: 8チャンネルのトレンド収集を並列実行し、横断マージしたダイジェストを生成
「トレンドは?」と聞くとtrend-checkが起動することもあれば、trend-digestが起動することもありました。ユーザーの意図としては軽い確認のつもりが、8チャンネル並列の重い処理が走ることもあったわけです。
対処として、trend-digestにはdisable-model-invocation: trueを設定し、明示的に/trend-digestで呼ばないと起動しないようにしました。
パターン4: 放置されて陳腐化したSkill
作った当初は便利でも、ワークフローの変化で使われなくなるSkillがあります。自分の環境では19個中4個がこのカテゴリに入っていました。
具体的に廃止した2つのSkillを挙げます。
1つ目はslack-notifyです。Slackへの通知を自動化するSkillでしたが、途中からDiscordに移行したため使わなくなりました。CLAUDE.mdのトリガーワード一覧には残っていたので、Claude側のコンテキストを無駄に消費していました。
2つ目はcompetitor-dailyです。競合調査を毎日自動実行するSkillでしたが、GitHub Actionsの課金が想定以上に膨らんだため、週次の手動実行(competitor-research)に切り替えました。旧Skillが残り続け、cronジョブも止め忘れていました。
テスト戦略の設計 — LLMベースのSkillをどうテストするか
Skillのテストは通常のソフトウェアテストとは根本的に異なります。LLMの出力は非決定的なので、「期待値と完全一致するか」というテストは書けません。
ここで重要なのは「決定論的に検証可能な部分」と「人間のレビューが必要な部分」を明確に線引きすることです。
| 検証の種類 | 検証可能性 | テスト手法 |
|---|---|---|
| Skillファイルの構造 | 決定論的 | frontmatterバリデーション |
| 依存ツールの存在 | 決定論的 | MCP参照チェック |
| 出力ファイルの生成 | 決定論的 | パス存在確認 |
| 出力のJSON構造 | 決定論的 | JSONスキーマ検証 |
| トリガーの正確さ | 半決定論的 | トリガー評価セット |
| 出力の品質・正確さ | 非決定論的 | 人間レビュー + promptfoo |
この線引きをもとに、3層のテスト戦略を設計しました。
レイヤー1: 静的解析(構造テスト)
Skillを実行せずに検証できるテストです。コストゼロで、CIの最初のゲートとして機能します。
#!/bin/bash
# scripts/test-skill-structure.sh
# Skillファイルの構造を静的に検証する
EXIT_CODE=0
for skill_dir in .claude/skills/*/; do
skill_file="${skill_dir}SKILL.md"
name=$(basename "$skill_dir")
# SKILL.mdの存在チェック
if [ ! -f "$skill_file" ]; then
echo "ERROR: SKILL.md not found in $skill_dir"
EXIT_CODE=1
continue
fi
# frontmatterの存在チェック
if ! head -1 "$skill_file" | grep -q "^---"; then
echo "ERROR: No frontmatter in $skill_file"
EXIT_CODE=1
fi
# descriptionの存在チェック
if ! grep -q "^description:" "$skill_file"; then
echo "WARN: No description in $skill_file"
fi
# ファイルサイズチェック(空ファイル検出)
if [ ! -s "$skill_file" ]; then
echo "ERROR: Empty skill file: $name"
EXIT_CODE=1
fi
# 最大行数チェック(500行以内推奨)
lines=$(wc -l < "$skill_file")
if [ "$lines" -gt 500 ]; then
echo "WARN: $name has $lines lines (recommended: <500)"
fi
# name フィールドとディレクトリ名の一致チェック
declared_name=$(grep "^name:" "$skill_file" | head -1 | sed 's/name: *//')
if [ -n "$declared_name" ] && [ "$declared_name" != "$name" ]; then
echo "ERROR: name '$declared_name' != directory '$name'"
EXIT_CODE=1
fi
done
exit $EXIT_CODE
加えて、Agent Skills標準仕様への準拠チェックにはskills-refを使います。
# skills-ref のインストール
pip install agentskills
# 個別Skillの検証
agentskills validate .claude/skills/trend-check
# 全Skillの一括検証
for dir in .claude/skills/*/; do
echo "=== $(basename "$dir") ==="
agentskills validate "$dir"
done
agentskills validateは、frontmatterの必須フィールド、name規約(小文字・ハイフンのみ、64文字以内)、descriptionの存在と長さを検証します。Agent Skills標準仕様に準拠しておくと、Claude Code以外のエージェント(Cursor、VS Code Copilot、Gemini CLI等)でも同じSkillが動作するため、品質管理の基盤になります。
レイヤー2: 副作用テスト(出力の構造検証)
Skillを実際に実行し、出力の「構造」を検証します。中身の品質ではなく、「正しいパスにファイルが生成されたか」「JSONとしてパースできるか」を見ます。
#!/bin/bash
# scripts/test-skill-output.sh
# Skillの出力構造を検証する
SKILL_NAME="$1"
SKILL_DIR=".claude/skills/${SKILL_NAME}"
SKILL_FILE="${SKILL_DIR}/SKILL.md"
# 出力先ディレクトリの抽出と検証
OUTPUT_DIRS=$(grep -oP 'output/[a-zA-Z0-9_/]+' "$SKILL_FILE" | sort -u)
for dir in $OUTPUT_DIRS; do
if [ ! -d "$dir" ]; then
echo "WARN: Output directory missing: $dir"
echo " Creating: mkdir -p $dir"
mkdir -p "$dir"
fi
done
# MCP依存ツールの存在チェック
if [ -f .mcp.json ]; then
DEFINED_SERVERS=$(jq -r '.mcpServers | keys[]' .mcp.json)
REFERENCED=$(grep -oP 'mcp__\K[a-zA-Z0-9_]+' "$SKILL_FILE" 2>/dev/null | sort -u)
for ref in $REFERENCED; do
# mcp__<server>__<tool> 形式からサーバー名を抽出
server=$(echo "$ref" | cut -d'_' -f1)
if ! echo "$DEFINED_SERVERS" | grep -q "$server"; then
echo "ERROR: MCP server '$server' not found in .mcp.json"
fi
done
fi
echo "PASS: $SKILL_NAME output structure check completed"
Skillが生成するファイルがJSON形式の場合は、スキーマ検証も組み込みます。
# 出力ファイルのJSON検証例
OUTPUT_FILE="output/trends/2026-04-02-ai.md"
if [ -f "$OUTPUT_FILE" ]; then
# frontmatterがYAMLとしてパースできるか
head -20 "$OUTPUT_FILE" | python3 -c "
import sys, yaml
try:
yaml.safe_load(sys.stdin)
print('PASS: Valid YAML frontmatter')
except yaml.YAMLError as e:
print(f'FAIL: Invalid YAML: {e}')
sys.exit(1)
"
fi
レイヤー3: トリガー精度テスト
Skillが「正しいタイミングで」起動するかを検証します。これはClaude Code公式のskill-creatorプラグインが提供する評価手法に基づいています。
[
{"query": "トレンドは?", "expected_skill": "trend-check", "should_trigger": true},
{"query": "ダイジェスト作って", "expected_skill": "trend-digest", "should_trigger": true},
{"query": "今日の予定は?", "expected_skill": "daily-schedule", "should_trigger": true},
{"query": "記事を書いて", "expected_skill": "write-draft", "should_trigger": true},
{"query": "経理を更新して", "expected_skill": "update-finance", "should_trigger": true},
{"query": "コードをリファクタリングして", "expected_skill": null, "should_trigger": false},
{"query": "このバグを直して", "expected_skill": null, "should_trigger": false},
{"query": "テストを書いて", "expected_skill": null, "should_trigger": false}
]
肯定例(発火すべきケース)と否定例(発火すべきでないケース)を合わせて13件以上用意します。skill-creatorのrun_eval.pyで自動評価し、保留テストセットで5/5を目標にします。
トリガー精度テストはdescriptionの改善に直結します。スコアが低いSkillは、descriptionのキーワードを見直すことで改善できることが多いです。
リグレッションテスト — 前回との差分比較
LLMの出力を「正解」と比較するのは難しいですが、「前回の出力と今回の差分が許容範囲か」は検証できます。
#!/bin/bash
# scripts/test-regression.sh
# 前回の出力と比較して大きな差分がないか確認
SKILL_NAME="$1"
BASELINE_DIR="tests/baselines/${SKILL_NAME}"
OUTPUT_DIR="output/"
if [ ! -d "$BASELINE_DIR" ]; then
echo "INFO: No baseline found. Creating initial baseline."
mkdir -p "$BASELINE_DIR"
# 現在の出力をベースラインとして保存
cp "${OUTPUT_DIR}"*.md "$BASELINE_DIR/" 2>/dev/null
exit 0
fi
# 構造の差分を比較(行数、セクション数、見出し構成)
for baseline_file in "$BASELINE_DIR"/*.md; do
fname=$(basename "$baseline_file")
current_file="${OUTPUT_DIR}${fname}"
if [ ! -f "$current_file" ]; then
echo "WARN: Expected output missing: $fname"
continue
fi
# 見出し構成の比較
baseline_headings=$(grep "^#" "$baseline_file" | wc -l)
current_headings=$(grep "^#" "$current_file" | wc -l)
diff_headings=$((current_headings - baseline_headings))
if [ ${diff_headings#-} -gt 3 ]; then
echo "WARN: Heading count changed significantly: $fname"
echo " Baseline: $baseline_headings, Current: $current_headings"
fi
done
CIパイプラインの設計
PRトリガーのCI
Skill関連ファイルが変更されたPRでのみ実行します。
# .github/workflows/skill-quality-gate.yml
name: Skill Quality Gate
on:
pull_request:
paths:
- '.claude/skills/**'
- '.mcp.json'
- 'CLAUDE.md'
push:
branches: [main]
paths:
- '.claude/skills/**'
jobs:
structure-check:
name: Structure Validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install skills-ref
run: pip install agentskills
- name: Validate Agent Skills spec compliance
run: |
EXIT_CODE=0
for dir in .claude/skills/*/; do
echo "=== $(basename "$dir") ==="
if ! agentskills validate "$dir"; then
EXIT_CODE=1
fi
done
exit $EXIT_CODE
- name: Check skill structure
run: bash scripts/test-skill-structure.sh
- name: Detect unused skills
run: |
# CLAUDE.mdで参照されているSkill名を抽出
referenced=$(grep -oP '`/[a-zA-Z0-9-]+`' CLAUDE.md \
| tr -d '`/' | sort -u)
# 実際に存在するSkillディレクトリ名
existing=$(ls .claude/skills/ 2>/dev/null | sort -u)
echo "--- Unreferenced skills ---"
comm -23 <(echo "$existing") <(echo "$referenced")
conflict-check:
name: Trigger Conflict Detection
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check description keyword overlap
run: |
# 全Skillのdescriptionを抽出し、重複キーワードを検出
declare -A KEYWORDS
for dir in .claude/skills/*/; do
name=$(basename "$dir")
desc=$(grep "^description:" "${dir}SKILL.md" \
| sed 's/description: *//' 2>/dev/null)
# 日本語の単語分割は簡易的にカタカナ・漢字の連続で行う
words=$(echo "$desc" | grep -oP '[\p{Katakana}\p{Han}]+' \
| sort -u)
for word in $words; do
if [ -n "${KEYWORDS[$word]}" ]; then
echo "WARN: Keyword '$word' shared by" \
"'${KEYWORDS[$word]}' and '$name'"
fi
KEYWORDS[$word]="$name"
done
done
pr-diff-summary:
name: PR Diff Summary
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate skill diff summary
run: |
DIFF=$(git diff origin/main...HEAD -- .claude/skills/)
if [ -n "$DIFF" ]; then
echo "## Skill Changes in This PR" >> summary.md
echo '```diff' >> summary.md
echo "$DIFF" | head -100 >> summary.md
echo '```' >> summary.md
fi
- name: Comment on PR
if: hashFiles('summary.md') != ''
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('summary.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
定期ヘルスチェック(cron)
壊れたことに気づけない問題への対策です。週次で全Skillをチェックします。
# .github/workflows/skill-health-check.yml
name: Skill Health Check
on:
schedule:
- cron: '0 9 * * 1' # 毎週月曜 9:00 UTC
jobs:
health-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Full skill validation
run: |
pip install agentskills
echo "# Weekly Skill Health Report" > report.md
echo "| Skill | Structure | Spec | Dependencies |" >> report.md
echo "|-------|-----------|------|-------------|" >> report.md
for dir in .claude/skills/*/; do
name=$(basename "$dir")
# 構造チェック
struct="OK"
[ ! -s "${dir}SKILL.md" ] && struct="FAIL"
# 仕様準拠チェック
spec="OK"
agentskills validate "$dir" > /dev/null 2>&1 || spec="FAIL"
# 依存チェック
deps="OK"
if grep -q 'mcp__' "${dir}SKILL.md" 2>/dev/null; then
deps="HAS_DEPS"
fi
echo "| $name | $struct | $spec | $deps |" >> report.md
done
- name: Post to issue
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('report.md', 'utf8');
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Weekly Skill Health Report - ${new Date().toISOString().split('T')[0]}`,
body: body,
labels: ['skill-health']
});
CIでのClaude APIコスト管理
CIでSkillを実際に実行するテストを組む場合、APIコストが問題になります。自分は以下のルールで管理しています。
- 静的解析(レイヤー1)はAPIコストゼロ。PRごとに毎回実行
- 副作用テスト(レイヤー2)はAPI不要のチェックのみCIで実行
- トリガー精度テスト(レイヤー3)は月次のFull Stocktakeでのみ実行
- CIでClaude APIを叩く場合は
effort: lowを指定してコスト削減
# コスト制御の例: 月間予算を環境変数で管理
env:
CLAUDE_CI_MONTHLY_BUDGET: "10.00" # USD
CLAUDE_CI_EFFORT: "low"
自分の環境(2026年3月時点、50個以上のSkill)での実測値として、静的解析は30秒以内で完了します。APIを叩くテストを含めても、月間コストは$5未満に収まっています。
品質スコアの設計 — なぜこの4観点か
everything-claude-codeのskill-stocktakeの結果をもとに、各Skillに品質スコアをつけています。
4観点の選定理由
| 観点 | 配点 | 選定理由 |
|---|---|---|
| 利用頻度 | 25点 | 使われないSkillはコンテキストの無駄遣い |
| 依存の健全性 | 25点 | 外部依存が壊れると最も復旧コストが高い |
| ドキュメント | 25点 | descriptionが不十分だとトリガー精度が低下 |
| 最終更新日 | 25点 | 90日放置はワークフロー変化の兆候 |
検討して外した観点が2つあります。
「コード行数」は一時期含めていましたが、Skillの複雑さと行数は相関しませんでした。10行のSkillでも高機能なものがあり、200行でも単純なテンプレート出力のものがあります。行数だけで品質は測れません。
「テストカバレッジ」も検討しましたが、LLMベースのSkillにカバレッジの概念を適用するのは無理がありました。代わりに、構造テストの通過率をレイヤー1のゲートで担保しています。
50点未満のSkillをどう扱うか
判断プロセスは以下の通りです。
50点未満のSkill
├─ 直近30日の利用が0回 → 廃止候補
│ ├─ 代替手段がある → 即廃止
│ └─ 代替手段がない → 30日の猶予期間
│ ├─ 猶予期間中に使用あり → リファクタリング
│ └─ 猶予期間中も利用なし → 廃止
└─ 利用はあるが他の観点が低い → リファクタリング
├─ 依存の健全性が低い → 依存先の更新 or 代替
├─ ドキュメントが不十分 → description改善
└─ 最終更新が古い → 現行ワークフローに合わせて更新
実際に廃止したslack-notifyは、利用頻度0点・最終更新10点で合計35点でした。Discord移行後に代替手段があったため即廃止しました。competitor-dailyは利用頻度0点・依存の健全性5点(GitHub Actionsのcron設定が残骸化)で合計30点。週次手動版のcompetitor-researchに統合しました。
スコアの推移を追う
月次でFull Stocktakeを実行し、結果をMarkdownで記録しています。
#!/bin/bash
# scripts/skill-quality-score.sh
REPORT_FILE="output/skill-scores/$(date +%Y-%m).md"
mkdir -p output/skill-scores
echo "# Skill Quality Score - $(date +%Y-%m)" > "$REPORT_FILE"
echo "| Skill | Frequency | Deps | Docs | Updated | Total |" >> "$REPORT_FILE"
echo "|-------|-----------|------|------|---------|-------|" >> "$REPORT_FILE"
for dir in .claude/skills/*/; do
name=$(basename "$dir")
file="${dir}SKILL.md"
freq=0 # 利用頻度はgit log等から別途集計が必要(ここでは簡易版のため0固定)
deps=25; docs=0; updated=0
# ドキュメント: frontmatter + description
if head -1 "$file" | grep -q "^---"; then
docs=$((docs + 10))
fi
if grep -q "^description:" "$file"; then
docs=$((docs + 15))
fi
# 最終更新日: 90日以内
last_mod=$(git log -1 --format="%at" -- "$dir" 2>/dev/null)
now=$(date +%s)
if [ -n "$last_mod" ]; then
days=$(( (now - last_mod) / 86400 ))
if [ "$days" -le 90 ]; then
updated=25
elif [ "$days" -le 180 ]; then
updated=10
fi
fi
# 外部依存チェック
if grep -q 'mcp__' "$file" 2>/dev/null; then
deps=15 # 外部依存あり = 減点
fi
total=$((freq + deps + docs + updated))
echo "| $name | $freq | $deps | $docs | $updated | $total |" >> "$REPORT_FILE"
done
echo "" >> "$REPORT_FILE"
echo "Generated: $(date)" >> "$REPORT_FILE"
echo "Report saved to: $REPORT_FILE"
3ヶ月分のスコアを並べると、各Skillの健康状態の推移が見えます。スコアが下降傾向のSkillは、ワークフローの変化に追従できていない兆候です。
Agent Skills標準仕様との関連
Agent Skills(agentskills.io)は、Anthropicが2025年12月にオープン標準として公開した仕様です。Claude Code、Cursor、VS Code Copilot、OpenAI Codex、Gemini CLIなど30以上のエージェントが採用しています。
標準仕様に準拠すると品質管理が楽になる理由
標準仕様に従うことで、以下の恩恵が得られます。
1つ目は、agentskills validateによる自動検証です。name規約、description長、frontmatter構造をワンコマンドでチェックできます。独自のバリデーションスクリプトを書く必要がありません。
2つ目は、Progressive Disclosure設計との相性です。標準仕様では、Skillの情報を3段階に分けて読み込むことを推奨しています。
- メタデータ(約100トークン): nameとdescriptionのみ、起動時に全Skill分を読み込み
- 本文(5000トークン以内推奨): Skill発火時にSKILL.md全体を読み込み
- リソース(必要時のみ): scripts/, references/, assets/配下のファイル
この設計に従うと、SKILL.md本体を500行以内に収め、詳細はreferences/に分離する習慣がつきます。結果としてSkillが「太りすぎる」問題を構造的に防げます。テスト対象も明確になります。SKILL.mdの構造テスト + references/の存在チェックという分割が自然にできるためです。
3つ目は、マルチエージェント対応です。標準仕様に準拠したSkillは、Claude Code以外のエージェントでもそのまま動きます。テストが通っていれば、別のエージェントに移行しても品質を担保できます。
チーム運用・スケール時の課題
自分は1人で50個以上のSkillを管理していますが、チームで100個を超える規模になると別の問題が発生します。
1人 vs チームで何が変わるか
| 観点 | 1人 (50個) | チーム (100個超) |
|---|---|---|
| 壊れたことの検知 | 自分で使って気づく | 誰も気づかない可能性 |
| トリガー競合 | 頭の中で把握可能 | 把握不可能、CIが必須 |
| 廃止の判断 | 即断即決できる | 合意形成が必要 |
| description改善 | 自分の言葉遣いに最適化 | チーム全員の表現を考慮 |
オーナーシップ問題
「このSkillは誰が責任を持つのか」は、チーム運用で最初にぶつかる壁です。
対策として、SKILL.mdのmetadataフィールドにownerを記載することを推奨します。
---
name: deploy-staging
description: ステージング環境にデプロイする
metadata:
owner: "@nogataka"
team: "platform"
created: "2026-01-15"
---
CIでownerが空のSkillを検出し、警告を出す仕組みも有効です。
依存関係グラフの管理
Skillが別のSkillを呼び出す依存関係が生まれると、管理が急激に複雑になります。自分の環境では、trend-digestが内部的にtrend-check相当の処理を含んでおり、実質的な依存がありました。
依存が3段以上になったら、Skillの分割か統合を検討する目安にしています。依存関係はSKILL.md内にコメントで明示しています。
<!-- Dependencies: trend-check, write-draft -->
<!-- Depended by: weekly-report -->
運用から得た教訓
「壊れたことに気づけない」の具体的な話
テスト導入前、壊れたSkillに気づくパターンは2つしかありませんでした。
1つ目は自分で使って「あれ?」と感じるパターン。前述のHookのfile_pathの件がこれです。3日間気づかなかったのは、そのSkillをたまたま使わなかったからです。
2つ目はもっと厄介で、「出力の質が劣化しているが、動いてはいる」パターン。MCPサーバーの仕様変更でパラメータの一部が無視されるようになり、取得するデータの範囲が狭くなっていました。動作自体は正常なので、出力を注意深く読まないと気づけません。
テスト導入前後の変化
定量的な比較です。
| 指標 | テスト導入前 | テスト導入後 |
|---|---|---|
| 障害の平均検知時間 | 5日 | 半日以内 |
| 月あたりの障害件数 | 3-4件 | 1件未満 |
| 障害の原因特定時間 | 2-4時間 | 30分以内 |
| Skill廃止の判断速度 | 数ヶ月放置 | 月次レビューで即判断 |
特に効果が大きかったのは「原因特定時間」です。CIのログに依存チェックの結果が残っているため、「どのMCPツール名が変わったか」がすぐにわかります。
「やりすぎた」ポイント
正直に書きます。最初はSkillごとにpromptfooの評価セットを作り、出力品質まで自動テストしようとしました。結果、評価セットのメンテナンスコストがSkill本体の開発コストを超えました。
50個のSkillそれぞれに8件以上のテストケースを書くと、400件以上の評価セットになります。Skillのdescriptionを1行変えるだけで、複数のテストケースを更新する必要がありました。
現在は、トリガー精度テストは主要な5つのSkillに限定しています。残りは構造テストとskill-stocktakeの定期レビューでカバーしています。全Skillにフルテストを書くのは、コストに見合いません。
サードパーティ製Skillとの共存戦略
Part 1で触れた「自作Skillとプラグイン経由のSkillが混在する問題」への対処です。superpowersやeverything-claude-code等のプラグインを入れると、数十個のSkillが一気に追加されます。自作とプラグインを合わせて50個以上が共存する環境では、以下の3つの対策が有効でした。
1. 命名規約でオーナーシップを明示する
自作Skillにプレフィックスをつけて、一目で区別できるようにします。
| 種別 | プレフィックス | 例 |
|---|---|---|
| 自作(個人用) | my- |
my-draft-article, my-fetch-trends
|
| 自作(チーム共有) | team- |
team-deploy-check |
| サードパーティ | つけない(そのまま) |
brainstorm, tdd
|
後付けでリネームするのは影響範囲が大きいため、新規作成時からこのルールを適用します。既存の自作Skillは、棚卸しのタイミングで段階的にリネームしました。
2. CIでオーナーシップを自動分類する
品質スコアのスクリプトにオーナーシップ判定を組み込みます。
#!/bin/bash
# scripts/classify-skills.sh
# Skill のオーナーシップを自動分類
echo "## Skill Ownership Report"
echo "| Skill | Owner | Source |"
echo "|-------|-------|--------|"
for dir in .claude/skills/*/; do
name=$(basename "$dir")
if [[ "$name" == my-* ]]; then
owner="self"
elif [[ "$name" == team-* ]]; then
owner="team"
else
owner="third-party"
fi
# git log でコミット元を確認
author=$(git log -1 --format="%an" -- "$dir" 2>/dev/null || echo "unknown")
echo "| $name | $owner | $author |"
done
3. サードパーティSkillとの競合を検出する
自作Skillとサードパーティ製Skillでdescriptionが類似している場合、トリガーの競合が起きます。静的解析のレイヤーに競合検出を追加しました。
#!/bin/bash
# scripts/detect-overlap.sh
# 自作Skillとサードパーティの用途重複を検出
for my_dir in .claude/skills/my-*/; do
my_name=$(basename "$my_dir")
my_desc=$(grep -m1 "^description:" "${my_dir}SKILL.md" 2>/dev/null | sed 's/description: //')
for other_dir in .claude/skills/*/; do
other_name=$(basename "$other_dir")
[[ "$other_name" == my-* ]] && continue # 自作同士はスキップ
other_desc=$(grep -m1 "^description:" "${other_dir}SKILL.md" 2>/dev/null | sed 's/description: //')
# 共通キーワードが3つ以上あれば競合の可能性
common=$(comm -12 \
<(echo "$my_desc" | tr ' ' '\n' | sort -u) \
<(echo "$other_desc" | tr ' ' '\n' | sort -u) | wc -l)
if [ "$common" -ge 3 ]; then
echo "OVERLAP: $my_name <-> $other_name (common words: $common)"
fi
done
done
この仕組みで、プラグインを新しくインストールした際に「既存の自作Skillと機能が被っていないか」を自動チェックできます。被っている場合は、自作側を廃止するか、サードパーティ側を無効化するかを判断します。
まとめ
本記事で紹介した設計を整理します。
- Skillが壊れる4パターンを具体的な障害事例とともに把握しました
- 「決定論的に検証可能な部分」と「人間のレビューが必要な部分」を明確に線引きしました
- 静的解析 / 副作用テスト / トリガー精度テストの3層でテスト戦略を設計しました
- GitHub ActionsでSkill変更時の自動チェックと週次ヘルスチェックを構築しました
- Agent Skills標準仕様(agentskills.io)の
agentskills validateで仕様準拠を自動検証しました - 品質スコアで定期的な棚卸しを仕組み化し、廃止判断を迅速化しました
Agent Skillsは、作って終わりではありません。コードと同じように、テストし、CIで守り、定期的に棚卸しします。この運用を回し始めてから、「久しぶりに使ったSkillが動かない」という事態がほぼなくなりました。
一方で、テストを書きすぎてメンテナンスコストが爆発した経験もあります。「構造テストは全Skillに、精度テストは主要Skillに限定」というバランスが、現時点での落としどころです。
Skillの数が10個を超えたあたりから、管理の仕組みがないと辛くなります。本記事の設計がその一助になれば幸いです。
前回記事: Skillsを50個運用して気づいた — 増やすほど生産性が下がるパラドックス
参考:
- Agent Skills Specification - Agent Skills標準仕様
- agentskills (PyPI) - 標準仕様のバリデーションツール
- Extend Claude with skills - Claude Code Docs - Claude Code公式ドキュメント
- everything-claude-code (GitHub) - skill-stocktake等のSkill管理ツール
- How to Write, Eval, and Iterate on a Skill - Skill評価ループの解説
- Claude Code Changelog - Hook仕様変更等の履歴