プラグインを入れるとスキルが付いてくる。マーケットプレイスを足すたびに増える。減らす作業は誰もやらない。
手元の ~/.claude/ を数えたら、skills/ 直下のディレクトリが214個あった。そのうち何個が実際に呼ばれたのかを、全ログ5,958ファイルから数えた。
入っているもの
=== 入っているもの
skills/ 直下のディレクトリ 214
SKILL.md 233 本 / ユニーク名 129 / 同名が複数の出所にある 42
本文合計 4,575,145 字 / 1本 中央値 11,736 最大 104,834
description 合計 70,657 字 = 本文の 1.5% / 中央値 254 字
プロジェクト側 .claude/skills のユニーク名 35
数え方が3通りある。
-
ディレクトリ 214:
plugins/**/skills/*が171、~/.claude/skills/*が43。 -
SKILL.md 233本: ディレクトリより多い。
skills/*/の直下ではなく、さらに深い階層に置かれたSKILL.mdが20本ある。逆にSKILL.mdを持たないディレクトリが1つある。 - ユニーク名 129: 42個の名前が複数の出所に重複している。同じプラグインを別のマーケットプレイスからも入れているためで、多いものは同名が11箇所にあった。
「214個入れている」と言うのは正確ではなく、実体としては129種類が正しい。
本文の合計は457万字。ただしこれが常に文脈に載るわけではない。呼び出す前に提示されるのは名前と description なので、常時かかっているのは 70,657字、本文の1.5%のほうだ。それでも1本あたり254字が129本ぶん並ぶ。
呼ばれたもの
=== 呼ばれたもの(ログ 5,958 ファイル)
Skill 呼び出し 334 回 / 異なるスキル 17 種
グローバルに入っている 129 種のうち呼ばれた 4 = 3.1%
プロジェクト側 35 種のうち呼ばれた 8 = 22.9%
どちらにも見つからない(組込み等) 5 種
呼び出し回数の出所別:
プロジェクト 182 = 54.5%
グローバル 122 = 36.5%
組込み等 30 = 9.0%
上位3種で 217 回 = 65.0%
129種類のうち、一度でも呼ばれたのは 4種類 = 3.1%。125種類は一度も呼ばれていない。
一方、リポジトリの .claude/skills/ に自分で書いた35種類のうちは、8種類 = 22.9% が呼ばれている。発火率が7倍違う。
呼び出し回数で見ても、過半数(54.5%)はプロジェクト固有のスキルが占める。グローバルに入れた129種類が担っているのは36.5%で、しかもその中身は数種類に偏っている。上位3種だけで全体の65.0%を占める。
なぜ差が出るか
自分で書いたスキルは、書いた理由がある。「この手順を毎回間違えるから固定した」という具体的な失敗が背後にある。だから同じ状況が再来したときに呼ばれる。
インストールしたスキルには、その理由がない。マーケットプレイスを足した副産物として入ってくる。description は読まれるが、それが自分の作業と結びつく瞬間が来ない。
125種類が眠っているのは、スキルの出来が悪いからではなく、そのスキルが想定する状況にこちらが入らないからだ。
減らすべきかどうかは、この数字だけでは決まらない
「3.1%しか使っていないから125種類消せ」とは言えない。理由が2つある。
- 呼ばれていないスキルの一部は、まだ来ていない状況のために存在している。年に1回のリリース手順が今年まだ来ていないだけかもしれない。
- 本文457万字は呼び出し時にしか読み込まれない。眠っているスキルが払わせているのは
descriptionの254字だけで、これは無視できる大きさではないが、破滅的でもない。
数字が言っているのはむしろこうだ。スキルは「集める」より「書く」ほうが効く。 発火率が7倍違い、呼び出しの過半数を自作が占めている。
スクリプト
スキル名は出さない(社内名が混じるため)。件数と割合だけを出す。
#!/usr/bin/env python3
"""入れてあるスキルと、実際に呼ばれたスキルの差を数える。
出力は件数と割合だけ。スキル名は出さない(社内名が混じるため)。
"""
import os, glob, json, re, collections
HOME = os.path.expanduser("~")
ROOT = os.path.join(HOME, ".claude")
SKIP = {"node_modules", ".venv", "venv", ".next", "dist", "build", ".git",
"__pycache__", "vendor", "target", ".cache", "site-packages", ".terraform"}
def find_repos(root, maxdepth):
out, root = [], os.path.expanduser(root)
def rec(d, depth):
if depth > maxdepth:
return
try:
entries = list(os.scandir(d))
except OSError:
return
if ".git" in {e.name for e in entries}:
out.append(d)
return
for e in entries:
if e.is_dir(follow_symlinks=False) and e.name not in SKIP and not e.name.startswith("."):
rec(e.path, depth + 1)
rec(root, 0)
return out
# --- 入っているもの -----------------------------------------------------
dirs = [p for p in glob.glob(os.path.join(ROOT, "plugins", "**", "skills", "*"), recursive=True)
if os.path.isdir(p)]
dirs += [p for p in glob.glob(os.path.join(ROOT, "skills", "*")) if os.path.isdir(p)]
mds = glob.glob(os.path.join(ROOT, "plugins", "**", "SKILL.md"), recursive=True)
mds += glob.glob(os.path.join(ROOT, "skills", "*", "SKILL.md"))
gnames = collections.Counter(os.path.basename(os.path.dirname(p)) for p in mds)
pnames = set()
for d in set(find_repos("~/git", 4) + find_repos("~", 2)):
for p in glob.glob(os.path.join(d, ".claude", "skills", "*", "SKILL.md")):
pnames.add(os.path.basename(os.path.dirname(p)))
body = desc = 0
sizes, dsizes = [], []
for p in mds:
try:
t = open(p, encoding="utf-8", errors="replace").read()
except OSError:
continue
body += len(t)
sizes.append(len(t))
m = re.search(r"^description:\s*(.+)$", t[:4000], re.M)
if m:
desc += len(m.group(1))
dsizes.append(len(m.group(1)))
print(f"=== 入っているもの")
print(f" skills/ 直下のディレクトリ {len(dirs)}")
print(f" SKILL.md {len(mds)} 本 / ユニーク名 {len(gnames)}"
f" / 同名が複数の出所にある {sum(1 for v in gnames.values() if v > 1)}")
print(f" 本文合計 {body:,} 字 / 1本 中央値 {sorted(sizes)[len(sizes) // 2]:,} 最大 {max(sizes):,}")
print(f" description 合計 {desc:,} 字 = 本文の {desc / body * 100:.1f}%"
f" / 中央値 {sorted(dsizes)[len(dsizes) // 2]} 字")
print(f" プロジェクト側 .claude/skills のユニーク名 {len(pnames)}")
# --- 呼ばれたもの -------------------------------------------------------
fired = collections.Counter()
def walk(o):
if isinstance(o, dict):
if o.get("type") == "tool_use" and o.get("name") == "Skill":
s = (o.get("input") or {}).get("skill")
if s:
yield s.split(":")[-1]
for v in o.values():
yield from walk(v)
elif isinstance(o, list):
for v in o:
yield from walk(v)
files = glob.glob(os.path.join(ROOT, "projects", "**", "*.jsonl"), recursive=True)
for f in files:
with open(f, encoding="utf-8", errors="replace") as fh:
for line in fh:
if '"Skill"' not in line:
continue
try:
j = json.loads(line)
except Exception:
continue
for s in walk(j):
fired[s] += 1
u = set(fired)
print(f"\n=== 呼ばれたもの(ログ {len(files):,} ファイル)")
print(f" Skill 呼び出し {sum(fired.values()):,} 回 / 異なるスキル {len(u)} 種")
print(f" グローバルに入っている {len(gnames)} 種のうち呼ばれた {len(u & set(gnames))}"
f" = {len(u & set(gnames)) / len(gnames) * 100:.1f}%")
print(f" プロジェクト側 {len(pnames)} 種のうち呼ばれた {len(u & pnames)}"
f" = {len(u & pnames) / len(pnames) * 100:.1f}%")
print(f" どちらにも見つからない(組込み等) {len(u - set(gnames) - pnames)} 種")
src = collections.Counter()
for k, v in fired.items():
src["グローバル" if k in gnames else "プロジェクト" if k in pnames else "組込み等"] += v
tot = sum(fired.values())
print(" 呼び出し回数の出所別:")
for k, v in src.most_common():
print(f" {k:14} {v:5} = {v / tot * 100:5.1f}%")
top = fired.most_common()
head = sum(v for _, v in top[:3])
print(f" 上位3種で {head} 回 = {head / tot * 100:.1f}%")
この計測の限界
-
ログに残るのは
Skillツールの明示的な呼び出しだけ。スキルの内容が別の形で参照された場合は数えられない。 - ユーザーが
/<name>と打った場合もツール呼び出しとして記録されるので、自動的に選ばれたのか人間が名指ししたのかを、この数え方では区別していない。上位3種は人間が繰り返し名指ししている可能性が高い。 - 「組込み等」5種は、グローバルにもプロジェクトにも
SKILL.mdが見つからなかったもの。Claude Code に内蔵されているスキルはディスク上の同じ場所には無いため、この分類に落ちる。 - プロジェクト側の35種は
~/git深さ4 と~深さ2 の範囲で見つかったリポジトリのみ。それ以外の場所のリポジトリは数えていない。
関連
129種類入れて4種類しか呼ばれない、という形の「積んだのに使われない」は、指示ファイルでも起きます。ただしそちらは呼ばれないのではなく、読まれた上で守られないので、症状が出ません。
まず何を削るか、層をどう切るか、規約が効かなくなる4つのパターンの見分け方をまとめた本があります。
→ AIコンテキスト設計ガイド ── 規約を配ったのに守られない理由(2,000円)
第3章「規約が効かなくなる4つのパターン」まで無料で読めます。
/compact の後もルートの CLAUDE.md は再注入されるが、サブディレクトリと paths: 付きは再注入されない ── 長いセッションで静かに落ちるのはどちらか、という章です。