TL;DR と結論
Claude Codeのスキルは、名前を打てば完璧に動くのに、普通の言葉で頼むと読み込まれないことがある。Claudeはスキルを読み込むと決める前に、スキルごとに1行、名前とdescriptionを読む。そしてその行をあなたの言葉と照合する。行があなたの言葉を使っていなければ、スキルは沈黙したままだ。
私たちの実行では、descriptionが「Generates changelog documents from repository history.」のリリースノート用スキルは、依頼「what did we ship this week?」に対して、新しいセッション5回中0回で読み込まれた。その1行を、何をするか、いつ使うか、私たちが打つ言葉として書き直した。すると同じ依頼で5回中5回読み込まれた。
結論:
- スキルが役立つのは、固定の出力で繰り返す作業だ。週次のノート、プルリクエストの説明、リリースのチェックリスト。
- 1回きりの質問には向かないし、常に守られるべきルールのための道具でもない。それはフックの仕事だ。
- 誰も使わないスキルの山にも代償がある。一覧の予算を超えると、Claude Codeは使用頻度の低いものからdescriptionを切るからだ。
- 唯一のルール: descriptionは、その作業を頼むときに実際に打つ言葉で書くこと。
以下のキットには、スキルファイルの修正前と修正後、descriptionのテンプレート、新しいセッション用のチェックリスト、私たちが使った20個の依頼のテストが入っている。
ドキュメントと現場の知見
スキルの置き場所。 スキルは中にSKILL.mdがあるフォルダで、全プロジェクト向けなら~/.claude/skills/<skill-name>/に、チームにも渡すなら.claude/skills/<skill-name>/に置く。単独のname.mdファイルは読み込まれない。 s1 s2
Claudeが読むのは本文ではなくdescription。 名前とdescriptionの一覧は毎ターンコンテキストにあり、本文はスキルが呼ばれたときにだけ読み込まれる。ドキュメントによれば、スキルをいつ適用するかをClaudeが決めるのに使うのはdescriptionだ。 s1
一覧には上限と予算がある。 descriptionとwhen_to_useのテキストは、合わせて1,536文字で切り詰められる。一覧全体はコンテキストウィンドウの1%に制限され、あふれると、Claude Codeは呼び出す頻度の低いスキルからdescriptionを落とす。ドキュメントによれば、これでClaudeが依頼の照合に必要なキーワードが消える。--debugでデバッグログに警告が出る。 s1
壊れたヘッダーは、成功に見える罠。 YAMLが壊れていると、本文はメタデータが空のまま読み込まれるので、/skill-nameは動くが、Claudeはdescriptionを依頼と照合できない。--debugで解析エラーを確認するか、claude plugin validate .claude/skillsを実行する。 s1 s2
バッジを確認する。 スキルが/skillsにあるのにClaudeが呼ばないなら、disable-model-invocation: trueが設定されているか、descriptionがあなたの頼み方と合っていない。user-onlyのラベルは、Claudeが自分では発動させないという意味だ。 s2
良いdescriptionの中身。 Anthropicのガイドは、descriptionにはスキルが何をするかと、いつ使うかの両方を含め、三人称で、ユーザーが打つキーワードを入れるよう述べている。100以上になりうるスキルの中から適切なものをClaudeが選ぶのに使う、とも書かれている。 s3
Claudeはundertriggerしがち。 Anthropicのskill-creatorは「currently Claude has a tendency to 'undertrigger' skills」と述べ、少し「pushy」なdescriptionを勧めている。 s18 そのトリガーテストは20個のクエリを使い、発動すべきものと発動すべきでないものを混ぜ、ニアミスが最も価値が高いとする。ループは60%を学習用、40%をテスト用に分け、各クエリを3回実行し、テストスコアが最も高いdescriptionを残す。 s4 評価スクリプトは各クエリをclaude -pで実行し、スキルが使われたかを確認する。 s19
発動はキーワード照合のように振る舞う。 実際のバイナリでのサードパーティのテスト(22個のプロンプト、Sonnet 4.5)では、2回の実行でベースラインが55%と50%、強制評価のフックが100%だった。明示的なトークンを含むプロンプトはほぼ100%発動し、間接的な言い回しはほぼ0%だった。著者の見方は、意味的な照合よりキーワード照合に近いというものだ。これは1つのブログ、古いモデル、別のリポジトリなので、ルールではなく補強的な証拠として扱うこと。 s5
descriptionは要約ではなくトリガー。 実際の9つのスキルの監査も同じ結論に達した。具体的な状況を列挙するdescriptionは、スキルが何であるかを述べるものに勝つ。 s8
逆の失敗もある。 ある開発者は、スキルが間違った作業で発動するのを見て、トリガー評価を書き、descriptionを「Load when...」に書き直した。残す価値のあるスキルは、二度見るのにうんざりした失敗から生まれることが多い、とも述べている。 s11
他の人も踏んでいる。 「Claude Code not aware of skills in .claude/skills」というissueには75のリアクションがあり、2026-10-17の時点でまだ開いていた。スレッドの回避策には、CLAUDE.mdへのポインタとフックがある。 s7
フックがスキルに勝つ場面。 778アップボートのスレッドの上位の回答は、スキルは「basically just a prompt you save for later」で、Claudeが使うと決めるか、あなたがスラッシュコマンドで呼ぶものであり、フックはスキルが緩すぎて決定論が欲しいときのものだと述べている。 s9 別のスレッドは、リマインダーによる後押しは長いセッションの中で薄れうると警告している。 s10
キット
修正前と修正後のスキル。違うのはdescriptionの行だけで、本文は同一。
release-notes.before.SKILL.md
---
name: release-notes
description: Generates changelog documents from repository history.
---
# Release notes
1. Find the range: commits since last Monday (`git log --since="last monday" --no-merges --pretty="%h %s"`).
2. Drop `chore` and `docs` commits.
3. Group what is left by the conventional-commit scope (router, css, jsx, serve-static...), fixes and features apart.
4. Write one line per change, in plain English, present tense, no commit hashes, with the PR number at the end.
5. Output a markdown block titled "Release notes, week of <date>", with a two-line summary on top.
release-notes.before.SKILL.md on GitHub
release-notes.after.SKILL.md
---
name: release-notes
description: Writes this week's release notes from git log, grouped by scope (router, css, jsx). Use when the user asks what we shipped, what changed this week, for release notes, a changelog, or a summary of recent commits.
---
# Release notes
1. Find the range: commits since last Monday (`git log --since="last monday" --no-merges --pretty="%h %s"`).
2. Drop `chore` and `docs` commits.
3. Group what is left by the conventional-commit scope (router, css, jsx, serve-static...), fixes and features apart.
4. Write one line per change, in plain English, present tense, no commit hashes, with the PR number at the end.
5. Output a markdown block titled "Release notes, week of <date>", with a two-line summary on top.
release-notes.after.SKILL.md on GitHub
descriptionのテンプレート。実際のdescriptionを実例として載せている。
description-template.md
# Skill description template
The description is the one line Claude reads before it decides to load your skill. Write it for the moment you ask, not for the file.
## Template
```
<Verb in third person> <what the skill produces>, <how it is shaped>. Use when the user asks <phrase 1>, <phrase 2>, <phrase 3>, or <phrase 4>.
```
Three parts, in this order:
1. What it does. Third person ("Writes", never "I can" or "You can use this"). Name the output.
2. When to use it. Start with "Use when the user asks".
3. Your words. The exact phrases you or your team type, copied from your own history, not synonyms you invented.
Put the key use case first. The description and `when_to_use` are cut together at 1,536 characters in the skill listing.
## Worked example (the real one from our run)
Before, a summary of the skill:
```
Generates changelog documents from repository history.
```
After, what + when + the words we type:
```
Writes this week's release notes from git log, grouped by scope (router, css, jsx). Use when the user asks what we shipped, what changed this week, for release notes, a changelog, or a summary of recent commits.
```
What changed:
- What: "Writes this week's release notes from git log, grouped by scope".
- When: "Use when the user asks ...".
- Your words: "what we shipped", "what changed this week", "release notes", "a changelog", "a summary of recent commits".
## Fill-in sheet
- Skill name:
- The prompt I keep retyping (copy it from your history):
- The output I want from it:
- Verb + output (what it does):
- Four phrases I really type (when):
- Phrases that sound close but are a different job (for your near misses):
## Check before you save
- Third person, no "I" and no "you".
- At least three phrases you really type.
- Under 1,536 characters in total.
- Not so wide that it matches a neighbour job. Add the near misses to your trigger test.
description-template.md on GitHub
新しいセッション用のチェックリスト。
trigger-checklist.md
# Fresh-session trigger checklist
A skill that runs by name can still be invisible to a plain request. Test it the way a teammate will use it.
## Test in a fresh session
1. Open a new session. A skill already used stays in the conversation, so a test in the same session proves nothing.
2. Ask for the task in your own words, without the skill's name. Example: "what did we ship this week?"
3. Watch for the Skill tool call before Claude touches anything else. If Claude runs the commands itself and writes its own summary, the skill was skipped.
4. Repeat in several fresh sessions. A handful of runs is a demo, not a statistic.
## If it was skipped, read why before you edit
1. `/skills`: is it listed, is it switched on, is there a `user-only` badge? A `user-only` badge means Claude will not trigger it on its own.
2. Ask "What skills are available?" and check your skill is in the answer.
3. `claude --debug`, then search the log for the skills line (how many loaded) and for a "Skill listing over budget" warning. Over budget means some descriptions are cut, the least invoked first.
4. `claude plugin validate .claude/skills`: a malformed header still lets `/name` work but leaves Claude with empty metadata to match against.
5. Check the file is `.claude/skills/<name>/SKILL.md`, not a loose `<name>.md`, and that the header starts on the first line.
## Fix, then prove it
1. Rewrite the description with `description-template.md`.
2. Fresh session, same plain request, several runs: does the Skill call come first now?
3. Baseline: set the skill to `"off"` in `skillOverrides` (saved to `.claude/settings.local.json` from `/skills`), ask again, then switch it back on.
## Hold it
1. Write twenty requests: ten that should load the skill, ten near misses (`evals.json` is our set).
2. Run each in its own fresh session and note whether the skill loaded (`trigger_test.py`, `tally.py`).
3. Re-run the list every time you change the description.
4. For a skill that must never miss: call it by name, or add a UserPromptSubmit hook (`hook.json`).
trigger-checklist.md on GitHub
20個の依頼のテストセット。スキルを読み込むべき依頼10個と、読み込むべきでないニアミス10個。クエリは自分のリポジトリに合わせて編集すること。
evals.json
[
{"query": "what did we ship this week?", "should_trigger": true},
{"query": "can you put together the release notes for this week", "should_trigger": true},
{"query": "what changed in the repo since monday", "should_trigger": true},
{"query": "write the changelog for the commits we merged this week", "should_trigger": true},
{"query": "I need a summary of this week's commits for the team update on friday", "should_trigger": true},
{"query": "draft the weekly notes of what got fixed, grouped by area", "should_trigger": true},
{"query": "list what landed this week, router and css changes apart", "should_trigger": true},
{"query": "give me something to post in the team channel about this week's fixes", "should_trigger": true},
{"query": "recap the work merged since last monday", "should_trigger": true},
{"query": "what's new in hono this week? short version for the release post", "should_trigger": true},
{"query": "explain what commit 08807ce1 changes in the trie router", "should_trigger": false},
{"query": "the jsx tests are failing on cloneElement, can you fix them", "should_trigger": false},
{"query": "write a commit message for my staged changes", "should_trigger": false},
{"query": "who wrote most of the serve-static code?", "should_trigger": false},
{"query": "add a CHANGELOG entry format section to CONTRIBUTING.md", "should_trigger": false},
{"query": "show me the diff between main and my branch", "should_trigger": false},
{"query": "bump the version in package.json to the next patch", "should_trigger": false},
{"query": "why does the css helper minify url() like that?", "should_trigger": false},
{"query": "squash my last three commits into one", "should_trigger": false},
{"query": "review this PR for breaking changes before I merge it", "should_trigger": false}
]
各依頼を専用の新しいclaude -pセッションで実行し、Skillツールが呼ばれたかを数えるスクリプト。
trigger_test.py
"""Twenty-prompt trigger test for one skill, the method skill-creator describes, run on the real skill folder.
For each query: a fresh `claude -p` in the repo; the run counts as loaded if the Skill tool is called with the skill's name
before the run ends (stopped early as soon as it is). Usage:
python3 trigger_test.py <repo> <skill-name> <SKILL.md to install> <evals.json> <out.json> [workers]"""
import json, os, shutil, subprocess, sys
from concurrent.futures import ThreadPoolExecutor
repo, name, skill_md, evals_path, out = sys.argv[1:6]
workers = int(sys.argv[6]) if len(sys.argv) > 6 else 4
dest = os.path.join(repo, '.claude', 'skills', name)
os.makedirs(dest, exist_ok=True)
shutil.copy(skill_md, os.path.join(dest, 'SKILL.md'))
evals = json.load(open(evals_path))
allowed = ['Bash(rtk git log *)', 'Bash(git log *)', 'Bash(git show *)', 'Bash(date *)', 'Skill', 'Read']
def run(item):
cmd = ['claude', '-p', item['query'], '--output-format', 'stream-json', '--verbose', '--allowedTools', *allowed]
p = subprocess.Popen(cmd, cwd=repo, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
loaded = False
try:
for line in p.stdout:
try:
d = json.loads(line)
except ValueError:
continue
if d.get('type') == 'assistant':
for c in d['message'].get('content', []):
if c.get('type') == 'tool_use' and c['name'] == 'Skill' and name in json.dumps(c.get('input', {})):
loaded = True
if loaded or d.get('type') == 'result':
break
finally:
if p.poll() is None:
p.kill()
return {**item, 'loaded': loaded, 'pass': loaded == item['should_trigger']}
with ThreadPoolExecutor(workers) as ex:
results = list(ex.map(run, evals))
should = [r for r in results if r['should_trigger']]
shouldnt = [r for r in results if not r['should_trigger']]
summary = {'loaded_when_it_should': sum(r['loaded'] for r in should), 'of_should': len(should),
'loaded_when_it_should_not': sum(r['loaded'] for r in shouldnt), 'of_should_not': len(shouldnt),
'passed': sum(r['pass'] for r in results), 'total': len(results)}
json.dump({'skill': name, 'skill_md': skill_md, 'summary': summary, 'results': results}, open(out, 'w'), indent=2)
for r in results:
print(f"[{'PASS' if r['pass'] else 'FAIL'}] {'loaded ' if r['loaded'] else 'skipped'} expected={'load' if r['should_trigger'] else 'skip'}: {r['query']}")
print(json.dumps(summary))
claude -p --output-format stream-jsonの実行結果のフォルダ向けの小さな集計スクリプト。
tally.py
"""Count whether a `claude -p --output-format stream-json` run loaded a given skill. Usage: python3 tally.py <skill> run1.jsonl [run2.jsonl ...]"""
import json, sys
skill, files = sys.argv[1], sys.argv[2:]
fired = 0
for f in files:
hit = False
for line in open(f):
try:
d = json.loads(line)
except ValueError:
continue
if d.get('type') == 'assistant':
for c in d['message'].get('content', []):
if c.get('type') == 'tool_use' and c['name'] == 'Skill' and skill in json.dumps(c.get('input', {})):
hit = True
fired += hit
print(f"{f}: {'loaded' if hit else 'skipped'}")
print(f"{skill}: loaded in {fired} of {len(files)} runs")
必ず動かすためのフォールバック: スキルの名前を挙げるUserPromptSubmitフック。
hook.json
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "echo 'If the user asks about release notes, a changelog or what shipped this week, use the release-notes skill.'"
}
]
}
]
}
}
月曜にやること
- 先週いちばん打ち直したプロンプトと、そこから欲しい出力を正確に書き出す。
- 名前、description、短い番号付きの本文で、
.claude/skills/<name>/SKILL.mdを作る。 - 名前でテストし、次に新しいセッションを開いて、名前を使わず普通の言葉で作業を頼む。
- スキップされたら、何かを編集する前に
/skills、claude --debug、claude plugin validate .claude/skillsを読む。 -
description-template.mdでdescriptionを書き直す。何をするか、いつ使うか、自分が打つ言葉。 - 複数の新しいセッションで再テストし、次に
skillOverridesでスキルを"off"にして1回、オンに戻して1回試す。 - 20個の依頼を書き、
trigger_test.pyで実行し、descriptionを変えるたびに再実行する。 - 絶対に外せないスキルは決める。名前で呼ぶか、フックを足すか。
私たちが測ったこと
これらは、オープンソースのHonoリポジトリ(s20)の作業用クローンで行った小さなデモ実行で、Claude Code 2.1.294、依頼は「what did we ship this week?」、対象は月曜2026-10-12以降のマージを除くconventional commit 8件だ。ベンチマークではない。あなたのモデル、スキル、リポジトリで数字は動く。
| テスト | description | 結果 |
|---|---|---|
| 普通の依頼、新しいセッション5回 | 要約: 「Generates changelog documents from repository history.」 | 5回中0回読み込み |
| 普通の依頼、新しいセッション5回 | 書き直し: 何を + いつ + 私たちの言葉 | 5回中5回読み込み |
名前で呼ぶ(/release-notes) |
要約 | 2回中2回実行 |
skillOverridesをオフ、次にオン |
書き直し | オフ: スキップ。オン: 読み込み |
| 20個の依頼のテスト、書き直し前 | 要約 | 読み込むべき依頼10個中4個が読み込み、ニアミス10個中0個、20個中14個が合格 |
| 20個の依頼のテスト、書き直し後 | 書き直し | 読み込むべき10個中10個、ニアミス10個中0個、20個中20個が合格 |
| フックと古いdescription | 要約 | 1回中1回読み込み |
書き直し前に読み込まれた4個の依頼は、「release notes」「changelog」「notes」「release」を含むものだった。これがキーワード効果の見える形だ。書き直し後は、広くなったdescriptionがニアミスを1つも引き込まなかった。
見つけた警告と、率直な読み方。 --debugで、私たちの環境は「Skill listing over budget: 296 skills, 144833 chars > 30000 budget」と出力した。私たちの環境はスキルを持ちすぎていて、descriptionが切り詰められる。それでもプロジェクトのスキルは読み込まれた(「Loaded 43 unique skills」、そのうちの1つがプロジェクトのスキル)。古いdescriptionも切り詰めを生き延びた。20個の依頼のテストで、descriptionにしかない語「changelog」を含む依頼はスキルを読み込んだ。予算が新しいスキルを一般にどう扱うかは証明していない。スキルを多く抱えているなら、自分で確認してほしい。私たちの環境の/skillsは277個のスキルを一覧にし、release-notesはプロジェクトのスキルとして、オンで、user-onlyバッジなしで表示された。
ツール自体についての注意: Anthropicの評価スクリプトは、私たちのバージョンではスキルをまったく検出しなかったので、同じ依頼をキットの短いスクリプトで実行した。方法は同じで、依頼ごとに新しいclaude -pセッションを1つ使う。
さらに先へ
逆の問題も直す。 合致しすぎるdescriptionは、発動すべきでない場面でスキルを発動させる。だからテストセットの半分はニアミスだ。思いつく限りの語ではなく、実際に使う語を足そう。 s11
最適化ツールに書き直しを提案させる。 skill-creatorのループは新しいdescriptionを提案し、チューニングに使ったクエリへの過学習を避けるため、テスト用に取り分けたスコアが最も高いものを残す。自分の20個の依頼を評価セットとして使おう。 s4
内容のあるテストプロンプトを使う。 skill-creatorは、Claudeが自力では簡単に扱えない作業にしかスキルを参照しないと述べている。そのため、1ステップだけの些細な依頼は、完璧に合致してもスキルを発動させないことがある。 s4
単純なフックの先へ。 上記のサードパーティのテストでは、Claudeに各スキルをyesかnoで列挙させてから発動させる強制評価のフックは100%に達したが、受け身のリマインダーは達しなかった。また、ネイティブのtype: "prompt"フックはフックなしと同じ挙動だった。私たちのフックは単純な版で、1回のデモ実行だ。 s5 フックによる後押しは長いセッションで薄れうるという報告もある。 s10
本文は短く。 上の監査では、壊れていたスキルは1,002行でMANDATORYだらけだったのに対し、健全なスキルは短かった。 s8
扱わなかったこと。 プラグインのスキルとプラグインの評価グレーダーは扱っていない。これらはプラグイン作者にしか関係しない。スキルを素のモデルと数か月にわたって比較する方法も扱っていない。弱いモデル向けに書かれたスキルが不要な重荷になりうるという報告もあるので、ときどき再テストしてほしい。これは測っていない。
FAQ
なぜ/release-notesは動くのに、普通の言葉では動かないのか?
名前を打つと照合を飛ばせる。普通の言葉では、Claudeが照合に使えるのは名前とdescriptionの1行だけなので、依頼と共通する語がない要約はスキップされる。 s1
スキルを編集したら、Claude Codeを再起動する必要はあるか?
ない。Claude Codeは、~/.claude/skills/や.claude/skills/の下で追加や編集されたスキルを、今のセッションのまま拾う。セッション開始時に存在しなかったトップレベルのskillsディレクトリを作った場合は、/reload-skillsを実行する。 s1
descriptionはどのくらいの長さにできるか?
descriptionとwhen_to_useは一覧で合わせて1,536文字で切られるので、主要な使い道を先頭に置く。Anthropicのプラットフォームのガイドは別に、Claude Codeの外でアップロードするスキルのdescriptionを1,024文字までとしている。 s1 s3
新しいセッション5回で、修正を信頼するのに十分か? 十分ではない。方向が分かるだけだ。私たちの結果は1つのリポジトリでのデモなので、キットの最後にdescriptionを変えるたびに再実行できる20個の依頼のテストを付けた。
スキルではなくフックを使うべきなのはいつか? Claudeが合致に気づいたときだけでなく、毎回守られるべきルールのとき。フックはメッセージごとに1行を足すので、動かす必要のある少数のスキルにとどめよう。 s9
AIDive