Claude Code (WSL) のトークン使用量が異常に多かった原因をセッションログ解析で特定した話

概要

今回はClaude Code(WSL)のトークン使用量が異常に多かった原因を、ローカルのセッションログを解析して特定し、対策するまでの話を紹介していきます。

WSL環境でClaude Codeを使って作業していたところ、いつもより明らかにトークン消費が多いことに気づいたのがきっかけです。
Claude Codeは各セッションを.jsonl形式でローカルに保存しているので、これを直接Pythonで解析すれば「何が原因で消費が増えているか」が具体的にわかるはずだと考えました。

結果的に、原因は1回の巨大な読み込みそのものよりも「長時間・多ターンの1セッション」自体が最大のコスト要因になっていたという、少し意外な構造でした。
実際にどんなコマンドが原因だったか、どう対策したかまで含めて共有していきます(^^b

それではやっていきましょう!

目次

きっかけ:WSL環境でトークン消費量に違和感

発端は単純で、「WSL環境で作業していたらトークン使用量が異常に多い」という違和感からでした。

普段の作業と同じような感覚で使っていたはずなのに、明らかに消費ペースが速い。
体感だけで判断すると誤りやすいので、まずは実際のデータを見て確認することにしました。

Claude Codeは各セッションのやり取りを.jsonl形式でローカルに逐次保存しているので、これを直接解析すれば感覚ではなく数字で原因を特定できます。

調査方法:セッションログ(.jsonl)をPythonで集計

Claude Codeのセッションログは、~/.claude/projects/以下にプロジェクトごとのディレクトリが作られ、その中にセッション単位で.jsonlファイルが保存されています。

各行がJSONのメッセージレコードになっていて、Claudeからの応答メッセージにはusageというフィールドが含まれています。
ここにinput_tokens(入力トークン)・output_tokens(出力トークン)・cache_read_input_tokens(キャッシュ読込トークン)・cache_creation_input_tokens(キャッシュ生成トークン)が記録されているので、これをPythonで集計しました。

analyze_usage.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json
from pathlib import Path

totals = {"input": 0, "output": 0, "cache_read": 0, "cache_creation": 0}

for path in Path.home().glob(".claude/projects/**/*.jsonl"):
for line in path.read_text(encoding="utf-8").splitlines():
record = json.loads(line)
usage = record.get("message", {}).get("usage")
if not usage:
continue
totals["input"] += usage.get("input_tokens", 0)
totals["output"] += usage.get("output_tokens", 0)
totals["cache_read"] += usage.get("cache_read_input_tokens", 0)
totals["cache_creation"] += usage.get("cache_creation_input_tokens", 0)

print(totals)

セッション単位・ツール呼び出し単位で集計すると、「何がどれだけトークンを食っていたか」が可視化できます。
実際にやってみると、想像していたのとは違う原因が見えてきました。

判明した原因(構造面):毎ターン全履歴が再送信される仕組み

まず構造面での原因です。

Claude Codeは会話が続く限り、それまでの全履歴を毎ターン再送信します。
プロンプトキャッシュ経由なので単価自体は割安になっているものの、使用量(トークン数)としては毎ターン計上される仕組みです。

プロンプトキャッシュはコストを下げる仕組みであって、トークン使用量そのものをゼロにするわけではありません。
「単価が安い=いくら再送信しても問題ない」ではない、という点は覚えておくとよいです。

このため、次のような傾向が生まれます。

  • セッションが長引く(ターン数が増える)ほど、再送信コストが線形〜それ以上に積み上がる
    • 会話が長くなるほど、1ターンあたりに再送信される履歴量そのものが増えていく
  • 1回の巨大ファイル読み込みより「長時間・多ターンの1セッション」自体が最大のコスト要因になる
    • 大きなファイルを1回読むより、同じセッションでやり取りを繰り返す方が積算コストが大きくなりやすい
トークン使用量急増の原因調査から対策までの流れ(セッションログ解析→構造面の原因→具体的な原因→settings.jsonでの対策)
Claude Code(WSL)のトークン使用量急増を調査してから対策するまでの4ステップフロー図。セッションログ解析、毎ターン全履歴再送信という構造面の原因、gh apiやgh run等の大出力コマンド多用という具体的な原因、settings.jsonのpermissions.denyによる対策の順に並んでいる

判明した原因(具体的な操作):大出力コマンドの多用

構造面の原因がわかったところで、次は「じゃあ具体的に何が全履歴の再送信を膨らませていたのか」です。

ログを見ていくと、特にトークンを消費していたのは以下のBashコマンド群でした。

  • gh api repos/.../actions/jobs/<id>/logs
    • GitHub Actionsのジョブログを生のまま全文取得していた
  • gh issue view <N> --json title,body,labels,comments,...
    • Issue本文+コメントを全部取得していた
  • gh run list / gh run view
    • CI実行状況を確認するために繰り返し呼び出していた
  • find / -maxdepth 6 -iname "hexo-cli"
    • WSL⇄Windows間のnpmパス探索で、/mnt/c/Users/.../npm/node_modulesのような広範囲を探索していた

これらが1セッション内で何十〜100回以上蓄積し、その都度「これまでの全履歴」が再送信されていたのが実態でした。
1つ1つのコマンドは軽く見えても、積み重なるとセッション全体の再送信コストを大きく押し上げていたわけです。

定量データ:8/2〜8/7の使用状況

実際にどれくらいの規模だったか、対象プロジェクト全体の集計値です(8/2〜8/7)。

指標 合計
API呼び出し回数 815回
出力トークン 約66万
キャッシュ読込トークン 約5,604万
キャッシュ生成トークン 約309万

合計約5,979万トークン(出力+キャッシュ読込+キャッシュ生成)のうち、キャッシュ読込トークンが約94%を占めています。
これが、まさに「毎ターン全履歴を再送信している」ことの裏付けです。

中でも、次の3セッションだけでキャッシュ読込トークン全体(約5,604万トークン)の約72%を占めていました。

セッション 所要時間 ツール呼び出し キャッシュ読込 全体比 内容
A 59分 198回(Bash 71, AskUserQuestion 16) 約1,769万トークン 約32% WSL上のhexo-cli/npmパス解決トラブル調査
B 129分 170回(Bash 56, Edit 13等) 約1,571万トークン 約28% 同種の環境トラブル継続対応
C 82分 103回(Bash 42等) 約711万トークン 約13% GitHub Actionsが起動しない問題のデバッグ(gh apiでログ全文取得、gh issue viewでIssue全文取得を繰り返し)

どれも「環境トラブルの原因が掴めず、同じような確認コマンドを繰り返した」セッションという共通点があります。

講じた対策:settings.jsonのpermissions.denyで恒久禁止

原因が具体的なコマンド群だとわかったので、対策はシンプルに該当コマンドをpermissions.denyで恒久的に禁止する方針にしました。

.claude/settings.json

1
2
3
4
5
6
7
8
{
"permissions": {
"deny": [
"Bash(gh api *)",
"Bash(gh run *)"
]
}
}

プロジェクト個別設定・WSL全体のグローバル設定の両方に追加し、CIジョブログの全文取得とgh run list/gh run viewによる監視ループを禁止しました。

一方で、gh issue view/gh issue commentgh pr create/gh pr viewは許可のまま維持しています。
Issue駆動の記事自動生成・修正ワークフロー(今回のようなissue-to-articlerevise-article)はこれらのコマンドが起点になっているため、ここまで塞いでしまうと自動化そのものが機能停止してしまうからです。

線引きの理由を整理すると、次のようになります。

内容 採用可否 理由
個別deny(採用) gh api/gh runのみ禁止 採用 CIログ全文取得・監視ループだけを塞ぎ、Issue駆動の自動化ワークフローは維持できる
全面禁止 ghコマンド全体を禁止 見送り Issue取得が起点の自動化ワークフローが最初の一歩で機能停止してしまうため

運用上の教訓(読者向けTIPS)

今回の調査を通じて、今後のためにまとめておきたい教訓がいくつかありました。

  • 長時間化しそうな環境トラブルシューティングは、要点が固まった時点でセッションを区切る
    • 1セッション100ターン超えは要注意サイン
  • gh apigh issue view --jsonのような大出力コマンドは、必要部分だけ渡す
    • --jsonのフィールド絞り込みや| tail -Nで出力量そのものを削る
  • find /のような広範囲検索は対象パスを絞る
    • WSL⇄Windows間のパス探索は特に肥大化しやすい
  • 恒久的に締め出したいコマンドはpermissions.denyにプレフィックスワイルドカードで登録しておく
    • 都度確認するより、再発防止として先に登録しておく方が確実

締め

いかがでしたか?

自分の場合、原因が判明するまでは「大きいファイルを読み込んだせいかな」くらいに考えていたのですが、実際は普段何気なく打っていたコマンドの積み重ねが原因でした。

ログを解析してみるまで「セッションが長引くこと自体がコストになる」という感覚がなかったので、調査してよかったです。

同じようにトークンがすぐなくなる!といった悩みがある方は、一度ログを覗いてみることをお勧めします。

まず記事内のanalyze_usage.pyで自分のキャッシュ読込トークンの割合を確認し、次にツール呼び出し100回・所要時間60分超えのような長時間セッションがないか見てみるとよいです。
該当する大出力コマンドが見つかったら、settings.jsonpermissions.denyに追加するだけで再発防止になります(^^

以上となります。
トークン少ないと文句を言っていた自分が恥ずかしい…
それではお疲れさまでした。