Claude Code の "今" を、ログから読む

ccmux のステータスバーに出る「🔧 Bash」「45.2k tokens」はどこから来ているのか。Claude Code が残すログファイルを覗いてみる話。

ccmux のペインには、こんな表示が出ます:

● claude [1] 🤖 evaluator 🔧 Bash ▓▓░░ 3/8 · 45.2k tokens
  • 今サブエージェントが動いている(🤖 evaluator)
  • その中で Bash を走らせている(🔧 Bash)
  • TodoList は 8 個中 3 個が済んでいる
  • セッション全体で 45,200 トークン使った

これ、Claude Code に何か組み込んでもらっているわけではありません。Claude Code が普通に残しているログファイルを、ccmux がそっと覗いているだけです。

ログの場所

Claude Code は会話ログをこの場所に書き出しています:

~/.claude/projects/<プロジェクト名>/<UUID>.jsonl

プロジェクト名は「今いるディレクトリのパス」を変換したもの。例えば C:\Users\じゅぶ\Desktop\dev\ccmux は:

C--Users-----Desktop-dev-ccmux

になります。英数字以外は全部 - に置換されるので、「じゅぶ」の 3 文字が --- に潰れているわけです。

実装当初、ccmux は ASCII だけ残して他を落とす処理をしていて、日本語パスのユーザーだけログが見つからないという静かなバグになってました。

src/claude_monitor.rs
fn encode_cwd_to_project_name(cwd: &Path) -> String {
cwd.to_string_lossy()
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect()
}

一次情報(実ファイル)を信じる、という学び。

JSONL の中身

JSONL は「1 行 1 イベント」の JSON です。中身はこんな感じ:

{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_01...","name":"Bash","input":{"command":"ls"}}]},"usage":{"input_tokens":12,"output_tokens":8}}
{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_01...","content":"README.md\nsrc\n..."}]}}

ccmux が見ているのは主にこの 4 つ:

フィールド何がわかるか
tool_use.name今動いているツール(Bash / Edit / Read / Agent)
tool_use.input.subagent_typeサブエージェントの種類
usage.*トークン消費量
message.stop_reason作業中か完了か

これをファイルの末尾まで舐めるだけで、さっきのステータス行が組み立てられます。

ハマりどころ: トークン二重計上

単純に usage.input_tokens を全部足したら、実際の 3 倍になりました。

原因は、1 つのリクエストに対して同じ usage が 3 回書かれるから。Claude Code は 1 ターンで複数行のイベントを書き出し、そのどれもに同じトークン情報を載せます。

解決は requestId を覚えておくだけ:

if !self.counted_request_ids.insert(req_id.to_string()) {
return; // 見たことある ID ならスキップ
}

ハマりどころ: ファイルがどんどん伸びる

Claude Code はログに追記し続けるので、ccmux 側は:

  1. 前回読んだ位置 (file_position) を覚えておく
  2. ファイルの更新時刻を見て、変わってたら開く
  3. 前回位置からだけ読む
  4. ファイルが縮んでいたら(セッション切替)位置をリセット

500ms ごとにこれをチェック。短すぎると IO が無駄、長すぎるとラグが出る。500ms は体感として「ちょうどいい」です。

ハマりどころ: UI が固まる

最初は雑にロックを取って JSONL を読んでいました。すると描画スレッドがロック待ちで停止して、UI がカクつく。

対策はロックを 3 段階に分けただけ:

  1. 短ロック: 次に読むべきファイルパスだけ取る
  2. ロックなし: ファイル IO(ここが重い)
  3. 短ロック: 読んだ結果を state に書き込む

ロックを持っている時間が合計で数十 μs 程度まで縮んで、描画に影響しなくなりました。


Claude Code は情報を親切に書き出してくれているので、読み取り側は愚直でいい。ccmux のモニタリング機能は、このスタンスで成り立っています。

次回は、この JSONL からサブエージェントの「起動中 / 終了」を判定する仕組みを書きます。