目次
- なぜ作ったのか:静かな失敗
- 38項目のrubric
- 核心の設計原則:ルール vs モデル
- アーキテクチャ:sub-agentの分離
- 検査の深さの調整
- gradeシステム
- 面白かった問題1:SF2の2段階確認
- 面白かった問題2:reportの自己完結型sub-agent
- セルフ評価:自分自身を採点したら?
- おわりに
なぜ作ったのか:静かな失敗
Claude Codeのskillは、YAML frontmatterが付いたMarkdown指示ファイルです。構造が単純なので作るのは簡単ですが、問題は 間違って作ってもエラーが一切出ない という点です。
- descriptionが抜けていたら? Claudeは単にそのskillを呼び出しません。
- トリガー条件が曖昧なら? 見当違いの状況で呼び出されたり、必要な瞬間に呼び出されなかったりします。
- frontmatterのYAMLが壊れていたら? skill自体がロードされません。
すべて 静かな失敗(silent failure) です。skillをマーケットプレイスに公開する前にこうした問題を体系的に検出する方法がなかったので、自分で作ることにしました。そうして生まれたのが /skill-quality pluginです。ソースコードはGitHubで公開しています。
38項目のrubric
/skill-qualityはrubricベースでskillを評価します。
rubricとは、評価対象を複数の基準に分け、基準ごとにgradeや合否を付ける評価表です。もともと教育現場で課題や発表を採点する際によく使われる方式ですが、「このコードは良いか?」のように主観に流れがちな判断を基準ごとに分解して客観化できるという点で、ソフトウェアの品質評価にもそのまま適用できます。
「良いskillとは何か」をチェック可能な項目に分解してみました。最終的に7つの次元、38項目になりました。各項目は 深刻度(BLOCKER · MAJOR · MINOR)と 検査方式(rule · model)で分類しています。
V — 妥当性(Validity)· 2項目
skillが存在する理由があるかを検証します。
| ID | 深刻度 | 方式 | 内容 |
|---|---|---|---|
| V1 | MAJOR | model | 一般的なClaudeプロンプトでは得られない組織固有の知識や手順が本文に1行以上あるか |
| V2 | MAJOR | model | 既存の組み込みskill(/code-review、/security-reviewなど)と大きく重複していないか |
V1が核心です。「このskillなしでClaudeにそのまま聞いても同じ結果が得られる」のであれば、skillとして作る理由がありません。チーム固有のルール、内部ツール名、社内の慣例などが最低1行以上必要です。
ST — 構造(Structure)· 8項目
frontmatterとファイル自体の構造的な有効性を検査します。BLOCKERの3件はすべてこのセクションにあります。
| ID | 深刻度 | 方式 | 内容 |
|---|---|---|---|
| ST1 | BLOCKER | rule | frontmatter(---ブロック)が有効なYAMLとしてパースできるか |
| ST2 | BLOCKER | rule | description: キーが存在し、値が空でないか |
| ST3 | MAJOR | rule | descriptionの全体の長さが1,536文字以下か |
| ST4 | MAJOR | rule | name の値が ^[a-z][a-z0-9-]*$ 形式か(小文字・数字・ハイフンのみ) |
| ST5 | BLOCKER | rule | ファイルがUTF-8としてエラーなく読み込めるか |
| ST6 | MINOR | rule | argument-hint がある場合、<、[、( のいずれかを1つ以上含むか |
| ST7 | MAJOR | rule | frontmatterに重複キーがないか |
| ST8 | MAJOR | rule | 本文(frontmatter以降)の行数が500行以下か |
ST2にはショートサーキットルールがあります。descriptionがまったく存在しない場合はST2だけをBLOCKERとして記録し、残りの37項目はすべてSKIP扱いにします。frontmatter自体がないファイルで意味のない連鎖的な失敗が大量に発生するのを防ぐためです。
F — frontmatterの意味論(Frontmatter Semantics)· 5項目
frontmatterの各フィールドの値が意味的に正しいかを検査します。
| ID | 深刻度 | 方式 | 内容 |
|---|---|---|---|
| F1 | BLOCKER | rule | name: キーが存在するか(値は問わない。なければskillを名前で呼び出せない) |
| F2 | MAJOR | rule | disable-model-invocation: true 設定時、descriptionにトリガー文言(“Use when”、“call this when” など)がないか |
| F3 | MAJOR | model | context: fork 設定時、本文に具体的な作業指示が1つ以上あるか |
| F4 | MAJOR | rule | effort: の値が low、medium、high、xhigh、max のいずれかか |
| F5 | MINOR | rule | tools: の値がYAMLシーケンスまたはカンマ区切り文字列の形式か |
F2は矛盾チェックです。「モデルからの呼び出しを無効にする」と設定しておきながら、descriptionに「こういうときに呼び出してください」と書いてあると、Claudeはskillを呼び出すべきか否かを判断できません。
T — トリガー(Trigger)· 6項目
Claudeがskillをいつ、どのように呼び出すかを判断するのに必要な情報が十分にあるかを検査します。
| ID | 深刻度 | 方式 | 内容 |
|---|---|---|---|
| T1 | MAJOR | model | descriptionにこのskillが 何を するのか(WHAT)が明確に記述されているか |
| T2 | MAJOR | model | descriptionに いつ 呼び出すのか(WHEN)が明確に記述されているか |
| T3 | MAJOR | rule | descriptionが1人称(“I ”、“We “)や2人称(“You “)で始まっていないか |
| T4 | MAJOR | model | トリガー条件が他のskillと十分に区別できるか |
| T5 | MINOR | model | WHATとWHENがdescriptionの最初の200文字以内に両方含まれているか |
| T6 | MINOR | rule | descriptionが50文字以上か |
T1・T2・T4がトリガー品質の核心です。「助けが必要なときに使ってください」はT2を形式的に通過してもT4で失敗します。他のskillと区別できないからです。T3は記述上の慣例です — descriptionは3人称の動詞で始めるべきです(“Generates…”、“Evaluates…” の形)。
C — コンテンツ(Content)· 6項目
本文が実際に有用な内容を含んでいるかを検査します。
| ID | 深刻度 | 方式 | 内容 |
|---|---|---|---|
| C1 | MAJOR | model | 公開ドキュメントでは得られない組織固有の事実・ルール・手順が1つ以上あるか |
| C2 | MAJOR | model | 入力→出力、変更前→変更後、コマンド→結果の形式の実際の例が1つ以上あるか |
| C3 | MINOR | rule | ”as of”、“currently”、「現在」、年号(2024など)のような期限付きの表現がないか |
| C4 | MINOR | model | 同じ概念を指す用語が本文全体で一貫して使われているか |
| C5 | MINOR | model | 作業のリスクに比例した指示の強度を備えているか(ファイル削除・デプロイなど高リスクの作業ほど厳格に) |
| C6 | MINOR | model | 失敗モード、制約事項、「してはいけないこと」の条項が1つ以上明示されているか |
C1とC2はコンテンツセクションの2つのMAJORです。C1はV1とつながっているため、組織固有の知識がなければV1とC1が一緒に失敗するケースが多いです。C3は将来的に誤りになってしまう内容を防ぎます。「2024年時点でサポートしています」という文は、1年後の読者を誤解させます。
R — リソース(Resources)· 8項目
外部ファイル参照とコードブロックの健全性を検査します。
| ID | 深刻度 | 方式 | 内容 |
|---|---|---|---|
| R1 | MINOR | rule | 本文が200行超なのに references/ ディレクトリがない場合は分離を推奨 |
| R2 | MINOR | rule | 30行超のbash/pythonブロックがあるのに scripts/ ディレクトリがない場合は分離を推奨 |
| R3 | MAJOR | rule | 本文に絶対パス(/Users/、/home/、C:\ など)がないか |
| R4 | MAJOR | rule | 本文の <<SKILL_DIR>>/path 参照が実在するファイルを指しているか |
| R5 | MAJOR | rule | 本文のbash/pythonコードブロックが文法エラーなくパースできるか |
| R6 | MINOR | model | 本文のシェルコマンドのシーケンスが終了時の挙動やエラー処理(|| exit 1、set -e など)を明示しているか |
| R7 | MINOR | rule | SKILL.md全体の行数(frontmatter含む)が200行を超える場合は分離を推奨 |
| R8 | MINOR | rule | references/ ディレクトリがある場合、内部のファイルが1階層を超えてネストしていないか |
R3は移植性の問題です。絶対パスは特定のマシンでしか動作しません。R4は存在しないファイルを指すデッドリンクを検出します。R5は本文に含まれる例示コードが実際に文法的に正しいかを確認します。
SF — 安全性(Safety)· 2項目
公開してはいけないセキュリティ・安全性の問題を検査します。
| ID | 深刻度 | 方式 | 内容 |
|---|---|---|---|
| SF1 | BLOCKER | rule | 本文に実際のシークレット(APIキー、パスワードなど)がないか |
| SF2 | MAJOR | rule+model | 本文に保護措置のない破壊的コマンドがないか(rm -rf、DROP TABLE、git push --force など) |
SF1はBLOCKERです。APIキーが含まれたskillをマーケットプレイスに公開すると、即座に流出します。<your-api-key> のようなプレースホルダーや example、dummy のような明らかな例示値は除外されます。SF2がrule+modelの2段階である理由は、次のセクションで説明します。
X1 — skill間の衝突(Cross-skill Collision)· 1項目
| ID | 深刻度 | 方式 | 内容 |
|---|---|---|---|
| X1 | MAJOR | model | 同じディレクトリ内の2つのskillのdescriptionが類似しすぎていて、Claudeがどちらを呼び出すべきか区別できないか |
/skill-quality report コマンドでのみ実行されます。複数のskillを一度に点検する際に、衝突しているペアを見つけ出します。
核心の設計原則:ルール vs モデル
38項目を定義してみると、自然とひとつの原則が導き出されました。
決定的な問題はルールで、意味の問題はモデルで。
「YAMLがパースできるか」「nameが ^[a-z][a-z0-9-]*$ にマッチするか」「シークレットのパターンがあるか」のような検査は正解がひとつです。こうした検査をLLMにやらせるのは無駄であり、何より 非決定的 になります。そこで決定的な検査は、Haikuモデルが実行する rule_checks atomに集約しました。Haikuはスクリプト実行係の役割だけを担うため、高速で安価です。
一方、「このdescriptionはClaudeがトリガーするのに十分具体的か」「本文に公開ドキュメントでは得られない組織固有の知識があるか」のような問いは、ルールでは表現できません。こうした意味論的な判断は、Sonnet/Opusが実行する model_checks atomが担当します。
この分離のおかげで、38項目それぞれに type: rule または type: model が明示され、どの判断が再現可能で、どの判断がモデル依存なのかが明確になりました。
アーキテクチャ:sub-agentの分離
ファイル構造は次のとおりです。
skill-quality-plugin/
├── .claude-plugin/
│ └── plugin.json
├── skills/
│ └── skill-quality/
│ ├── SKILL.md # 라우터 + 공통 정의
│ └── commands/
│ ├── check.md # 단일 스킬 평가 (오케스트레이터)
│ ├── report.md # 디렉토리 일괄 평가
│ ├── rubric.md # 38개 항목 표시
│ ├── help.md
│ └── atoms/
│ ├── rubric-spec.md # 항목 정의 (단일 소스)
│ ├── rule_checks.md # 결정적 검사 (Haiku)
│ └── model_checks.md # 의미 검사 (Sonnet/Opus)
└── fixtures/
├── example-s-grade/
├── example-b-grade/
└── example-f-grade/
最も気を配ったのは、メインセッションのコンテキストを汚染しないこと です。38項目を検査すると中間成果物がかなり多く生成されますが、これがすべてメインコンテキストに積み上がると、その後の作業品質が低下します。
そこで、checkコマンドはオーケストレーターとしてのみ動作します。rule_checks → model_checksの順にsub-agentを起動し、各atomは詳細な結果を /tmp/skill-quality-rule.json、/tmp/skill-quality-model.json に記録した後、最後の行に1行だけの要約を出力します。
>>> RESULT <<<
OK FAIL: BLOCKER st1,sf1; MAJOR st4; MINOR st6
メインセッションは、このターミナル行とJSONファイルだけを読みます。評価対象であるSKILL.md自体をメインセッションが読むことはありません。
検査の深さの調整
あらゆる状況でOpusまで回す必要はありません。状況に応じて検査の深さを選べるよう、3段階のオプションを提供しています。
| フラグ | rule checks | model checks | 用途 |
|---|---|---|---|
--rules-only / --depth=shallow | ✓ | 省略 | pre-commitフック(高速) |
| (デフォルト) | ✓ | Sonnet | 日常的な点検 |
--depth=deep | ✓ | Opus | 公開前の最終ゲート |
コミット前にはshallowで構造上の問題だけを素早く検出し、マーケットプレイスへの公開直前にだけdeepで意味論的な品質まで点検する、といった使い方です。
/skill-quality check ./my-skill --rules-only # 빠른 검사
/skill-quality check ./my-skill --depth=deep # 최종 게이트
/skill-quality report ./skills # 디렉토리 일괄 평가
gradeシステム
結果は、BLOCKER/MAJORの数でgradeが決まります。MINORはgradeに影響せず、「Suggestions」としてのみ表示されます。
BLOCKER ≥ 1 → F
MAJOR 0 → S
MAJOR 1–2 → A
MAJOR 3–5 → B
MAJOR 6–9 → C
MAJOR 10+ → D
MINORをgradeから外したのは意図的です。些細な提案のせいでgradeが下がると、人々がrubric自体を信頼しなくなるからです。
面白かった問題1:破壊的コマンド検出の文脈問題
安全性の項目のひとつに、「本文に保護措置のない破壊的コマンドがないか」を検査する項目があります。rm -rf、DROP TABLE、git push --force のようなパターンを正規表現で捕まえること自体は簡単です。問題は 文脈 です。
절대 `rm -rf /`를 실행하지 마세요. ← 경고 문서 (PASS여야 함)
정리 단계: rm -rf ./build 를 실행 ← 실제 실행 지시 (FAIL이어야 함)
正規表現だけでは、この2つを区別できません。そこで、この項目だけ rule+model の2段階確認として設計しました。1段階目でruleがパターンにマッチした行番号を記録しておき、2段階目でモデルが 該当行だけ を取り出して読み、警告・ドキュメントの文脈なのか、実際の実行指示なのかを判定します。モデルの判定が、ルールのフラグを最終的に置き換えます。
正規表現の再現性とモデルの文脈理解を組み合わせた、このpluginでいちばん気に入っている部分です。
面白かった問題2:reportのsub-agentネスト制約
reportコマンドは、ディレクトリ内のすべてのskillを一括評価します。最初は「skillごとにcheck.mdをsub-agentとして実行すればいいだろう」と考えていたのですが、ここで行き詰まりました。check.mdは自分自身がsub-agentを起動する構造なのに、sub-agentがさらにsub-agentを起動することはランタイムがブロック するのです。
解決策は、claude --print(非対話型のCLI呼び出し)を活用することでした。skillごとに1つのsub-agent(並列最大4つ)を起動しつつ、各sub-agentはsub-agentをスポーンする代わりに、Bashで claude --print "..." コマンドを実行します。このコマンドで開始されたプロセスは 完全に新しい独立セッション なので、自前でrule_checksとmodel_checksをsub-agentとして起動できます。ネスト制約を回避するのではなく、ランタイム外部の独立プロセスを通じて同じ品質を得る方式です。
report (메인 세션)
└─ 서브에이전트 × 4 (병렬, Task 하니스)
└─ claude --print "..." (독립 프로세스, 새 세션)
├─ rule_checks 서브에이전트 (haiku)
└─ model_checks 서브에이전트 (sonnet/opus)
結果として、reportもcheckと同じ38項目全体を評価します。Claude Codeのplugin環境では claude CLIが標準でインストールされているため、この方式が自然に適用できます。
セルフ評価:自分自身を採点したら?
品質評価ツールであれば、当然まず自分自身が合格しなければなりません。pluginのSKILL.mdに対して、--depth=deep でセルフ評価を回しました。
/skill-quality check: .../skill-quality/SKILL.md
══════════════════════════════════════════════════
Grade: A (rubric v1.0, depth=deep)
MAJOR (1)
[C2] No worked example — body shows output format patterns
but no end-to-end command → result scenario
Fix: Add a ## Example section with sample invocation and output
══════════════════════════════════════════════════
BLOCKER: 0 MAJOR: 1 MINOR: 0 SKIP: 9
最初の結果は Grade A でした。--depth=deep のopusモデルがC2(worked exampleなし)を検出したのです。--rules-only やデフォルトのdepthでは通過していた項目です。
SKILL.mdに /skill-quality check ./... コマンドとその出力を示す ### Example セクションを追加して、再評価しました。
/skill-quality check: .../skill-quality/SKILL.md
══════════════════════════════════════════════════
Grade: S (rubric v1.0, depth=deep)
══════════════════════════════════════════════════
BLOCKER: 0 MAJOR: 0 MINOR: 0 SKIP: 9
--depth=deep に意味がある理由が、まさにこれです。ルール検査だけでは「例が十分に具体的か」を判断できず、モデルが実際に読んでみて初めて分かります。ツールを作る過程そのものが、良いskillを書く練習になったわけです。
おわりに
今回のpluginを作りながら学んだことをまとめると、次のとおりです。
- 品質基準は分解してこそ実行可能になる。 「良いskill」という漠然とした概念を38個のpass/fail項目に分解して、ようやく自動化できました。
- 決定的な問題はルールで、意味の問題はモデルで。 LLMに正規表現マッチングをやらせず、正規表現に文脈判断をやらせないでください。それぞれ得意なことが違います。
- コンテキストの分離は設計の初期段階から。 sub-agentが一時ファイルに書き込み、1行の要約だけを返すパターンは、長いパイプラインでメインセッションをクリーンに保つ最も確実な方法でした。
- 自分自身にツールを適用してみる。 セルフ評価で出た指摘が、rubricの穴を最も多く明らかにしてくれました。
skillを作って公開する計画があるなら、公開前に一度こうしたrubricで点検してみることをおすすめします。静かな失敗は、ユーザーに発見される前に捕まえるのがいちばん安上がりです。
pluginのソースコードと38項目のrubric仕様の全体は、GitHubで確認できます。
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Dekuが開発したアプリを使ってみてください。Dekuが開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。