0
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?

🔍 transcript.jsonlで「トヌクン浪費Skill」を特定する

0
Posted at

月商120䞇の自埋環境を回しおいおも、どのSkillが毎週静かにトヌクンを食い続けおいるかは、誰も教えおくれたせんでした。

Claude Codeの /usage を叩くず、出おくるのは claude-sonnet-4-6: Â¥3,240 ずいうモデル別の合蚈額だけです。「先週より2割高い」はわかる。「どのSkillが原因か」はわからない。そのギャップを埋めるのが usage-breakdown.sh です。transcript.jsonlをPythonでパヌスし、Skill・Agent・MCPサヌバヌごずの呌び出し回数を Counter で集蚈する、106行のシェルスクリプトです。

この蚘事では、そのスクリプトの仕組みず動かし方を実コヌドず実数字で解説したす。

なぜこの仕組みが効くのか

Claude Codeが蚘録しおいるものの正䜓

Claude Codeはセッション䞭の党操䜜を ~/.claude/projects/ 以䞋の .jsonl ファむルぞ逐次曞き出しおいたす。1行1むベントのJSONL圢匏で、1セッション1ファむルです。ファむル名は <project-id>/ ディレクトリ以䞋に䞊びたす。

1レコヌドの骚栌はこうなっおいたす。

{
  "message": {
    "role": "assistant",
    "content": [
      {
        "type": "tool_use",
        "name": "Skill",
        "input": {
          "skill": "pre-completion-self-audit"
        }
      }
    ]
  }
}

message.content[] の䞭に "type": "tool_use" ブロックが䞊ぶ。name フィヌルドが呌び出されたツヌル名です。Bashツヌル、Editツヌル、Skillツヌル、Agentツヌル、MCP呌び出し——すべおこの圢匏で蚘録されおいたす。

ここに気づいたずき、「これをCounterで集蚈すれば䞞芋えだ」ず思いたした。Skillツヌルの堎合は input.skill にスキル名が入っおおり、Agentツヌルなら input.subagent_type、MCPサヌバヌなら mcp__<server>__<tool> ずいうツヌル名の呜名芏則から __ 分割でサヌバヌ名を抜出できたす。構造が䞀貫しおいるので、パヌサヌは驚くほどシンプルに曞けたす。

/usage が教えおくれないこず

Claude Codeの /usage コマンドが出力するのは、期間内のモデル別コスト集蚈です。

Model                    Cost
claude-sonnet-4-6        ¥3,240
claude-opus-4-8          ¥  892

これはこれで有甚ですが、コストの内蚳が芋えない。どのセッションで、䜕のSkillが、䜕回呌ばれおトヌクンを䜿ったのかがわかりたせん。

usage-breakdown.sh はトヌクン量ではなく呌び出し回数を集蚈したす。トヌクン数の正確な集蚈にはAPIレスポンスの usage オブゞェクトを拟う必芁がありたすがスクリプト内コメント: token 数は usage オブゞェクト集蚈が必芁だが、たずは call count で代替、呌び出し回数だけでも「䜕が倚いか」の茪郭は十分に芋えたす。100回呌ばれおいるSkillず1回しか呌ばれおいないSkillでは、トヌクン消費のオヌダヌが違いたす。

mtimeりィンドりで「盎近N日」に絞る

党セッション分を集蚈するず叀いログが混入しお比范がぶれたす。スクリプトはファむルの mtime で時間窓を切りたす。

cutoff_ts = (now - datetime.timedelta(days=days)).timestamp()

for path in glob.glob(f"{tr_dir}/*.jsonl"):
    mtime = os.path.getmtime(path)
    if mtime < cutoff_ts: continue

デフォルトは 7d、匕数で 30d や 14d に倉えられたす。--short フラグを枡すず statusline 向けの1行サマリだけ出たす。

5015 tool_use across 39 sessions (7d)

これをmacOSのステヌタスバヌりィゞェットに流せば、週をたたいで積み䞊がっおいく総コヌル数が垞時芋えたす。

Counterで集蚈する4本の軞

スクリプトが集蚈するのは4本のカりンタヌです。

skill_calls    = collections.Counter()   # Skillツヌル → input.skill
agent_calls    = collections.Counter()   # Agentツヌル → input.subagent_type
mcp_calls      = collections.Counter()   # mcp__<server>__* → サヌバヌ名
plugin_skill_calls = collections.Counter()  # plugin:skill 圢匏のnamespace

tool_calls が党ツヌルのカりンタヌで、䞊蚘4本はその内蚳です。Skillの䞭でも plugin:skill-name 圢匏のものはnamespaceごずに束ねる——この粒床が実際に圹立ちたす。superpowers:brainstorming ず superpowers:research を別々に数えおも、superpowers pluginが重いずいう事実しか知りたくない局面があるからです。

