
News Observation Lab for Future
Claude Code の Stop フックでコスト追跡が失敗する原因と正しい実装方法

概要
筆者は月収10万円のアルバイト生活から、独自環境構築で月120万円の収益へ転換した。その鍵は「環境の品質向上」にあり、特にコスト可視化が課題となった。
Stop フックの誤解
Claude Code の Stop フックは、アシスタントが応答を完了するたびに起動するイベント通知であり、ペイロードにトークン使用量は含まれない。実際には session_id、transcript_path、cwd、hook_event_name の4項目だけが渡される。
誤った実装例と問題点
最初の実装では、ペイロードから直接 usage.input_tokens と usage.output_tokens を取得しようとしたため、存在しないフィールドは undefined になり、Number(undefined) が NaN を生成。結果として JSON.stringify が null に変換し、ゼロが記録され続けた。
正しいアプローチ:トランスクリプトから集計
Stop フックは transcript_path というファイルへのパスだけを提供するので、その JSONL ファイル内の type: "assistant" 行から input_tokens、output_tokens、cache_creation_input_tokens、cache_read_input_tokens を抽出し合計する。
実装のポイント
- エラー耐性:JSON パースエラーは
try…catchで捕捉し、処理を継続。 - NaN 対策:
toNumber()ヘルパーで有限数以外は 0 を返す。 - モデル名取得:セッション中にモデルが変更された場合でも最後に見つかった有効なモデル名を使用。
- 浮動小数点対策:
*1e6 → Math.round → /1e6の三段階でマイクロドル単位以下の丸め誤差を除去。
標準入出力とサイズ制限
Stop フックは標準入力から JSON を受け取り、最大 64 KB に制限して安全に処理。解析に失敗した場合でも process.stdout.write(raw) で入力はそのまま下流へ転送し、フック全体の停止を防止。
パスとフィールド名の不一致によるトラブル
修正版では ~/.claude/metrics/costs.jsonl に書き込むが、集計スクリプト cost-summary.sh は ~/.claude/logs/cost-log.jsonl を参照していた。また、書き込み側のキーは estimated_cost_usd/timestamp、読み込み側は cost_usd/ts を期待していたため、両方とも不一致で「$0.00 / 0 sess」と表示されていた。
デバッグの教訓
集計結果に疑問が生じたら、まず tail -f 等で実際の JSONL 出力を確認し、パイプラインの各段階を順に検証することが重要。
まとめ
Stop フックはコスト情報を直接提供しないイベント通知であることを理解し、トランスクリプトからトークン使用量を取得すれば正確なコスト追跡が可能になる。実装時はエラーハンドリング、NaN 防止、浮動小数点丸め、入出力サイズ制限に留意し、ファイルパスとフィールド名の整合性を保つことで、ゼロ記録の罠を回避できる。
元記事: https://dev.to/bokuwalily/52-days-of-silent-zeros-the-stop-hook-payload-has-no-usage-field-1kg8