朝いちばんに、壁紙アプリの素材を仕分けるスキルを直そうとして一覧を開きました。上から目で追っていって、途中で手が止まりました。行が思ったより多いのです。
数えたら42本ありました。半分以上について、最後にいつ呼んだのかを思い出せませんでした。
入れた記憶はあります。どれも入れた日には必要でした。ただ、外す日というのは自分から訪れてくれません。棚は増える方向にしか動かないのだと、その朝にようやく認めました。
Claude Code v2.1.261(9月4日)に /skill-doctor が入りました。読み込んでいるスキルのうち、どれが使われておらず、どれだけ文脈を食べているかを一覧にしてくれるコマンドです。せっかくなので、自分の棚を先に数えてから向き合うことにしました。
毎ターン乗るのは、説明の1行だけです
先に構造の話をお伝えします。ここを取り違えると、数字の読み方がまるごとずれてしまうからです。
スキルが読み込まれているとき、毎ターン文脈に乗っているのは SKILL.md の本体ではありません。frontmatter に書いた name と description、つまり「このスキルは何をするものか」の説明にあたる部分です。本体が読まれるのは、そのスキルが実際に呼ばれたときだけになります。
言い換えますと、一度も呼んでいないスキルでも、一覧に載っているというだけで説明の分は毎ターン支払っている、という構図です。「使っていないから無料」ではありません。
/skill-doctor が出す表は6つの列で構成されています。
| 列 | 意味 |
|---|---|
| skill | スキル名 |
| source | どこから来ているか(ユーザー/プラグイン/マーケットプレイス) |
| context | 説明1行分として毎ターン乗っている量 |
| 7d tokens | 直近7日で実際に消費した量 |
| uses | 呼ばれた回数 |
| last used | 最後に呼ばれた日時 |
並びは last used 順で、一度も呼ばれていない never が先頭に来ます。開いた瞬間に、いちばん見たくない行から目に入る設計になっています。
自分の棚を、実際に数えてみました
コマンドの結果を待つ前に、ファイルの側から数えられる分だけ数えました。フォルダを走査して、frontmatter の description が何文字あるかを足すだけの小さなスクリプトです。
import os, re, sys
root = sys.argv[1] if len(sys.argv) > 1 else "."
rows = []
for name in sorted(os.listdir(root)):
path = os.path.join(root, name, "SKILL.md")
if not os.path.isfile(path):
continue
text = open(path, encoding="utf-8", errors="replace").read()
m = re.match(r"^---\n(.*?)\n---\n", text, re.S)
desc = ""
if m:
d = re.search(r"^description:\s*(.*(?:\n[ \t]+.*)*)$", m.group(1), re.M)
if d:
desc = re.sub(r"\s+", " ", d.group(1).strip().strip("\"'"))
rows.append((name, m is not None, len(desc), len(text), len(text.splitlines())))
with_fm = [r for r in rows if r[1]]
without = [r for r in rows if not r[1]]
desc_total = sum(r[2] for r in with_fm)
body_total = sum(r[3] for r in rows)
print(f"SKILL.md のあるフォルダ : {len(rows)}")
print(f"frontmatter あり : {len(with_fm)}")
print(f"frontmatter なし : {len(without)}")
print(f"説明行の合計 : {desc_total:,} 文字 (平均 {desc_total // max(len(with_fm),1)})")
print(f"本体の合計 : {body_total:,} 文字 / {sum(r[4] for r in rows):,} 行")
print(f"説明行が占める割合 : {desc_total / body_total * 100:.2f} %")
print("説明行の長い順 上位3件 :")
for r in sorted(with_fm, key=lambda x: -x[2])[:3]:
print(f" {r[0]:<32} {r[2]:>4} 文字")手元のスキルフォルダに向けた結果です。
SKILL.md のあるフォルダ : 42
frontmatter あり : 38
frontmatter なし : 4
説明行の合計 : 11,752 文字 (平均 309)
本体の合計 : 332,292 文字 / 8,931 行
説明行が占める割合 : 3.54 %
説明行の長い順 上位3件 :
claude-api 744 文字
ai-seo 698 文字
seo-audit 686 文字本体は33万文字あるのに、毎ターン乗るのはそのうち 3.54% にあたる1万2千文字弱でした。この比率を見て、少し安心しました。棚が重いのは事実でも、重さのほとんどは呼んだときにだけ発生しているのです。
同時に、平均309文字という数字は軽くありません。説明が744文字あるスキルと79文字のスキルが同じ棚に並んでいて、その差は9倍を超えていました。説明を短く書き直すだけで減らせる分が、まだ残っているということになります。
4本は、移転を告げる紙だけが残っていました
いちばん効いたのは、frontmatter が無いフォルダが4本あった、という行でした。
中を開いたら、どれも「このファイルは移動しました」と新しい置き場所を書いただけの短い文書でした。前にスキルの置き場を整理したときの案内で、移した側の作業は終わっているのに、案内の紙だけが棚に残っていたのです。移した日には、その紙が要ると思っていました。
こういうものは、名前を見ているかぎり気づけません。名前は生きているままだからです。中身を開いて初めて、もう役目が終わっていると分かります。
棚に置いた数ではなく、手が伸びた数を数えます。 一覧の行数は、自分が使っているものの数ではありませんでした。この一文を、しばらく作業机の近くに置いておこうと思っています。
never を「不要」と読み替えないようにしています
ここで急ぎたくなるのですが、一度立ち止まる必要があります。
last used が never であることは、「この端末の記録上まだ呼ばれていない」という意味です。入れたばかりのスキルも never になりますし、年に数回の作業でしか呼ばないスキルも never になります。表示された時点では削除の候補であって、不要が確定したわけではないのです。
私の42本にも、四半期に一度だけ使うものが混ざっていました。これを勢いで消してしまうと、必要になった日に「あのとき消したもの」を思い出すところから始めることになります。
もう一つ、混ぜないようにしている話があります。未使用のスキルを減らすと応答の品質や速度が上がるのか、という点については、公式の情報に書かれていません。文脈のコストの話は文脈のコストの話として書き、品質の話とは別に扱うほうが誠実だと感じています。減ったのは支払いであって、腕前ではありません。
同じ名前が二つの棚から来ていないか
もう一つ確かめる価値があるのが、同じプラグインを二つのマーケットプレイスから有効にしていないか、という点です。名前が同じでも出どころが違えば別々に読み込まれますから、スキルの行が丸ごと二重になります。
/skill-doctor の出力をファイルへ落として、名前が重複している行を探すのが手早い方法です。ただ、素朴に一列目だけを取ると、空白の入った名前で足をすくわれます。手元で試したものを、そのまま置きます。
# 検証用に、名前へ空白を含む行を混ぜた表を作ります
cat > /tmp/sd.txt <<'TABLE'
skill source context 7d tokens uses last used
brand voice marketplace-a 300 0 0 never
brand guidelines user 280 0 0 never
seo-audit user 686 3120 4 2 days ago
seo-audit claude-plugins-official 686 0 0 never
TABLE
awk 'NF>3 {print $1}' /tmp/sd.txt | sort | uniq -d結果はこうなりました。
brand
seo-auditseo-audit は当たりです。しかし brand は外れでした。brand voice と brand guidelines は別のスキルなのに、一列目だけを見ると先頭の語が揃ってしまい、重複として報告されてしまったのです。名前に空白が入るスキルは珍しくありませんから、この誤検出は普通に起こります。
名前の列を「2つ以上の空白が現れるまで」で切ると、この取り違えは消えます。
awk 'NR>1 {
if (match($0, / +/)) {
n = substr($0, 1, RSTART - 1)
c[n]++
}
} END {
for (k in c) if (c[k] > 1) printf "%s\tx%d\n", k, c[k]
}' /tmp/sd.txt | sortseo-audit x2NR>1 で見出し行を落としているのは、skill という語が名前として数えられてしまうのを避けるためです。素朴な版では、見出しも1件として数に入っていました。
出どころそのものを確かめたい場合は、設定ファイルの側から見ます。enabledPlugins は配列の形と辞書の形の両方を見かけますので、どちらでも通るようにしてあります。
import json, sys, collections
path = sys.argv[1] if len(sys.argv) > 1 else "settings.json"
try:
data = json.load(open(path, encoding="utf-8"))
except FileNotFoundError:
print(f"設定が見つかりません: {path}")
sys.exit(0)
except json.JSONDecodeError as e:
print(f"JSON を読めません: {path} ({e})")
sys.exit(2)
raw = data.get("enabledPlugins")
pairs = []
if isinstance(raw, list):
for entry in raw:
name, _, market = str(entry).partition("@")
pairs.append((name, market or "(marketplace 不明)"))
elif isinstance(raw, dict):
for market, names in raw.items():
for name in names or []:
pairs.append((str(name), market))
else:
print("enabledPlugins が配列でも辞書でもありません。形を先に確認してください。")
sys.exit(2)
by_name = collections.defaultdict(list)
for name, market in pairs:
by_name[name].append(market)
dups = {n: m for n, m in by_name.items() if len(m) > 1}
print(f"有効プラグイン {len(pairs)} 件 / 名前 {len(by_name)} 種")
for name in sorted(dups):
print(" 重複: " + name + " <- " + " / ".join(sorted(dups[name])))
sys.exit(1 if dups else 0)配列の形と辞書の形、それぞれの設定を通した結果です。どちらでも同じ答えが返りました。
有効プラグイン 5 件 / 名前 3 種
重複: aws-core <- agent-toolkit-for-aws / claude-plugins-official
重複: frontend-design <- claude-plugins-official / my-marketplace終了コードを 1 にしてあるのは、あとで朝の点検スクリプトに並べるためです。ファイルが無いときは 0 で静かに抜け、JSON が壊れているときだけ 2 を返します。無いことと壊れていることは、別の扱いにしておきたいのです。設定ファイルが無言で効かないまま放置される話は、settings.json のキー名を1文字間違えても、Claude Code は何も言わずに無視しますにも書きました。
明日の朝に、最初の一手
やることを一つだけに絞ります。/skill-doctor を一度だけ走らせて、last used が never の行が何本あるかを数えてください。数えるだけで、その場では消さないでください。
一週間置いてもう一度走らせると、そのあいだに一度でも呼ばれたスキルが never から外れます。二回目でも never のまま残ったものが、はじめて外す候補になります。私はこの二段構えにしてから、消して後悔することがなくなりました。
説明1行あたりの重さをもっと細かく詰めたい方には、起動しただけで1万トークン消えるスキルを、大きさではなく到達率で切り分けるで、節ごとに測って切り分けた実測を残しています。どのスキルを日常に残すかという手前の判断は、Claude Code Skills を毎日の開発に組み込むときに、私が見ている 5 つの選定軸のほうにまとめました。
棚を数えるのは気の重い作業でした。それでも、42という数字を見たあとのほうが、道具との距離が近くなった気がしています。最後までお付き合いいただき、ありがとうございました。