刀定ロゞックは単玔な分岐です。

if name == "Skill":
    skill_name = inp.get("skill", "?")
    if ":" in skill_name:
        plugin_skill_calls[skill_name.split(":", 1)[0]] += 1
    skill_calls[skill_name] += 1
elif name == "Agent":
    st = inp.get("subagent_type", "?")
    agent_calls[st] += 1
elif name.startswith("mcp__"):
    parts = name.split("__")
    if len(parts) >= 2:
        mcp_calls[parts[1]] += 1

ルヌプは1ファむルを1行ず぀読んで json.loads するだけ。파싱゚ラヌは try/except で無芖。党郚で30行足らずの集蚈コアです。

党䜓の流れ

スクリプトの凊理フロヌをアスキヌ図で瀺したす。

~/.claude/projects/
  └─ -Users-<username>/
       ├─ abc123.jsonl  ─┐
       ├─ def456.jsonl   ├─► mtime >= cutoff? ─NO─► スキップ
       └─ ghi789.jsonl  ─┘        │
                                  YES
                                   │
                            jsonl 1行ず぀読む
                                   │
                            message.content[]
                                   │
                     type=="tool_use" のブロック抜出
                                   │
                    ┌──────────────┌──────────────┐
                    │              │              │
                 name==           name==       name starts
                "Skill"          "Agent"      "mcp__"
                    │              │              │
              input.skill    subagent_type   __split[1]
                    │              │              │
               skill_calls    agent_calls    mcp_calls
                    │              │              │
                    └──────────────┮──────────────┘
                                   │
                         Counter.most_common(10)
                                   │
                            stdout ぞ出力

スクリプトの構成党106行

usage-breakdown.sh は3぀のパヌトに分かれおいたす。

Part 1: シェル局1〜16行

匕数パヌス・トランスクリプトディレクトリの存圚確認・Pythonスクリプトぞの受け枡しを担いたす。

#!/usr/bin/env bash
set -uo pipefail
ARG="${1:-7d}"
TR_DIR="$HOME/.claude/projects/-Users-<username>"
[ -d "$TR_DIR" ] || { echo "(no transcript dir)"; exit 0; }

python3 - "$TR_DIR" "$ARG" <<'PY'

<<'PY' ... PY のヒアドキュメントでPythonコヌドをむンラむンに埋め蟌んでいたす。倖郚の .py ファむルを眮かずに1ファむルで完結させるための構造です。むンストヌル䞍芁・パス解決なし・どこから呌んでも動く、ずいう運甚䞊の利点がありたす。

Part 2: 匕数解析ず時間窓の蚈算18〜28行

SHORT = arg == "--short"
days = int((arg if arg.endswith("d") else "7d").rstrip("d"))
cutoff_ts = (now - datetime.timedelta(days=days)).timestamp()

--short フラグを分岐させた埌、7d → 7 の数倀倉換。endswith("d") チェックで 30d 圢匏ず玔粋な敎数の䞡方を受け付けたす。

Part 3: ファむル走査ず集蚈コア37〜73行

glob.glob でJSONLファむル䞀芧を取埗し、mtimeフィルタを通過したものだけを開きたす。1行ず぀ json.loads → message.content のリスト走査 → tool_use ブロック抜出 → 4本のCounterぞ加算、ずいうパむプラむンです。

for path in glob.glob(f"{tr_dir}/*.jsonl"):
    mtime = os.path.getmtime(path)
    if mtime < cutoff_ts: continue
    total_files += 1
    with open(path, "r", encoding="utf-8", errors="replace") as f:
        for line in f:
            rec = json.loads(line)
            msg = rec.get("message", {})
            content = msg.get("content")
            if not isinstance(content, list): continue
            for block in content:
                if block.get("type") != "tool_use": continue
                name = block.get("name", "")
                inp = block.get("input") or {}
                tool_calls[name] += 1
                # ... 4本の分岐

errors="replace" を枡しおいるのは、皀に含たれる䞍正バむトでファむル党䜓の読み蟌みが止たるのを防ぐためです。

Part 4: 出力75〜106行

--short なら1行サマリ、通垞モヌドなら most_common(10) で䞊䜍10件をセクション別に出力したす。

print(f"=== usage breakdown (last {days}d, {total_files} transcripts) ===")
print(f"\ntotal tool_use: {sum(tool_calls.values())}")

if skill_calls:
    print(f"\n--- top skills ({len(skill_calls)} unique) ---")
    for sk, n in skill_calls.most_common(10):
        print(f"  {n:>5}  {sk}")

{n:>5} の右寄せフォヌマットで、桁数が違っおも瞊棒が揃いたす。タヌミナルで芋たずきの芖認性のための现工です。

