この記事は、連載「読まない技術」の第 4 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。

今回は、スキルの話です。スキルというのは、Claude Code で、AIにカスタムコマンドやツールを登録しておくためのものです。AIに使ってほしくて登録したスキルには、並べるほど選ばれなくなるという問題があります。登録するスキルの数が増えるほど、AIは、使ってほしい場面でも、そのスキルを選ばなくなります。

まず一つだけ、数えてみてください。

AIに登録してあるスキル(カスタムコマンド、ツール)はいくつありますか。そのうち、直近1週間でAIが自分から選んで使ったものはいくつありますか。

登録した数より使われた数がずっと少ないなら、原因は、スキルの書き方より、スキルそのもののほうにあります。スキルでは、あなたが、「こういうときに使う」という条件文を付けて、ツールを登録します。使うかどうかを判断するのは、AIです。スキルを登録するコストは二つあります。一つは、登録しただけで、AIが読む量が増えることです。もう一つは、そうして読ませたスキルが、AIに選ばれないことです。

条件文が並ぶほど、一番使ってほしいスキルも、選ばれにくくなります。序論では、ルールは「足すほど薄まる」と書きました。同じことが、スキルでも起きています。

私の手元の本番プロジェクト——YouTube の音楽動画でタイピング練習ができる Web サービス typingtube——には、AIに実行させるスクリプトが84本あります(十数言語ぶんの翻訳の検証、デザインのラチェット、ベンチマーク、キャラクター画像の生成……画面に出ている機能の数だけ、裏で動くスクリプトがあります)。スキルとして登録しているものは、1個です。AIが毎セッション読まずに済んでいるのは、84本ぶんの使い方です。

スキルを登録したのに、使ってほしい場面で、AIがそのスキルを選ばない。このとき、あなたなら次のどちらを選びますか。

  • 選ばれるように、スキルの条件文を書き直す
  • スキルには登録せず、使い方を間違えたコマンドを、hookが入口で止めて、読むべきドキュメントを案内する

答えは、入口で止めるほうです。AIがスキルを選ばないのは、条件文の書き方より、並んでいる数のためです。スキルの一覧には、コンテキストに対する予算があり、超えたぶんは一覧から外されます。予算に収まっていても、並ぶ数が増えるほど、AIは正しいものを選びにくくなります。長いセッションでは、圧縮のあと、一覧そのものが戻りません。条件文をどう書き直しても、並んでいる数は変わらず、圧縮のあとに一覧が戻らないことも変わりません。入口で止める形では、AIは一覧から何も選びません。使い方を間違えたコマンドは実行される前に止まり、止めた理由と読むべきドキュメントが届くのは、AIが次の操作を決める前です。

理解したことは一文にできます。

スキルは、条件文を並べるほど選ばれなくなる。だから、AIに選ばせる代わりに、使う前にドキュメントを読ませる。

CC BY 4.0

なぜ、並べるほど選ばれなくなるのか

スキルの条件文は、セッションが始まった時点で、全部がコンテキストに載っています。載るのは、スキルの名前と、条件文を書いた description だけです。スキルの本文がコンテキストへ足されるのは、そのスキルが呼ばれたときです。Claude Code の公式ドキュメントのスキルのページによると、Claude Code は description を when_to_use と合わせて1,536文字で切り、スキルの一覧の全体には、コンテキストの1%という予算を置いています。予算を超えたぶんは、一覧から外されます。スキルを1つ登録することは、この予算を分け合うスキルを1つ増やすことです。

では、予算に収まるように条件文を短く書けばいいのかというと、Claude の公式ドキュメントのうち、ツール定義のページは、説明を極めて詳細に書け、それが群を抜いて最も重要だと書いています。書くのは、何をするか、いつ使うか、いつ使わないか、パラメータの一つずつで、最低でも3〜4文です。そして同じページは、関連する操作を少ないツールにまとめよ、とも書いています。スキルを数多く登録する設計は、この指針に反します。

予算に収まっていても、選び方を悪くするのは、並ぶ数そのものです。同じ公式ドキュメントのうち、ツール検索のページは、使うことのできるツールが30〜50を超えると、Claude が正しいツールを選ぶ能力が落ちると書いています。スキルも、AIが一覧から選んで呼び出すものです。登録するスキルが増えるほど、AIが選ぶ一覧は長くなります。

そのうえ、「こういうときに使う」と書いた時点で、いまの場面がその条件に当てはまるかどうかの判断を任されるのは、条件文を読むAIです。

コンテキストに載った量が上限に近づくと、Claude Code はそれまでの履歴を圧縮します。このとき、指示ファイルとメモリの索引は、ディスクから読み直されます。ところがスキルの一覧は再注入されません。長く続けたセッションの後半ほど、AIは、登録したスキルを選ぶことができなくなります。

仕組み: 選ばせる代わりに、入口で読ませる

スクリプトは、スキルに登録せずに素のまま置き、使い方はドキュメントに書きます。ここまでは、よくあるやり方です。そのうえで置くのは、hookを1つだけです。このhookは、AIがシェルのコマンドを実行しようとした入口で、使い方が間違っているときだけコマンドを止め、読むべきドキュメントを1枚案内します。第1回で、素のテストコマンドを止めてラッパーへ案内したhookと同じ形です。

