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 だけ残して他を落とす処理をしていて、日本語パスのユーザーだけログが見つからないという静かなバグになってました。
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 側は:
- 前回読んだ位置 (
file_position) を覚えておく - ファイルの更新時刻を見て、変わってたら開く
- 前回位置からだけ読む
- ファイルが縮んでいたら(セッション切替)位置をリセット
500ms ごとにこれをチェック。短すぎると IO が無駄、長すぎるとラグが出る。500ms は体感として「ちょうどいい」です。
ハマりどころ: UI が固まる
最初は雑にロックを取って JSONL を読んでいました。すると描画スレッドがロック待ちで停止して、UI がカクつく。
対策はロックを 3 段階に分けただけ:
- 短ロック: 次に読むべきファイルパスだけ取る
- ロックなし: ファイル IO(ここが重い)
- 短ロック: 読んだ結果を state に書き込む
ロックを持っている時間が合計で数十 μs 程度まで縮んで、描画に影響しなくなりました。
Claude Code は情報を親切に書き出してくれているので、読み取り側は愚直でいい。ccmux のモニタリング機能は、このスタンスで成り立っています。
次回は、この JSONL からサブエージェントの「起動中 / 終了」を判定する仕組みを書きます。