実際に7日分を流した出力

=== usage breakdown (last 7d, 39 transcripts) ===

total tool_use: 5015

--- top tools ---
   3656  Bash
    508  Edit
    304  Read
    240  Write
     37  Monitor
     35  ToolSearch
     23  AskUserQuestion
     20  TaskUpdate
     19  mcp__plugin_playwright_playwright__browser_take_screenshot
     16  mcp__claude-in-chrome__navigate

--- top skills (3 unique) ---
      3  artifact-design
      1  dataviz
      1  claude-api

--- top agents (1 unique) ---
      1  code-reviewer

--- top MCP servers (4 unique) ---
     79  plugin_playwright_playwright
     45  claude-in-chrome
     15  claude_ai_Google_Calendar
      2  claude_ai_Gmail

7日間で39セッション、ツヌル呌び出し総数5,015回。Bashが3,656回72.9%でダントツ銖䜍、Editが508回で続きたす。SkillずAgentは思ったより少ない——この数字が䜕を意味するかは次の章で掘りたす。30日分に広げるず絵が倉わりたす。

=== usage breakdown (last 30d, 203 transcripts) ===

total tool_use: 21215

--- top agents (7 unique) ---
     94  general-purpose
     22  Explore
      6  reviewer
      ...

--- top MCP servers (5 unique) ---
   1571  claude-in-chrome
     81  plugin_playwright_playwright
     54  computer-use

30日スパンで芋るず claude-in-chrome が1,571回——週換算で玄366回です。Agentでは general-purpose が94回週換算23回。7日窓では芋えにくかった定垞的な重さが、30日窓で浮かび䞊がりたす。

このギャップ——短窓では芋えず、長窓で初めお芋える重さ——が、定期実行される自動化がらみのチュヌニングポむントになりたす。

実装の詳现——「なぜそう曞くか」を掘る

二重 try/except の蚭蚈意図

スクリプトの集蚈コア37〜73行を読むず、try/except が2局になっおいるこずに気づきたす。

for path in glob.glob(f"{tr_dir}/*.jsonl"):
    try:
        mtime = os.path.getmtime(path)
        if mtime < cutoff_ts: continue
        total_files += 1
        with open(path, "r", encoding="utf-8", errors="replace") as f:
            for line in f:
                try:
                    rec = json.loads(line)
                except: continue  # ← 内偎
                ...
    except Exception:
        continue  # ← 倖偎

内偎の try/except は json.loads だけを包みたす。JSONLは1行1レコヌドの圢匏なので、1行がパヌス倱敗しおも残りの行は読み続けられたす。continue で次の行ぞ飛ぶだけです。

倖偎の try/except Exception はファむル単䜍の䟋倖を受けたす。パヌミッション゚ラヌ、ファむルが削陀された、mtime取埗がコケた——どれが起きおも continue でそのファむルをスキップし、次のファむルぞ進みたす。total_files のカりントが倖偎の try の䞭にあるのはそのためです。ファむルを正垞に開けた時だけカりントしたい。

2局にする理由は、粒床の違いです。「このファむルが読めない」ず「この行がJSONでない」は別の障害で、凊理の継続範囲が違いたす。1局にたずめおファむルごず continue しおいたら、先頭行が壊れた1ファむルで残りの䜕千行かを䞞ごず捚おるこずになりたす。

isinstance ガヌドの培底

47行目には、䞀芋くどく芋えるガヌドが入っおいたす。

msg = rec.get("message", {}) if isinstance(rec.get("message"), dict) else {}

rec.get("message", {}) だけでよさそうに芋えたすが、これは足りたせん。transcript.jsonlには "message": null ずいうレコヌドが存圚したす。null はJSONずしおは正圓なので json.loads を通過したすが、Pythonでは None になりたす。{}.get("content") は問題ないですが、None.get("content") は AttributeError で萜ちたす。isinstance で dict であるこずを確認しおから .get() を呌ぶ、ずいうパタヌンを入れないず、null レコヌドを螏むたびに内偎の except が拟い続けたす。

同じ理由で54行目にも防埡がありたす。

inp = block.get("input") or {}

block.get("input") は None を返すこずがありたす。None or {} は {} になるので、以降の inp.get("skill", "?") が安党に動きたす。if inp is None: inp = {} ず曞くより短く、「Noneず空dictの䞡方に察しお空dictを䜿いたい」ずいう意図が1行で䌝わりたす。

さらに50行目。

for block in content:
    if not isinstance(block, dict): continue

content は list だず確認枈みですが、その芁玠が党お dict ずは限りたせん。Claude Codeのtranscriptを眺めるず、content が文字列のリストになっおいるレコヌドも皀に出おきたすテキストブロックずツヌルブロックが混圚しおいる堎合の䞀郚。芁玠ごずに isinstance(block, dict) を確認しお、dictでなければスキップするのが堅牢です。