使う前に準備が要るコマンドには、この形を一歩進めて使います。例に挙げるのは、gitのpre-commit hookです。pre-commit hookは、コミットのたびにgitが実行し、秘密情報の混入や規約違反があればコミットを止めます。使う前に、gitのhookとして設置しておく必要があります。設置の手順を踏んだときにできるのが、.git/hooks/pre-commit というファイルです。Claude Code の側に置くhookは、次のとおりです。

#!/bin/bash
# .claude/hooks/require_setup.sh — 手順を踏んだ形跡(生成物)が無ければ止める(手元のコードを短くしたもの)
command=$(cat | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))')
case "$command" in *--no-verify*|*SKIP_HOOK_CHECK=*) exit 0 ;; esac   # 明示の逃げ道は通す
printf '%s' "$command" | grep -qE '(^|[;&|[:space:]])git[[:space:]]+commit([[:space:]]|$)' || exit 0
hooks_dir=$(git rev-parse --git-path hooks 2>/dev/null || echo .git/hooks)   # worktree でも正しい場所を見る
if [ ! -x "$hooks_dir/pre-commit" ]; then
  echo "Pre-commit checks are not installed. Run the setup in docs/setup.md (bash scripts/setup_hooks.sh) first" >&2
  exit 2   # 実行させず、stderr の案内を AI に読ませる
fi
exit 0

AIがドキュメントを読みに行ったかは、hookから測ることができます。hookは入力で、会話の記録のファイルの場所を受け取るので、AIが読むツールを呼んだかは、その記録に残っています。測ることができないのは、読んだ中身が、そのあとのAIの操作にどれだけ効いたかです。そこでこのhookは、読んだかどうかの代わりに、手順を踏んだときにだけ作られるファイルがあるかを確かめています。ファイルがあれば、誰がどう設置したにせよ、手順は踏まれています。

ファイルが無くてもコマンドを通すのは、コマンドの中で明示したときだけです。上のコードは、コマンドに SKIP_HOOK_CHECK= が含まれるときと、gitが元から持っている、pre-commit hookを飛ばすオプションの --no-verify が含まれるときに、コマンドを止めずに通します。

hookがコマンドを終了コード2で止めると、Claude Code はそのコマンドを実行せず、hookが標準エラーに書いた文を、止めた理由としてAIに渡します。読むべきドキュメントの場所は、AIが次の操作を決める前に届きます。スキルでは、「いつ使うか」をAIに事前に選ばせていました。この形では、AIは何も選びません。案内が届くのは、AIがコマンドを使おうとして、手順を踏んでいなかったときだけです。手順を踏むまで、コミットは通りません。

この形にする前は、私がクローンした作業ディレクトリで、pre-commit hookが未設置のままでした。その作業ディレクトリでは39コミットが、別の環境では2ヶ月ぶんのコミットが、gitのhookを1つも通らずに積み重なっていました。怖いのは、どのコミットも止められなかったことです。止められずに通ったコミットには、誰も気づきません。

CC BY 4.0 はここまで

読まなくなったもの

スキルの条件文です。条件文を書くことも、AIに毎セッション読ませることも、選ばれるように条件文を直し続けることも、私はやめました。84本のスクリプトの使い方は、ドキュメントに1枚ずつ書いてあります。AIがそのドキュメントを読むのは、hookなどの仕組みに案内されて、読む必要が出たときだけです。

CC BY 4.0

注意: hookを置くのは、間違えると高くつく入口だけ

私の環境でhookを置いているのは、テストの実行、コミット、サブエージェントの起動の数か所です。残りの入口は、間違えてもすぐに分かり、損も小さいので、hookを置かずに素のままにしています。すべての入口にhookを置くと、私は、スキルの条件文の一式を、hookで作り直すことになります。

検証手順: ファイルを退避して、止まることを確かめる

前提

この回のhookを1つ登録し終えておきます。止める対象のコマンドと、hookが存在を確かめるファイルの組も、決めておきます(本文の例では、AIが git commit を実行しようとしたときに、hookが .git/hooks/pre-commit があるかどうかを確かめます)。

所要時間: 5分

手順

  1. あなたが、hookが存在を確かめているファイルを、別の場所へ退避します(mv .git/hooks/pre-commit /tmp/ のように)。退避したファイルは、後始末で元の場所へ戻します
  2. あなたが、AIに、止める対象のコマンドを実行させます(本文の例なら「コミットして」と頼みます)

合格条件(すべて満たすこと)

コマンドが実行される前に止まり、hookの出力に、読むべきドキュメントの場所が出ていれば合格です。

合格しなかったとき

  • 止まらずに通ってしまった —— hookが登録されていないか、hookが見ているパスが、手順1で退避したファイルのパスと違います
  • 止まったが、案内が出ない —— hookが、案内を標準エラーに書いていません。終了コード2で止めたときに、止めた理由としてAIに渡るのは、標準エラーに書いた文です

後始末

退避したファイルを元の場所へ戻し、同じコマンドをもう一度AIに実行させます。今度は止まらずに通れば終わりです。まだ止まるなら、原因は、hookが見ているパスと、あなたが戻した場所の食い違いです。

コマンドを止めるhookは、壊れると、すべてのコマンドを通すようになります。一度もコマンドを止めたことのないhookは、壊れていても、正しく動いているhookと同じに見えます。どちらも、コマンドを黙って通すからです。


CC BY 4.0 はここまで

次回からは応用編、「サブエージェントの出力だけは読め」。サブエージェントの出力だけは、読まないととんでもないことになります。

連載「読まない技術」