はじめに:「書いて」と言ったのに、違うスキルが動いた
当社(合同会社ジョインクラス)では、Claude Code に会社の運用ほぼ全部を任せています。launchd ジョブが17個あり、Zenn・Qiita・note への記事公開も毎日自動で回しています。
プロジェクトの .claude/skills/ にあるスキルは現在13個です。数としては多くありません。それでも次のような誤爆が起きました。
- 「Qiita 用に書いて」と頼んだのに
write-zennの採点基準(90点以上)で書かれる - 「Kindle にして」と頼むと、
publish-kindle/convert-to-epub/format-kindle-epubのどれが動くかが毎回変わる
原因はモデルの賢さではありませんでした。スキルの名前と description の設計です。Claude はスキル本文を最初から全部読むわけではありません。まず name と description だけを見て、どれを使うか決めます。つまり description は、モデルにとってのルーティングテーブルです。
この記事では、当社で実際に使っている SKILL.md の書き方と、それを機械的にチェックする lint スクリプトを紹介します。
1. 選択ミスが起きる3つのパターン
僕のところで実際に起きたものを分類すると、次の3つになりました。
| パターン | 例 | 何が起きるか |
|---|---|---|
| 動詞が同じ |
write-note / write-qiita / write-zenn
|
どれも「記事を書く」なので、媒体名が会話に出ないと区別できない |
| 工程が重なっている |
convert-to-epub / format-kindle-epub / publish-kindle
|
「EPUB」「Kindle」という語が3つの description すべてに入っている |
| description が抽象的 | 「〜を最適化する」「〜を支援する」 | どんな依頼にも当てはまりそうに見えて、優先度がおかしくなる |
一番効いたのは2番目の対策です。「自分はどこからどこまでを担当するか」を description に書くようにしました。
2. 命名規則:動詞-対象 で揃え、接頭辞を名前空間として使う
当社のスキル名は、すべて 動詞-対象 の kebab-case で揃えています。
.claude/skills/
├── write-note/ # 記事作成(媒体ごと)
├── write-qiita/
├── write-zenn/
├── publish-kindle/ # 出版(チャネルごと)
├── publish-zenn-book/
├── generate-book-cover/ # 生成物(素材ごと)
├── generate-book-charts/
├── convert-to-epub/
├── format-kindle-epub/
├── polish-note/
├── create-youtube-video/
├── upgrade-automation/
└── validate-hypothesis/
ルールは3つだけです。
-
ディレクトリ名と frontmatter の
nameを一致させる。ずれると、スラッシュコマンド名と自動選択の対象が別物に見えてしまいます。 -
接頭辞(
write-/publish-/generate-)は「同じ種類の作業」にだけ使う。接頭辞が同じなら、後ろの部分(媒体名など)が一番大事な識別子になります。 -
後ろの部分には、ユーザーが実際に口にする単語を使う。「Qiita に書いて」と言われるなら
qiitaです。tech-blog-jpのような社内用語は使いません。
3. description の設計テンプレート
当社の write-qiita の frontmatter(実物)はこうなっています。
---
name: write-qiita
description: Qiita記事を作成する。LGTM最大化のスコアリング基準(100点満点)に基づき、80点以上の記事のみ出力。使い方 /write-qiita "テーマ"
---
これを分解すると、次の4つの要素でできています。
[対象] + [動詞]。 [何が他と違うか=判断基準や出力条件]。 使い方 /<name> "<引数>"
- 対象を先頭に置く:「Qiita記事を」が最初に来るので、媒体名でマッチしやすくなります
- 他との違いを1文で書く:write-zenn は「90点以上」、write-qiita は「80点以上」。採点基準が違うことが description に出ていれば、取り違えたときに人間もすぐ気づけます
-
使い方で引数の形を示す:引数のない呼び出しや、間違った呼び出しが減ります
工程が重なるスキルは、**「入力 → 出力」と「担当外のこと」**を書き足します。たとえば Kindle 系の3つは、次のように整理しました。
# convert-to-epub: Markdown → EPUB 変換だけを担当
description: Zenn書籍のMarkdownをKindle向けの高品質EPUBに変換する。使い方 /convert-to-epub "書籍slug"
# format-kindle-epub: 既存 EPUB の組版だけを担当
description: Kindle EPUB書籍をプロの組版品質にフォーマットする。フォント、行間、余白、章区切り、図表配置を最適化し、出版品質のEPUBを生成する。使い方 /format-kindle-epub "書籍パス"
# publish-kindle: 上の2つを束ねる入口
description: Zenn書籍をAmazon Kindle向けに出版する。EPUB品質チェック→修正→再生成→KDP登録プロンプト作成まで一貫実行。使い方 /publish-kindle "書籍名"
ポイントは、ユーザーの曖昧な依頼(「Kindle にして」)を受ける入口スキルを1つ決めておくことです。入口の description には → で工程の流れを書き、ほかの2つは引数を「slug」「パス」のように具体的にして、部品として使うスキルだと分かるようにします。
落とし穴
-
description に複数行の YAML(
|)を使わない:パーサーによって改行の扱いが変わり、先頭の1行しか効かないことがありました。1行で書きます - 長くしすぎない:全スキルの description は毎回コンテキストに載ります。当社では200字を上限にしました
- CLAUDE.md に同じ説明を書かない:CLAUDE.md が1,000行を超えたとき、ルール同士が矛盾して AI が指示と逆の動きをしたことがあります。スキルの説明は SKILL.md だけに書いて、CLAUDE.md には書かないようにしています
4. lint スクリプトで機械的にチェックする
ルールは人間が守り続けられないので、スクリプトでチェックします。
#!/usr/bin/env bash
# SKILL.md の name / description を検査する
set -uo pipefail
SKILLS_DIR="${1:-.claude/skills}"
MAX_DESC=200
fail=0
for file in "$SKILLS_DIR"/*/SKILL.md; do
dir=$(basename "$(dirname "$file")")
name=$(awk '/^name:/{sub(/^name: */,"");print;exit}' "$file")
desc=$(awk '/^description:/{sub(/^description: */,"");print;exit}' "$file")
[[ "$name" == "$dir" ]] || { echo "NG name≠dir: $dir ($name)"; fail=1; }
[[ -n "$desc" ]] || { echo "NG descなし: $dir"; fail=1; continue; }
len=$(printf '%s' "$desc" | wc -m | tr -d ' ')
(( len <= MAX_DESC )) || { echo "NG 長すぎ(${len}字): $dir"; fail=1; }
[[ "$desc" == *"使い方"* ]] || echo "WARN 使い方なし: $dir"
done
# 同じ接頭辞(write-*, publish-* など)が3つ以上 → 境界をdescriptionで明記すべき
ls "$SKILLS_DIR" | cut -d- -f1 | sort | uniq -c | awk '$1>=3{print "INFO 接頭辞 "$2" が"$1"個 → 境界をdescriptionで明記"}'
exit $fail
実行します。
chmod +x scripts/lint-skills.sh
./scripts/lint-skills.sh .claude/skills
当社の13スキルでは、次のような出力になります。
INFO 接頭辞 write が3個 → 境界をdescriptionで明記
NG が1件でもあると終了コードが 1 になります。そのため、pre-commit や Claude Code の Hooks(PostToolUse で Write|Edit にマッチさせる)にそのまま組み込めます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "./scripts/lint-skills.sh .claude/skills >&2 || exit 2" }
]
}
]
}
}
exit 2 で終わると、stderr の内容が Claude に返されます。AI が自分で SKILL.md を作ったり直したりしたときも、命名違反をその場で直させることができます。
注意点
-
wc -mで文字数を数えるにはマルチバイト対応のロケールが必要です。LANG=Cの環境ではバイト数になって、日本語の description がすべて「長すぎ」と判定されます。CI ではLANG=ja_JP.UTF-8(またはC.UTF-8)を指定してください - frontmatter の値をクォートで囲んでいる場合(
description: "...")は、subでクォートも取り除いてください
5. 選択精度の検証方法
lint は形式しかチェックできません。実際に正しいスキルが選ばれるかどうかは、曖昧な依頼文を投げて確認するしかありません。僕は次の手順で確認しています。
- ユーザーが実際に言いそうな依頼を、スキルごとに3つ書き出す(例:「Kindle にして」「EPUB 作って」「本の組版を整えて」)
- 新しいセッションで1つずつ投げ、どのスキルが呼ばれたかを記録する
- 外れたら、負けた側ではなく勝った側の description を絞る
3番目が大事です。間違って選ばれたスキルは、description の範囲が広すぎるのが原因であることがほとんどでした。
まとめ
- Claude はスキルを
nameとdescriptionで選ぶ。description はルーティングテーブルとして設計する - 名前は
動詞-対象。接頭辞は名前空間として使い、後ろにはユーザーが口にする単語を入れる - description は「対象+動詞 / 他との違い / 使い方」の3つで構成する。工程が重なるスキルは入口を1つに決める
- lint と Hooks で形式をチェックし、選択精度は実際の依頼文で確認する
当社は以前、10事業を同時に回して9事業が売上ゼロになったことがあります。「何でもできる」状態は「何も選べない」状態と同じでした。スキルも同じで、数を増やすより境界をはっきりさせる方が成果につながります。
もっと深く知りたい方へ
スキルの設計(progressive disclosure、補助スクリプトの同梱、プラグインとしての配布)をまとめて解説した本を書いています。
- 『Claude Skills 完全ガイド』
- 書籍一覧: https://zenn.dev/joinclass?tab=books