ヒアドキュメントのクォヌトが呜綱

16行目を泚意深く芋おください。

python3 - "$TR_DIR" "$ARG" <<'PY'

<<'PY' のシングルクォヌトが絶察に必芁です。<<PYクォヌトなしにしおしたうず、ヒアドキュメントの内偎でシェルの倉数展開が走りたす。Pythonコヌド䞭に $tr_dir ずいう曞き方が䞀箇所でもあれば、シェルが展開しようずしお意図しない文字列に化けたす。f"{tr_dir}/*.jsonl" はPythonのf-stringなので$はありたせんが、$1や${HOME}に芋える文字列があるず壊れたす。<<'PY'のようにデリミタをクォヌトで囲むず、ヒアドキュメント内の展開が完党に無効化され、Pythonコヌドがそのたたの文字列ずしおpython3の暙準入力に枡されたす。

ヒアドキュメントでPythonをむンラむンに埋める利点は、1ファむルで完結するこずです。スクリプトをどこかのディレクトリに眮いおPATHを通しおおけばそれだけで動く。~/.claude/scripts/usage-breakdown.sh をlaunchdから呌ぶ堎合、Pythonファむルのパスを別途管理する必芁がありたせん。倖郚ファむル䟝存は、そのファむルが消えたり移動した瞬間に黙っお壊れたす。

plugin namespace の分離ロゞック

59〜61行目のブロックは小さいですが、実際に䜿っおみるず䟡倀がわかりたす。

if ":" in skill_name:
    plugin_skill_calls[skill_name.split(":", 1)[0]] += 1
skill_calls[skill_name] += 1

split(":", 1) の 1 が重芁です。最倧分割数を1に限定するこずで、expo:eas-hosting なら ["expo", "eas-hosting"] になりたすが、仮に expo:eas:hosting ずいう圢のスキル名があっおも ["expo", "eas:hosting"] になり、namespace郚分だけを正確に取り出せたす。

plugin_skill_calls ず skill_calls の䞡方にカりントを入れおいるのは集蚈の軞を分けるためです。skill_calls は個別スキル名を、plugin_skill_calls はnamespaceを集蚈したす。週次レポヌトで「expoプラグむンを合蚈12回䜿った」ずいう束ねた数字ず、「expo:eas-hostingが5回、expo:expo-upgradeが4回」ずいう内蚳を、どちらも手元で匕けたす。

--short フラグずステヌタスバヌ統合

--short モヌドが返す1行は、macOSのステヌタスバヌりィゞェットxbar、Übersicht等から盎接呌んで衚瀺するためのものです。

5015 tool_use across 39 sessions (7d)

launchdのplistで5分おきにスクリプトを走らせ、結果を /tmp/usage-short.txt に曞き出しお、りィゞェットがそれを読むずいう構成です。りィゞェット偎でファむルを読むだけなので、Claude Codeのセッション䞭に定期起動しおも競合したせん。--short フラグがなければ出力が10行以䞊になり、りィゞェットに埋め蟌むには長すぎたす。甚途別に出力圢匏を切り替えるオプションを最初から蚭蚈に入れおおくず、埌からはたりたせん。


私が詰たった話

詰たり①errors="replace" を入れおいなかったら党ファむルが通らなかった

最初のバヌゞョンには errors="replace" がありたせんでした。

with open(path, "r", encoding="utf-8") as f:  # ← errorsなし

これで走らせるず、䞀郚のtranscriptファむルで UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position ... が出おスクリプトが止たりたす。ファむルを倖偎の try/except Exception で囲んでいたので止たりはしたせんが、そのファむル党䜓が continue でスキップされたす。

原因はtranscriptにPNG画像のbase64デヌタが含たれおいる堎合です。Claude Codeでスクリヌンショットを䜿うセッションでは、画像がtranscript.jsonlにbase64゚ンコヌドで曞き蟌たれたす。base64文字列自䜓はASCIIなのでUTF-8で読めたすが、皀にバむナリが混入した䞍正なJSONLが生成されるこずがありたす。errors="replace" を枡すず、読めないバむトを U+FFFDREPLACEMENT CHARACTERに眮換しお読み進めたす。JSON倀の䞭の壊れたバむトが眮換文字になっおも、json.loads のパヌス察象はその行党䜓なので、構造が厩れおいなければパヌスは通りたす。構造が厩れおいれば内偎の except: continue が拟いたす。

errors="replace" は情報の欠損を蚱容する代わりに、ファむル党䜓の読み蟌みを完走させる遞択です。usage集蚈では1バむトの粟床より「党ファむルを走査できたか」の方が倧事なので、この刀断は正しかったです。

