1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スキルを129種類インストールして、実際に呼ばれたのは4種類だった

1
Posted at

プラグインを入れるとスキルが付いてくる。マーケットプレイスを足すたびに増える。減らす作業は誰もやらない。

手元の ~/.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: 付きは再注入されない ── 長いセッションで静かに落ちるのはどちらか、という章です。

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?