← ブログに戻る

107本のSKILL.mdをlintしたら、稼働中のskillが21本「壊れてる」と言われた

自作のskill評価lintを初めて実行した日、107本のSKILL.mdに対して違反67件という結果が出ました。全件を手で確認したら、49件が誤検知でした。中でも痛かったのは、毎日普通に稼働しているskillが21本、「frontmatterが壊れている」と判定されていたことです。誤検知率73%の検査ツールの誕生です。

この記事は、Claude Codeのskill評価の仕組みを作ったときの記録です。ただし主役は実装ではありません。専用リポジトリを作る構想を調査の末に捨てた判断と、この誤検知を校正して分かったことの2つです。

作ったのは新リポジトリではなく、collector 1本

最初の構想は「skillとharnessを評価する専用リポジトリ」でした。着手前に先行事例を半日調べたところ、skill評価と呼ばれているものは実は3つの別の問いに分かれていました。

1つ目は「skillは壊れていないか」。pulser のような静的lintの領域で、SKILL.mdの構文や参照切れを実行なしで検査します。2つ目は「skillは効いているか」という実行evalの領域です。Anthropic公式の skill-creator には評価パイプラインが内蔵されています。evals.jsonに貯めたテストプロンプトを、grader/comparator/analyzerサブエージェントが採点する作りです。StackHawkのブラインドA/Bや、OpenSkillEvalの系統評価もここに入ります。3つ目は「harness自体はどうか」。skillでなく、Claude CodeやCodex CLIといったharness側を差し替えて測る領域で、terminal-benchが代表です。

私が欲しかったものの半分は、既に世の中にありました。残り半分は、運用中の週次コード健全性ハーネスにcollectorを1本足せば済むと分かりました。新リポジトリは作りませんでした。

ただし全部をlintに寄せたわけではありません。実行evalは捨てたのではなく、席を分けました。理由は3つあります。

コストが違う。 決定的なlintは無料で数秒なので日次cronに乗ります。実行evalは1回ごとに課金と数分の待ちが発生し、「毎日全skillに回す」が成立しません。

採点の型が違う。 LLM-as-judgeは非決定です。同じskillに毎週同じ点が出ないと、時系列の変動がskillの悪化なのか採点者のブレなのか区別できません。

測るものが違う。 lintが見るのはファイルとしての健全性、実行evalが見るのはエージェントの挙動です。健康診断の紙に営業成績を書き込むと、両方とも読めなくなります。

静的lint層は既存ハーネスの1観点として即日実装し、実行eval層は将来の別基盤に分離して、やるとしてもサマリーの成績だけを書き戻す。これが二層方針です。

6チェックと107本の実測

実装したチェックは6種類です。

チェック内容
fm_missingfrontmatterが読めない
name_mismatchnameとディレクトリ名の不一致
desc_overdescriptionが1024字超
oversize本文500行超
broken_refs相対リンク切れ
broken_symlinksymlink切れ

6本目は私の運用固有です。skill本体は定義元リポジトリに1つだけ置き、使う側へはsymlinkで配っています。symlinkが切れてもエラーは出ず、skillが一覧から静かに消えるだけです。呼ばれないskillは失敗ログすら残さないので、この故障は静的lintでしか捕まりません。

校正後の実測は、8リポジトリ・107本で違反18件でした。内訳はfrontmatter欠落2件と500行超16件です。スコア最下位は57本のskillを抱える最古参リポジトリのC評価で、残りはBからAに散りました。一番古くて大きい場所が一番低いという、直感どおりの分布です。ものさしを信用できるのは、答えを知っている場所で直感と一致したときだけなので、この「面白くない結果」がいちばんの安心材料でした。

誤検知49件の校正が本題だった

冒頭の67件に戻ります。誤検知49件は、きれいに2クラスに分かれました。

クラス1: 厳密YAMLパースが稼働中の21本を壊れ判定。 skillのfrontmatterには次のような行がよく出てきます。

---
name: research
argument-hint: [検索クエリ] or [--full <id>]
---

厳密なYAMLパーサはこの argument-hint の値をflow sequenceとして解釈し、ドキュメントごとパースに失敗します。一方でClaude Code本体は、この書き方を問題なく読み込んで毎日動かしています。つまりlintの正解仕様として採るべきは「YAML仕様」ではなく「ランタイムの寛容さ」でした。厳密パーサを捨て、ランタイムと同程度に寛容な行ベースのパーサへ置き換えました。

教訓を一般化すると、lintの正は仕様書でなくランタイムの実装に置くべきです。仕様書を正にすると、仕様より寛容なランタイムの上で元気に動いている現物を壊れ扱いすることになります。

クラス2: プレースホルダをリンク切れ判定。 [search](url) のような説明用の飾りをbroken_refsに数えていました。リンク先に ./ を含むものだけ検査する条件で除外しました。

両クラスとも回帰テストに固定し、残った18件は全件実物を開いて確認して誤検知ゼロでした。

この体験から、他所のskill監査の数字の読み方も1つ変わりました。pulserの作者は214本を監査して73%が壊れていたと報告しています。私の初回実行も63%が違反でしたが、校正後は17%まで落ちました。linterの正解仕様がランタイムより厳しいと、監査の数字はその差の分だけ膨らみます。その監査がそうだと言いたいわけではありません。ただ、大きな割合の見出しを見たら「壊れているのはskillか、ものさしか」をまず疑う価値はあります。私の場合は21本分、ものさしのほうでした。

問いで選ぶ

skill評価の手法選びは、結局この対応表に落ちました。

  • 毎日の回帰チェック: 静的lint。無料・数秒・決定的。ただし「効くか」は見えない
  • skill改修のbefore/after: ブラインドA/B型の実行eval。リリース時だけの課金イベントとして扱う
  • 導入するskillの選定: skillあり・なし比較。「入れれば効く」を前提にしない
  • harnessの乗り換え判断: タスクを固定してharness側を差し替える

lintで済む問いをLLMに聞くのは、体温計で足りる診察をMRIから始めるようなものです。逆に「効いているか」をlintに聞いても、一生答えは返ってきません。仕組みの賢さより、問いとコストの対応を先に仕分けたことが、今回いちばん効きました。