詰たり②mtime窓が「停陜性」を倧量に返した

スクリプトをしばらく䜿っおいるず、盎近7日の集蚈なのに「明らかに叀いセッションのデヌタが混入しおいる」ず気づく日がありたした。出力の総件数が普段の3〜4倍に膚らんでいお、しかも内容を芋るず2週間前のやりずりが含たれおいたす。

原因はバックアップ゜フトのリストアでした。Time Machineやrsyncでホヌムディレクトリを同期するず、~/.claude/projects/ 以䞋のファむルが䞊曞きコピヌされたす。コピヌはファむルの䜜成時刻を倉えたすが、mtimeも「コピヌした時刻」になりたす。䞭身は叀いセッションのtranscriptなのに、mtimeは今日の日時です。

cutoff_ts = (now - datetime.timedelta(days=days)).timestamp()
for path in glob.glob(f"{tr_dir}/*.jsonl"):
    mtime = os.path.getmtime(path)
    if mtime < cutoff_ts: continue

mtimeフィルタは「このファむルを最埌に曞き換えた時刻」を芋るので、コピヌによっおmtimeが曎新されたファむルはすべお「最新」扱いになりたす。164ファむルが䞀括で停陜性になり、5窓連続で「新芏れロなのに数字が膚らむ」ずいう状態が続きたした。

根本的な解決は、transcript内の 日時: フィヌルドをパヌスしお実際のセッション時刻で刀定するこずです。ただしそれは実装コストが䞊がるため、珟圚は30日窓で集蚈しお長期トレンドを芋るずいうワヌクアラりンドを採甚しおいたす。䞀括コピヌで停陜性が混入しおも、30日分の総量の䞭では統蚈的な倖れ倀に収たりたす。7日窓を䜿う堎合は、バックアップ盎埌の数日は数字を信甚しないずいう運甚ルヌルを自分で持぀しかありたせん。

この問題に気づかないたた「先週はSkillが100回呌ばれた」ずいう数字を信じおplistのStartIntervalを倉えおいたら、実際には倉曎が䞍芁な堎所を觊っおいたかもしれたせん。ツヌルの出力を鵜呑みにする前に「この数字の取埗方法は䜕に圱響されるか」を䞀床疑う、ずいうクセが必芁だず孊びたした。

詰たり③json.loads の䟋倖をブランケットキャッチしたら副䜜甚が芋えなくなった

内偎の except: continue は except Exception: continue ではなく、裞の except: です。これは BaseException を含む党䟋倖を捕たえたす。KeyboardInterrupt も SystemExit も党郚飲み蟌みたす。

最初はこれで良いず思っおいたしたが、デバッグ䞭に Ctrl+C でスクリプトを止めようずしたら止たらないこずがありたした。KeyboardInterrupt が内偎の except: に捕たっお continue されおいたのです。ルヌプが進んで次のファむルに入れば倖偎の try も except も越えられたせんでした。

盎し方は内偎を except (json.JSONDecodeError, ValueError): continue に絞るこずです。json.loads が倱敗する理由は実質的に JSONDecodeErrorPython 3.5以降か、皀に ValueError だけです。それ以倖の䟋倖KeyboardInterrupt を含むは内偎で捕たえず、倖偎の except Exception に䌝播させるか、ナヌザヌに届かせたす。珟圚のコヌドでは except: のたたになっおおり、これは今でも改善の䜙地があるず思っおいたす。実際の運甚では問題が出たこずはありたせんが、「Ctrl+C が効かない可胜性がある」ずいう挙動は知っおおくべきです。

詰たり④most_common の返り倀を゜ヌトし盎そうずしお二床手間だった

集蚈結果を出す段階で、skill_calls.most_common(10) の返り倀をさらに sorted() で䞊べ替えようずしたこずがありたした。アルファベット順でも出したかったからです。

# やっおしたったパタヌン
for sk, n in sorted(skill_calls.most_common(10), key=lambda x: x[0]):
    print(f"  {n:>5}  {sk}")

これは most_common(10) を先に取っおから名前順に䞊べ盎すので、「党䜓の䞊䜍10件をアルファベット順にした」ずいう結果になりたす。䞀芋問題なさそうですが、「頻床11䜍のSkillが名前順で䞊䜍に来るはずなのに出ない」ずいう混乱が起きたす。

most_common() の圹割はカりンタヌを頻床降順で返すこずです。匕数 10 は頻床䞊䜍10件だけに絞る。その埌で゜ヌトするなら、most_common() に匕数を枡さずに党件取っおから゜ヌトするか、最初から目的に合った別のデヌタ構造を䜿うべきでした。

この件は1行の盎し方で枈みたしたが、「CollectionsのCounterはどう動いおいるか」を理解せずに䜿っおいたこずが問題でした。Counter は内郚的には dict のサブクラスで、most_common() はヒヌプを䜿った O(n log k) の操䜜です。100䞇件あっおも䞊䜍10件は高速に返りたす。逆に党件取埗から自力で゜ヌトするのは O(n log n) になりたす。芏暡が小さければ誀差ですが、将来transcriptが増えたずきに差が出たす。

詰たり⑀MCP サヌバヌ名が空文字になるケヌスがあった

mcp__<server>__<tool> の圢匏からサヌバヌ名を取り出す郚分です。

elif name.startswith("mcp__"):
    parts = name.split("__")
    if len(parts) >= 2:
        mcp_calls[parts[1]] += 1

if len(parts) >= 2: のガヌドを倖しおいた初期バヌゞョンで、mcp__ だけのツヌル名parts が ["mcp", ""]が混入したずき、parts[1] が空文字列になっお mcp_calls[""] += 1 が積み䞊がっおいたした。出力に " 23 " ずいう空の゚ントリが出おきお最初は䜕だかわかりたせんでした。

空ツヌル名が発生する原因は䞍完党なレコヌドです。たれにMCPのレスポンスが䞭断されおツヌル名が途䞭で切れたtranscriptが生成されたす。len(parts) >= 2 のガヌドで匟くのが䞀番シンプルな察凊で、これを入れたら空゚ントリは消えたした。さらに蚀えば parts[1] が空文字の堎合もスキップしたいので、本来は if len(parts) >= 2 and parts[1]: ずすべきでした。珟圚のコヌドでは parts[1] が空の堎合は匟かれずに mcp_calls[""] になりたすが、出力では most_common(10) に含たれるほどの件数にはなっおいないので実害はありたせん。

぀たずきポむント

前の章で詳述した5件UnicodeDecodeError・mtimeの停陜性・裞のexcept:・most_commonの二床手間・空MCPサヌバヌ名以倖に、実際の運甚でぶ぀かった现かい眠をたずめたす。

  • TR_DIRがハヌドコヌドされおいお他人の環境で動かない。スクリプト13行目は TR_DIR="$HOME/.claude/projects/-Users-yourname" のようにナヌザヌ名が埋たっおいたす。別アカりントや別マシンに持ち蟌もうずしお動かなかった。-Users-$(whoami) で解決できたすが、そもそもディレクトリ名のスラッシュがハむフン眮換されおいる呜名芏則を知らないず原因が぀かめたせん。

  • --short フラグず日数指定が排他になっおいる。匕数は $1 の1぀だけを受け取るので usage-breakdown.sh --short 30d のように曞いおも 30d は無芖されたす。「30日分の1行サマリが欲しい」ずいう組み合わせが玠盎に曞けない。実際は usage-breakdown.sh 30d の出力の1行目だけ取るか、スクリプト偎を $1 / $2 察応に改造するしかありたせん。

  • launchdから呌ぶず python3 のPATHが通っおいない。launchdで起動したスクリプトは /usr/bin:/bin:/usr/sbin:/sbin だけのPATHで動きたす。homebrewやnvmが入れたpython3は /usr/local/bin や /opt/homebrew/bin 等にあるため、玠のlaunchd plistでは python3: command not found になりたす。plistの <key>EnvironmentVariables</key> に <key>PATH</key> を明瀺するか、スクリプトの先頭で /usr/bin/env python3 でなく絶察パス/opt/homebrew/bin/python3を指定する必芁がありたす。

  • StartInterval倉曎埌にplistを再ロヌドしおいなかった。~/Library/LaunchAgents/com.lily.usage-breakdown.plist の StartInterval を 3005分から 180030分に盎しおも、launchctl unload + launchctl load を忘れるず叀い蚭定のたた動き続けたす。倉曎が効いおいるかは launchctl list com.lily.usage-breakdown で LastExitStatus ず次回起動時刻を確認するのが確実です。ファむルだけ曞き換えた気になっお、実は倉わっおいなかった——ずいう状態に数日気づかなかったこずがありたす。

  • glob.glob の返り順が保蚌されない。ファむルシステムによっお䞊び順が倉わりたす。集蚈結果は合蚈が同じでも、ファむルを凊理する順序が倉わるず total_files のカりントには圱響したせんカりンタヌは环積なのでが、デバッグで特定のファむルが䜕番目に凊理されおいるか远うずきに順序が毎回倉わるので混乱したす。確実に順序を固定したいなら sorted(glob.glob(...)) ず明瀺する方が安党です。

  • most_common(10) の䞊限が固定で、Skillが増えるず䞋䜍が芋えなくなる。環境にむンストヌルされおいるSkillが50を超えるず、10䜍以䞋のSkillが集蚈から芋えなくなりたす。週次チュヌニングの目的なら「100回以䞊のもの」ずいう閟倀で絞る方が珟実的です。珟圚のコヌドは出力数がハヌドコヌドなので、環境が育぀に぀れお芋たい情報が切り捚おられたす。

  • 7日窓の数字だけを芋おplistを倉曎しおしたった。7日間のトランスクリプト数が少ない週連䌑明けなどは絶察数が䜎く芋えたす。「claude-in-chrome が20回しか呌ばれおいない」は、普段が140回の週から芋るず「今週たたたた少なかっただけ」です。30日窓ずの比范を必ずセットにしないず、倖れ倀で刀断しお䞍芁なチュヌニングをしおしたいたす。

  • content がリストでなく文字列になっおいるレコヌドのガヌドを忘れた。isinstance(content, list) のチェックは入っおいたすが、最初のバヌゞョンでは msg.get("content") だけで枈たせおいたために文字列が入っおきたずき for block in content: がcharの反埩になっおしたいたした。1文字ず぀ isinstance(block, dict) を刀定しお党郚萜ずすので実害はありたせんでしたが、ルヌプ回数が無駄に増えおファむルサむズが倧きいtranscriptで䜓感できるほど遅くなりたした。

  • スクリプトの出力を保存しおいなかったので時系列比范ができなかった。usage-breakdown.sh 7d を手で叩いお眺めるだけでは、先週より増えたか枛ったかが蚘憶頌みになりたす。launchdで週1回 /tmp/usage-weekly-$(date +%Y-%m-%d).txt に曞き出すように倉えおから、「先月はMCPが週550回だったのが今月は280回に半枛した」ずいう比范が客芳的にできるようになりたした。

  • Skill名に:を含む堎合のsplitがplugin_skill_callsにだけ入る意図を誀読した。plugin_skill_calls ず skill_calls の䞡方にカりントを入れおいるのは集蚈軞を分けるためですが、最初はバグだず思っお skill_calls ぞの加算を消したした。結果ずしお個別スキル名の集蚈が党お ? になり、「Skillが呌ばれおいるのに名前が党郚䞍明」ずいう出力が出お混乱したした。コヌドを読むずきは刀定分岐の elif でなく if が䜿われおいる理由を確認するのが倧切です。


ベストプラクティス

1. ヒアドキュメントのデリミタは必ずシングルクォヌトで囲む

<<'PY' ず曞かないずPythonコヌド内に $ が含たれるずきf-stringや $HOME に芋える郚分でシェル展開が走りたす。<<PY ず曞いお動いおいるうちはたたたたで、$tr_dir ずいう倉数名を増やした瞬間に壊れたす。むンラむンPythonスクリプトを扱う堎合のルヌルずしお固定しおおきたす。

2. open() は垞に errors="replace" を枡す

ログ・transcriptの類はバむナリが混入する可胜性がありたすスクリヌンショットのbase64、倖郚コンテンツのコピヌ等。errors="replace" はデヌタの粟床よりも「党ファむルを走査できたか」を優先する集蚈系の凊理に向いおいたす。完走率を䞊げるための䞀手です。

3. try/except は粒床ごずに2局に分ける

「ファむル単䜍の倱敗」ず「1行単䜍の倱敗」は継続範囲が違いたす。倖偎はファむルスキップ、内偎は行スキップ——この2局構造を最初から蚭蚈に入れるず、デバッグ時に「䜕行目が壊れおいるか」ず「どのファむルが壊れおいるか」を分離しお远えたす。

4. isinstance ガヌドは芋た目がくどくおも省略しない

rec.get("message", {}) は "message": null を匟けたせん。isinstance(rec.get("message"), dict) の1行が、NoneでAttributeErrorを出す経路を完党に塞ぎたす。transcript.jsonlは仕様倖の倀が普通に含たれるため、型を信頌せず毎回確認するのが安党です。

5. Noneず空dictを同時に察凊する or {} パタヌン

inp = block.get("input") or {}

if inp is None: inp = {} より短く、意図が明確です。or 挔算子はfalsyな倀None・空dict・空文字列を党お {} に眮き換えるので、埌続の .get() を安党に呌べたす。

6. 集蚈軞を耇数のCounterに持぀

skill_calls個別名ず plugin_skill_callsnamespaceの2本を持぀こずで、「expoプラグむン党䜓が重い」ずいう䞊䜍の芖点ず「expo:eas-hostingが5回」ずいう個別の芖点を同じ実行で取り出せたす。埌から軞を远加するより、蚭蚈段階で「䜕単䜍で芋たいか」を敎理しおおく方が楜です。

7. 7d窓ず30d窓を必ずセットで䜿う

7日窓はノむズに敏感です。連䌑・バックアップ埩元・倧型䜜業週のどれかが混入するず倖れ倀になりたす。30日窓を䞊べお「定垞的に倚いか、今週だけ倚いか」を刀定しおからplistに手を入れる——この2りィンドり運甚がチュヌニング刀断のブレを防ぎたす。

8. --short フラグで甚途別出力を蚭蚈段階から分ける

人間が読む詳现モヌドず、りィゞェット・ログファむルに食わせる1行モヌドを最初から分けおおくず、同䞀スクリプトを耇数の文脈で䜿い回せたす。出力圢匏を埌から远加しようずするずコヌドの分岐が増えお芋通しが悪くなりたす。

9. 週次ログをファむルに曞き出しお時系列を残す

~/.claude/scripts/usage-breakdown.sh 7d > /tmp/usage-$(date +%Y-%m-%d).txt

これをlaunchdで毎週月曜に走らせるだけで、4週前ずの比范が diff で芋られたす。「最近コストが䞊がった気がする」ずいう感芚を数字で怜蚌するずき、手元にログがあるかないかで話が党く倉わりたす。

10. plist倉曎埌は必ず unload → load の2ステップを螏む

launchctl unload ~/Library/LaunchAgents/com.lily.usage-breakdown.plist
launchctl load  ~/Library/LaunchAgents/com.lily.usage-breakdown.plist

ファむルを曞き換えただけでは反映されたせん。launchctl list com.lily.usage-breakdown の "NextScheduledFire" を芋お次回起動時刻が新しい StartInterval に沿っおいるかを確認する習慣を぀けたす。

11. launchdのplistにPATHを明瀺する

<key>EnvironmentVariables</key>
<dict>
  <key>PATH</key>
  <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>

homebrewやnvmのツヌルを䜿うスクリプトをlaunchdから動かす堎合、これがないず command not found で黙っお倱敗したす。/usr/bin/env python3 だけでは足りない環境がありたす。

12. MCPサヌバヌ名の空文字ガヌドは parts[1] たで確認する

if len(parts) >= 2 and parts[1]:
    mcp_calls[parts[1]] += 1

len(parts) >= 2 だけでは mcp__ → ["mcp", ""] の空文字が通り抜けたす。and parts[1] を足すこずで、空キヌが most_common に混入するのを防げたす。

13. スクリプトは1ファむル完結で蚭蚈する

倖郚Pythonファむルぞの䟝存を持぀ず、ファむルが移動・削陀されたずきに黙っお壊れたす。<<'PY' ... PY のむンラむンヒアドキュメント方匏は、スクリプト1本をどこに眮いおもそのたた動く自己完結型にする最も手軜な方法です。

14. 集蚈結果の数字を「取埗方法ぞの信頌」ず切り離しお読む

mtime窓の話がその兞型です。数字が出おも、その数字の根拠——どのファむルのどのフィヌルドを読んでいるか——を理解しおいないず、誀った前提でチュヌニングを進めたす。スクリプトの動䜜を1床手で远っお「バックアップ埌に停陜性が出る」ずいう限界を把握しおから、定垞運甚に乗せる順序が倧切です。


たずめ

Claude Codeの /usage が出すのは「モデル別の合蚈額」だけです。106行の usage-breakdown.sh はそのギャップを埋めるために曞いたスクリプトで、transcript.jsonlをPythonでパヌスしおSkill・Agent・MCPサヌバヌごずの呌び出し回数をCounterで集蚈したす。

実際に7日分を流すず Bash: 3,656回72.9% ずいう実態が芋え、30日分たで広げるず claude-in-chrome: 1,571回週366回換算 ずいう定垞的な重さが浮かびたす。この数字を基に、週換算100回以䞊のコンポヌネントを特定しおplistのStartIntervalを調敎する——それが本皿の手順党䜓のゎヌルでした。

぀たずきポむントをならべるず倚く芋えたすが、党お「実コヌドを読んだ埌でしか気づけなかった萜ずし穎」です。スクリプトの30〜50行目の集蚈コアを1床手で読んで動䜜を远えば、それだけで半分は予防できたす。残りはlaunchdずの組み合わせ特有の環境䟝存で、PATHずunload/loadを抌さえれば片付きたす。

月商120䞇の自埋環境を支えおいるのは、個々のSkillやMCPサヌバヌの賢さだけでなく、どのコンポヌネントがどれだけ動いおいるかを数字で芋られる仕組みが垞時回っおいるこずです。蚈枬できないものは改善できたせん。手元のtranscript.jsonlに同じデヌタが既に溜たっおいるので、今日䞭に動かせたす。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。

📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍

皆さんの ❀ やシェアが励みになりたす

0
0
0

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
0
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?