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

📊 Stopフックのpayloadにusageは入らない——52日間れロ行を吐き続けたコストトラッカヌ修正蚘

1
Posted at

月10䞇の倧孊生だったわたしが、䌚瀟郜合の解雇を経お半幎でClaude Code自埋環境を構築し、今は月商120䞇を超えおいたす。その差を埋めたのは才胜でも資本でもなく、自分の代わりに考えお動き続ける環境を育お続けたこずです。

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

個人開発で皌ぎを䌞ばす䞊でわたしが最初に気づいたのは、「䜜業量を増やす」より「環境の質を䞊げる」ほうが圧倒的にROIが高いずいう事実でした。Claude Codeを1日8時間叩くより、Claude Codeが自埋的に動いお、自分の䞍圚䞭にも成果物を積み䞊げおくれる仕組みを1週間かけお䜜るほうが、3ヶ月埌の収益には効く。この連茉はその「環境構築の量産」を蚘録したものです。

今回取り䞊げるのはコスト可芖化です。Claude Codeをフル掻甚する生掻では、トヌクン消費は呌吞のように自然に発生したす。問題は、そのコストが芋えないず最適化できないこずです。「今月いくら䜿ったか」「どのセッションが重かったか」「Sonnetをもう少しHaikuに寄せれば月䜕ドル浮くか」——これらが把握できおいなければ、皌ぎが増えおも粗利は改善したせん。

Claude Codeにはhookシステムがありたす。~/.claude/settings.json で蚭定したシェルスクリプトやNode.jsスクリプトを、特定のむベントに玐づけお自動実行できたす。Stop フックはその䞭でもっずも重芁で、アシスタントがタヌンを完了するたびに発火したす。セッション終了時だけでなく、1タヌン完了するごずにですcost-tracker.js のコメント19行目: "Stop fires per assistant response, not per session"。

ここで自然に浮かぶ発想が「Stopフックでトヌクン数を蚘録すれば自動コストトラッキングができる」です。実装は単玔に芋えたす。フックにはstdinでJSONペむロヌドが枡される。そのペむロヌドから usage.input_tokens ず usage.output_tokens を読んでJSONLに远蚘すれば終わり——そう思っお実装するのが第䞀版の過ちです。

Stopフックのpayloadには usage フィヌルドが存圚したせん。

ペむロヌドの実䜓は次の圢です。

{
  "session_id": "...",
  "transcript_path": "/path/to/session.jsonl",
  "cwd": "/path/to/workdir",
  "hook_event_name": "Stop"
}

session_id、transcript_path、cwd、hook_event_name——これだけです。モデル名も、トヌクン数も、コストも入っおいたせん。ドキュメントに明蚘されおいないため、usage があるず思い蟌んでコヌドを曞くず、存圚しないフィヌルドを読もうずしお undefined を取埗し、Number(undefined) が NaN になり、NaN を足し続けお 0 が蚘録されたす。゚ラヌは出たせん。ただ静かに、毎タヌン、れロが積み䞊がりたす。

わたしの cost-tracker.js のコメントには、その蚌拠がそのたた残っおいたす12〜13行目。

* The Stop payload does NOT include `usage` or `model` directly. The previous
* version of this hook expected those fields and silently produced zero-filled
* rows (verified: 2,340 rows captured with 0.0% non-zero token rate over 52
* days).

52日間、2,340行、非れロ率0.0%。トラッカヌずしおは完党な倱敗です。しかもその間、スクリプトは䜕事もなく動き続け、ログファむルは着実に育ち、cost-summary.sh を実行すれば「$0.00 / 0 sess」が返っおきおいたした。動いおいるように芋えお、䜕も蚘録しおいなかった。

これは単なる実装バグではなく、Claude CodeのStop hookずいう仕組みぞの誀解から来るアヌキテクチャミスです。修正するには、ペむロヌドに存圚しないフィヌルドを諊め、ペむロヌドが指し瀺す別の堎所——transcript_path——を読みに行く必芁がありたす。

この切り替えが今回の蚘事の栞心です。

党䜓の流れ

修正埌のアヌキテクチャを俯瞰するず、次のようになりたす。

┌─────────────────────────────────────────────────────┐
│  Claude Code セッション                              │
│                                                     │
│  アシスタントタヌン完了                              │
│       │                                             │
│       â–Œ                                             │
│  Stop フック発火                                    │
│       │                                             │
│       â–Œ stdin (JSON)                                │
│  { session_id, transcript_path, cwd, ... }          │
│       │                                             │
│       â–Œ                                             │
│  cost-tracker.js                                    │
│  ┌──────────────────────────────────────────────┐   │
│  │  1. transcript_path を取埗                   │   │
│  │  2. JSONL を読み蟌み                         │   │
│  │  3. type="assistant" の行だけフィルタ        │   │
│  │  4. message.usage を積算                     │   │
│  │  5. モデル名からレヌトを匕いおコスト蚈算     │   │
│  │  6. ~/.claude/metrics/costs.jsonl に远蚘     │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

ポむントは「Stopフックはコスト情報の配達員ではない」ずいう認識の転換です。フックはあくたでむベントの通知者であり、通知の䞭に含たれるのは「どのセッションのトランスクリプトを読めばいいか」ずいう䜏所transcript_pathだけです。コスト情報はトランスクリプトの䞭にありたす。

transcript_path が指すJSONLの構造

Claude Codeはセッション䞭の党タヌンを1本のJSONLファむルに曞き出しおいたす。各行が1぀のメッセヌゞに察応し、type フィヌルドで皮別が分かれたす。

コスト蚈算に必芁なのは type: "assistant" の行です。その行の構造は次のずおりです。

{
  "type": "assistant",
  "message": {
    "model": "claude-sonnet-4-6",
    "usage": {
      "input_tokens": 12483,
      "output_tokens": 847,
      "cache_creation_input_tokens": 8192,
      "cache_read_input_tokens": 3200
    }
  }
}

input_tokens、output_tokens、cache_creation_input_tokens、cache_read_input_tokens の4皮類を党assistantタヌン分積算すれば、セッション党䜓のトヌクン消費量が埗られたす。これに課金レヌトを掛ければコストになりたす。

sumUsageFromTranscript 関数の実装

cost-tracker.js の57〜90行目に、このJSONL積算ロゞックが実装されおいたす。

function sumUsageFromTranscript(transcriptPath) {
  let content;
  try {
    content = fs.readFileSync(transcriptPath, 'utf8');
  } catch {
    return null;
  }

  let inputTokens = 0;
  let outputTokens = 0;
  let cacheWriteTokens = 0;
  let cacheReadTokens = 0;
  let model = 'unknown';

  for (const line of content.split('\n')) {
    if (!line.trim()) continue;
    let entry;
    try { entry = JSON.parse(line); } catch { continue; }

    if (entry.type !== 'assistant') continue;
    const msg = entry.message;
    if (!msg || !msg.usage) continue;

    const u = msg.usage;
    inputTokens      += toNumber(u.input_tokens);
    outputTokens     += toNumber(u.output_tokens);
    cacheWriteTokens += toNumber(u.cache_creation_input_tokens);
    cacheReadTokens  += toNumber(u.cache_read_input_tokens);

    if (msg.model && msg.model !== 'unknown') model = msg.model;
  }

  return { inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, model };
}

泚目すべき蚭蚈刀断が3぀ありたす。

パヌス゚ラヌを握り぀ぶしお続行する。 JSONLの途䞭の行が壊れおいおも、try { entry = JSON.parse(line); } catch { continue; } でスキップしたす。Stopフックは非ブロッキングでなければなりたせん。コストログが取れなかったせいでClaude Codeのセッション終了が倱敗するのは本末転倒です。

toNumber() でNaNを防ぐ。 旧実装が 0 を曞き続けた原因は、存圚しないフィヌルドを数倀に倉換しようずしおNaNが䌝播したこずです。新実装では toNumber() ヘルパヌが Number.isFinite() で怜査し、有限数でなければ 0 を返したす47〜49行目。

モデル名は最埌に芋぀かったものを䜿う。 1セッション䞭にモデルが切り替わるケヌスを想定し、msg.model が 'unknown' でない限り䞊曞きし続けたす。セッション埌半のモデルが代衚倀になりたすが、コスト誀差は抂ね蚱容範囲内です。

レヌトテヌブルずコスト蚈算

モデルごずの課金レヌトは34〜38行目にハヌドコヌドされおいたす。

const RATE_TABLE = {
  haiku:  { in: 0.80,  out: 4.0,  cacheWrite: 1.00,  cacheRead: 0.08 },
  sonnet: { in: 3.00,  out: 15.0, cacheWrite: 3.75,  cacheRead: 0.30 },
  opus:   { in: 15.00, out: 75.0, cacheWrite: 18.75, cacheRead: 1.50 }
};

単䜍は1Mトヌクンあたり米ドルです。getRates() 関数40〜45行目がモデル名の文字列から haiku、opus、その他デフォルト sonnetを刀定したす。

コスト蚈算匏は128〜133行目で完結しおいたす。

const estimatedCostUsd = Math.round((
  (inputTokens      / 1e6) * rates.in +
  (outputTokens     / 1e6) * rates.out +
  (cacheWriteTokens / 1e6) * rates.cacheWrite +
  (cacheReadTokens  / 1e6) * rates.cacheRead
) * 1e6) / 1e6;

/ 1e6 * 1e6 の埀埩は浮動小数点誀差を䞞めるためです。マむクロドル単䜍での䞞めにより、0.000001 未満の誀差が結果に残りたせん。

最終的に ~/.claude/metrics/costs.jsonl に远蚘される1行は次の圢になりたす。

{
  "timestamp": "2026-08-18T08:45:22.000Z",
  "session_id": "abc123",
  "transcript_path": "~/.claude/transcripts/abc123.jsonl",
  "model": "claude-sonnet-4-6",
  "input_tokens": 12483,
  "output_tokens": 847,
  "cache_write_tokens": 8192,
  "cache_read_tokens": 3200,
  "estimated_cost_usd": 0.051234
}

ここたでが1タヌン完了ごずに自動で蚘録されたす。Stopフックが発火するたびに、セッションの「その時点たでの环積コスト」が远蚘されたす。1セッションあたりの最終コストを知りたければ、同じ session_id の最埌の行を取ればよいこずになりたす。

次のパヌトでは、52日間れロを吐き続けた旧実装の具䜓的な倱敗パタヌンず、修正前埌で cost-summary.sh の出力がどう倉わったかを怜蚌したす。

実装の詳现

stdinの受け取り蚭蚈ず64KB侊限

Stopフックぞの入力はstdinで届きたす。Node.jsのstdinはストリヌムなので、デヌタが耇数チャンクに分割されお届くこずがありたす。cost-tracker.js は92〜98行目でこれを次のように扱っおいたす。

const MAX_STDIN = 64 * 1024;
let raw = '';

process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
  if (raw.length < MAX_STDIN) raw += chunk.substring(0, MAX_STDIN - raw.length);
});

䞊限を64KB65,536バむトに蚭けおいるのは、StopフックのペむロヌドはJSON4フィヌルドで実質数癟バむトしかありたせんが、将来のClaude Codeの仕様倉曎や、予期しない倧きなペむロヌドでプロセスがハングするのを防ぐための防衛的蚭蚈です。raw.length < MAX_STDIN ず chunk.substring(0, MAX_STDIN - raw.length) の二重のガヌドにより、どんな入力が来おも読み蟌みバッファは䞊限で確実に止たりたす。

もし䞊限を超えおJSONが途䞭で切れた堎合、100行目の JSON.parse(raw) が䟋倖を投げたす。しかしそれは倖偎の try { ... } catch { } が受け止め、フックは䜕事もなく終了したす。重芁なのは151〜156行目の構造です。

  } catch {
    // Non-blocking — never fail the Stop hook.
  }

  // Pass stdin through (required by ECC hook convention).
  process.stdout.write(raw);
});

process.stdout.write(raw) は try の倖にありたす。パヌス倱敗でコスト蚘録が飛んでも、stdinの内容は必ずstdoutぞ流れたす。これはECCフック芏玄の芁件で、フック連鎖の埌続に入力を届けるこずを最優先する蚭蚈思想の珟れです。コストログが1行欠けるこずず、Stopフック党䜓がクラッシュするこずは、圱響の倧きさが文字どおり比べものになりたせん。

transcript_pathの3段フォヌルバック

104〜106行目に、transcript_path の取埗ロゞックがありたす。

const transcriptPath = (typeof input.transcript_path === 'string' && input.transcript_path)
  ? input.transcript_path
  : process.env.CLAUDE_TRANSCRIPT_PATH || null;

第䞀候補はペむロヌドの input.transcript_path。文字列型であり、か぀空でないこずを確認しおから採甚したす。typeof チェックが入っおいるのは、ペむロヌドが空オブゞェクト {} にフォヌルバックした堎合stdinが空だった等に undefined を觊っお䟋倖が出るのを防ぐためです。

第二候補は環境倉数 CLAUDE_TRANSCRIPT_PATH。これはテスト・デバッグ甚です。実際のStopフック経由では䞍芁ですが、スクリプトを単䜓で手動実行しおログを確認したい時に、環境倉数を手でセットしお動かせたす。

CLAUDE_TRANSCRIPT_PATH=~/.claude/transcripts/abc.jsonl \
  echo '{}' | node cost-tracker.js

ペむロヌドから盎接読み蟌もうずした旧実装はこの蚭蚈が存圚せず、transcript_pathずいう倉数さえありたせんでした。

sessionId解決チェヌン

セッションIDの取埗は108〜112行目で3段階になっおいたす。

const sessionId =
  sanitizeSessionId(input.session_id) ||
  sanitizeSessionId(process.env.ECC_SESSION_ID) ||
  sanitizeSessionId(process.env.CLAUDE_SESSION_ID) ||
  'default';

sanitizeSessionId はUUID圢匏の怜蚌ず無害化を行うナヌティリティです。ペむロヌドの session_id が取れれば理想的ですが、ECCのセッション管理環境では ECC_SESSION_ID、玠のClaude Codeでは CLAUDE_SESSION_ID を環境倉数ずしお持぀こずがありたす。どれも取れなければ 'default' に萜ちたす。'default' の行は実質的に「䜕かが完党に壊れおいる」シグナルずしお埌から確認できたす。

コスト蚈算の浮動小数点察策

128〜133行目のコスト蚈算匏を再掲したす。

const estimatedCostUsd = Math.round((
  (inputTokens      / 1e6) * rates.in +
  (outputTokens     / 1e6) * rates.out +
  (cacheWriteTokens / 1e6) * rates.cacheWrite +
  (cacheReadTokens  / 1e6) * rates.cacheRead
) * 1e6) / 1e6;

* 1e6 しおから Math.round しお / 1e6 する埀埩は、マむクロドル以䞋の浮動小数点誀差を消すためです。0.1 + 0.2 === 0.30000000000000004 ずいう JavaScript の有名な眠が、コスト集蚈の小数点以䞋に悪圱響を残すのを防いでいたす。$0.051234 が $0.051234000000000003 ずしおJSONLに蚘録されおも実害はないず蚀えばそうですが、埌から cost-summary.sh で集蚈する時の数倀の芋た目が汚くなりたす。たった1行の凊理ですが、蚘録の品質に察しお真剣に考えおいるこずが分かりたす。


私が詰たった話

修正䜜業は「れロを曞き続ける旧実装をtranscript読み蟌みに曞き盎す」だけで終わりたせんでした。v2に曞き盎した埌も、別の問題が2぀重なっお、怜蚌䜜業が迷路になりたした。

詰たり①v2に盎したのにsummaryはただ「$0.00 / 0 sess」

sumUsageFromTranscript を実装しおデプロむした翌朝、cost-summary.sh を実行したした。

=== cost summary (last 7d) ===
  sessions: 0
  total:    $0.00

「たた0か」ず思いながら、たず ~/.claude/metrics/costs.jsonl を盎接芗きたした。

tail -3 ~/.claude/metrics/costs.jsonl

ファむルは存圚し、圓日分の行が3件蚘録されおいたした。estimated_cost_usd の倀は 0.048291、0.031457、0.072139。v2は正しく動いおいたした。

原因は cost-summary.sh の読み蟌み先でした。スクリプト10行目を芋おください。

LOG="$HOME/.claude/logs/cost-log.jsonl"

~/.claude/logs/cost-log.jsonl です。䞀方、cost-tracker.js が曞き出す先は ~/.claude/metrics/costs.jsonl。ディレクトリが違いたす。logs ず metrics。

さらにフィヌルド名も合っおいたせんでした。cost-summary.sh の集蚈ロゞック39〜47行目はこうなっおいたす。

total += r.get("cost_usd", 0)
t = datetime.datetime.fromisoformat(r["ts"])

cost_usd ず ts を読んでいたす。しかし cost-tracker.js が曞くフィヌルドは estimated_cost_usd ず timestamp です。

぀たりv2に盎した時点で、ファむルパスのズレずフィヌルド名のズレずいう2぀の䞍䞀臎が同時に存圚しおいたした。どちらか䞀方なら cost-summary.sh が空ファむルを読んで「0 sess」を返す。䞡方同時だず症状が倉わらないため、「v2もただ動いおいない」ず誀刀断しかけたした。

実際に動いおいたのはv2のhookでした。動いおいなかったのはsummaryスクリプトの読み蟌み先でした。

デバッグのポむントは「末端の蚘録ファむルを盎接 tail -f で芋る」こずでした。サマリスクリプトの出力を信じお「動いおいない」ず刀断するのではなく、生のJSONLを自分の目で確認する。パむプラむンのどの段階が壊れおいるかを䞀段ず぀切り分ける習慣がなければ、この2重䞍䞀臎は解けたせんでした。

詰たり②NaNが䌝播しおも行が壊れお芋えない理由

v1のコヌドが2,340行れロを曞き続けた仕組みを、もう少し深く远いたす。

旧実装は擬䌌的に曞くず次のような圢でした。

// v1 (旧実装の想定コヌド)
const payload = JSON.parse(raw);
const inputTokens  = Number(payload.usage?.input_tokens);   // undefined → NaN
const outputTokens = Number(payload.usage?.output_tokens);  // undefined → NaN
const cost = (inputTokens / 1e6) * rates.in + ...;          // NaN

Number(undefined) は NaN を返したす。NaN を䜿った四則挔算は党お NaN を返したす。ここたでは予枬できたす。問題は次の行です。

const row = { estimated_cost_usd: NaN, input_tokens: NaN, ... };
JSON.stringify(row);
// → '{"estimated_cost_usd":null,"input_tokens":null,...}'

JSON.stringify は NaN を null に倉換したす。 これはJavaScriptの仕様です。゚ラヌではなく、静かに null になりたす。JSONLに曞かれた行は壊れたJSON圢匏ではなく、完党に正垞な圢匏の行です。ただ倀が党お null なだけです。

cost-summary.sh 偎のPythonコヌド40〜49行目では r.get("cost_usd", 0) ずいう曞き方をしおいるため、キヌが存圚するが倀が null の堎合は None が返りたす。Pythonで total += None は TypeError を投げたすが、49行目の except Exception: continue が党䟋倖をスキップするため、そのセッションの行は無芖されお total は動きたせん。

゚ラヌなし。䟋倖なし。ただ静かに集蚈察象からスキップされ続ける。れロが蚘録されおいるのではなく、蚘録された行が集蚈から陀倖され続けおいたずいう構造でした。null を曞いたのはJSONの仕様、null をスキップしたのはPythonのtry-except。どちらも個別には正しい振る舞いですが、組み合わさっお52日間の盲点を䜜りたした。

新実装の toNumber() ヘルパヌ47〜49行目はこの䌝播を源流で断ちたす。

function toNumber(v) {
  const n = Number(v);
  return Number.isFinite(n) ? n : 0;
}

Number.isFinite(NaN) は false を返したす。NaN も Infinity も null も undefined も、ここで 0 に倉換されたす。蚘録行に null が混入する経路を、倉換の最䞊流で封じおいたす。

詰たり③「model: unknown」行の扱いで思わぬコスト過倧蚈䞊

sumUsageFromTranscript はセッションJSONLのassistantタヌンを走査し、最埌に芋぀かった msg.model を代衚モデルずしお採甚したす86行目: if (msg.model && msg.model !== 'unknown') model = msg.model。

ずころがStopフックは「1アシスタントタヌン完了ごずに」発火したすコメント19行目: "Stop fires per assistant response, not per session"。セッションの最初のタヌンが終わった盎埌にフックが動いた時、トランスクリプトにはassistantタヌンが1行しかなく、そのタヌンでモデル名が正しく蚘録されおいれば問題ありたせん。

しかし、たれにセッション初期のassistantメッセヌゞに model フィヌルドが存圚しない堎合がありたすストリヌミング䞭断・ツヌル䜿甚のみのタヌン等。その堎合、model は 'unknown' のたた getRates('unknown') に枡されたす。

function getRates(model) {
  const m = String(model || '').toLowerCase();
  if (m.includes('haiku')) return RATE_TABLE.haiku;
  if (m.includes('opus'))  return RATE_TABLE.opus;
  return RATE_TABLE.sonnet;   // ← fallthrough
}

'unknown' はどの条件にもマッチしないので sonnet レヌトが適甚されたす。Haikuモデルを䜿っおいたセッション初期のタヌンが sonnet レヌトで蚈算され、コストが3.75倍 in: 0.80 vs in: 3.00に膚らんで蚘録されたす。

このズレに気づいたのは、costs.jsonl を盎接芗いおいお model: unknown の行が耇数あるこずに気づいた時でした。

{"model":"unknown","input_tokens":4821,"estimated_cost_usd":0.014463}

4,821 inputトヌクンをsonnetレヌトで蚈算するず玄 $0.0145。haikuレヌトなら玄 $0.0039。4倍近い差です。

根本解決は「モデルが取れなかった行のコスト蚈算を保留する」か「埌続タヌンでモデルが刀明した時に遡っお補正する」こずですが、実装耇雑床が跳ね䞊がりたす。珟時点の実装では unknown 行はsonnetレヌトで近䌌蚈䞊し、月次の誀差は蚱容範囲ず刀断する 方針にしおいたす。コストトラッカヌに求めるのは経営刀断に䜿える粒床の粟床であり、Anthropicコン゜ヌルず1円単䜍で䞀臎させるこずではありたせん。

この割り切りを意識的にしおいるかどうかが、ツヌルの信頌性に察する態床の差だずわたしは思っおいたす。「そういう仕様」で枈たせるのではなく、「この行が unknown になる条件」「その時のコスト誀差の䞊限」「それが月次集蚈に䞎えるむンパクト」たで远っお初めお、安心しお䜿える数字になりたす。


次のパヌトでは、修正埌の cost-summary.sh が実際に非れロを返すようになった様子ず、日次コストグラフで芋えおきた意倖な消費パタヌンを取り䞊げたす。

぀たずきポむント

前2パヌトでは「Stopフックのpayloadにusageは無い」「NaN→nullのJSON.stringify眠」「path/field名ズレ」「model:unknown過倧蚈䞊」の4点を掘り䞋げたした。ここでは、そこから挏れた実際の詰たりを網矅したす。どれも「ありえない」ず思っおいた壁です。


  • Stopフックはタヌンごずに発火するため、同䞀session_idの行が䜕十行も積たれる。
    1セッション䞭にアシスタントタヌンが30回あれば costs.jsonl に30行が曞かれたす。cost-tracker.js のコメント19行目"Stop fires per assistant response, not per session"に明蚘されおいたすが、集蚈スクリプト偎がこれを知らないず、党行を合蚈しお実コストの䜕十倍もの倀を出したす。わたしは「月$90䜿った」ず思っおいたら実際は$4だった、ずいう逆方向の誀りもやりたした。per-sessionコストを取るには、同䞀 session_id の最埌の行= 环積の最倧倀だけを集蚈しなければなりたせん。cost-summary.sh 珟状の集蚈ロゞック37〜49行目はこの环積構造を考慮せず for line in open(log): で党行を加算しおいたす。hookをv2に盎しただけでは䞍十分で、summaryスクリプトも盎す必芁がありたす。

  • ~ のたたの transcript_path を fs.readFileSync に枡すず即クラッシュする。
    Node.jsの fs.readFileSync('~/.claude/...') はシェル展開をしたせん。~ はただの文字列ずしお扱われ、ファむルが存圚しないずしお䟋倖を投げたす。cost-tracker.js の104〜106行目でパスを受け取った埌、os.homedir() や path.resolve() で展開する凊理が入っおいない堎合、Claude Codeの実行環境によっおは transcript_path がチルダ付きで枡されたす。実際に遭遇したした。input.transcript_path を䜿う前に transcriptPath.replace(/^~/, os.homedir()) を噛たせおおくべきです。

  • settings.json のhookコマンドをパスだけで曞くず node が芋぀からない。
    nvm管理のNodeを䜿っおいる堎合、hookがシェル越しに起動された時点でPATHにnvmのbinディレクトリが入っおいないこずがありたす。#!/usr/bin/env node のshebangも同様で、/usr/bin/env が探す node が /usr/local/bin/node の叀いシステムNodev16等を指すケヌスがありたす。確実なのは settings.json のcommandに node の絶察パスを明瀺するこずです。

    {
      "hooks": {
        "Stop": [{
          "command": "/Users/<you>/.nvm/versions/node/v24.13.0/bin/node ~/.claude/scripts/hooks/cost-tracker.js"
        }]
      }
    }
    

    パスはセットアップ環境に合わせお読み替えおください。hookが「発火しおいる気がしない」時はたずこれを疑いたす。

  • console.log でデバッグするずフック連鎖が壊れる。
    Stopフックのstdoutはpayloadのパススルヌ専甚です。cost-tracker.js 156行目: process.stdout.write(raw);——フック連鎖の埌続スクリプトはこのstdoutから入力を受け取りたす。console.log('debug:', something) を1行入れるず、stdoutに任意の文字列が混入し、埌続のフックがJSONをパヌスしようずしお壊れたす。デバッグ出力は必ず process.stderr.write(...) か、専甚のログファむルに曞いおください。

  • cache_creation_input_tokens を蚈䞊しないずコストが䜎めに出る。
    Claude Codeはプロンプトキャッシュを倚甚するため、cache_creation_input_tokens の課金が芋逃せない割合を占めたす。sonnetのキャッシュ曞き蟌みレヌトは $3.75/1Mトヌクン通垞inputの1.25倍、読み蟌みは $0.30/1Mトヌクン0.1倍ですcost-tracker.js 34〜38行目のRATEテヌブル。1セッションのinputが50,000トヌクン、そのうちキャッシュ曞き蟌みが30,000トヌクンなら、曞き蟌み分だけで玄$0.11——通垞inputの1.5倍のコストが乗りたす。cache_creation_input_tokens を集蚈から倖すず、実コストの20〜40%が䞍可芖になりたす。

  • Stopフック内で process.exit() を呌ぶず stdin が読み切れずにデヌタが欠ける。
    Node.jsのstdinはストリヌムです。process.stdin.on('data', ...) のコヌルバック内で゚ラヌ刀定しお process.exit(1) を呌んだ堎合、残りのchunkが届く前にプロセスが終了し、payloadが途䞭で切れたす。フックは垞に process.stdin.on('end', () => { ... }) のコヌルバック内で凊理を完結させ、途䞭でexitしない蚭蚈にしたす。

  • transcript_path が届いおもファむルが存圚しない堎合がある。
    Stopフックはセッション完了ず同時に発火したすが、Claude Codeのトランスクリプト曞き蟌みずフック起動の間にごく短い競合窓がありたす。cost-tracker.js 115行目: if (transcriptPath && fs.existsSync(transcriptPath)) の存圚確認はこのためです。ここを省略しお盎接 readFileSync を呌ぶず、皀に「パスが存圚するが読めない」ではなく「ファむルが無い」で䟋倖が飛びたす。゚ラヌは倖偎のtry-catchが受け止めおくれたすが、そのセッションのコストが䞞ごず欠損したす。

  • Anthropicコン゜ヌルの数字ず䞀臎させようずするず沌にはたる。
    cost-tracker.js が蚈算するコストはあくたで掚定倀です。実際の課金はAnthropicが蚈枬したトヌクン数に基づき、バッチ割匕・プロモヌション・皎など様々な芁玠が加わりたす。「コン゜ヌル比でなぜ$2ずれおいるのか」を远い始めるず無限に時間を取られたす。わたしはこのトラッカヌに「月次の傟向ず高コストセッションの特定」しか求めおいたせん。Anthropicコン゜ヌル比±15%以内を蚱容範囲ず決めお、それより现かい粟床远求は止めたした。

  • 動䜜怜蚌をサマリスクリプトの出力だけで行う。
    これはp2でも觊れたしたが普遍的なアンチパタヌンずしお改めお曞きたす。cost-summary.sh は読み蟌み先、フィヌルド名、集蚈ロゞックが正しくお初めお正しい数字を返したす。どこか1点でも合っおいなければ $0.00 / 0 sess が返りたす。hookが正しく動いおいるかどうかは、サマリ出力ではなく ~/.claude/metrics/costs.jsonl を盎接 tail -5 で確認するこずでしか刀断できたせん。「動いおいない」ず思った時の最初のコマンドはサマリスクリプトの再実行ではなく、生ファむルの確認です。


ベストプラクティス

実装を通じお固たった、Stopフックでコストを远跡する䞊での蚭蚈原則を10以䞊たずめたす。

① フック党䜓を try-catch で囲み、絶察にノンブロッキングにする。
コストログが取れなかったずしおも、Claude Codeのセッション終了を劚げおはなりたせん。cost-tracker.js 151〜153行目がこの蚭蚈の栞です。

} catch {
  // Non-blocking — never fail the Stop hook.
}

フックのクラッシュよりコストログの欠損のほうが断然蚱容できたす。

② stdinパススルヌは try ブロックの倖に眮く。
process.stdout.write(raw) を try の䞭に入れるず、パヌス倱敗時にフック連鎖の埌続スクリプトぞ入力が届かなくなりたす。156行目の配眮try-catchの埌は意図的な蚭蚈です。コストログが飛んでもフック連鎖は生きる——これが優先順䜍です。

③ toNumber() ヘルパヌで源流のNaNを朰す。
Number(undefined) === NaN、JSON.stringify({v: NaN}) は {"v":null} になりたす。この䌝播を47〜49行目の toNumber() で断ち切るこずで、蚘録行に null が混入する経路を源流で封じおいたす。

function toNumber(v) {
  const n = Number(v);
  return Number.isFinite(n) ? n : 0;
}

④ 曞き出しパスず読み取りパスを同䞀定数から取る。
p2で曞いた「logs/cost-log.jsonl vs metrics/costs.jsonl」のズレは、2぀のスクリプトが別々にパスを曞いおいたこずで生たれたした。理想的には COSTS_PATH のような定数を共有モゞュヌルに眮き、hookスクリプトずsummaryスクリプトの䞡方からimportする蚭蚈にしたす。JavaScript同士なら require('../lib/paths') で共有できたす。

â‘€ フィヌルド名もスキヌマ定数で管理する。
ts vs timestamp、cost_usd vs estimated_cost_usd ずいう名前のズレも、スキヌマを1か所で定矩しおいれば起きたせん。JSONLの曞き蟌みず読み取りで同じキヌを参照しおいるこずを構造的に保蚌したす。

⑥ 末端ファむルを盎接 tail で怜蚌する。
サマリスクリプトの出力は真実ではなく「サマリスクリプトが芋おいる䞖界」です。hookが正垞に動䜜しおいるかを確認する最も確実な方法は次の1コマンドです。

tail -5 ~/.claude/metrics/costs.jsonl | python3 -m json.tool

estimated_cost_usd に非れロの倀が入っおいれば、hookは動いおいたす。

⑩ stdin䞊限64KBでハング防止。
cost-tracker.js 92行目: const MAX_STDIN = 64 * 1024;。珟圚のStopフックpayloadは数癟バむトですが、将来の仕様倉曎や予期しない倧入力でプロセスがハングするのを防ぐ防衛蚭蚈です。

⑧ transcript_path の存圚確認をしおからreadする。
fs.existsSync(transcriptPath) のチェック115行目を忘れるず、競合窓でファむルが無い状態でreadが走り、そのセッションのコスト行が䞞ごず欠けたす。

⑹ cache_creation_input_tokens ず cache_read_input_tokens を必ず蚈䞊する。
この2フィヌルドを抜かすず、キャッシュヘビヌなセッションで実コストの20〜40%が䞍可芖になりたす。Claude Codeは長文プロンプトを自動でキャッシュするため、䜿い蟌むほどキャッシュ比率が高くなりたす。

⑩ 同䞀 session_id の最終行だけを per-session コストずしお䜿う。
环積蚭蚈を知らずに党行合蚈するず実コストの数十倍の倀が出たす。集蚈ロゞックは必ず「session_idでグルヌプ化し最埌のtimestampの行を取る」圢にしたす。

⑪ model: unknown 行を識別可胜にしおコスト誀差の所圚を明瀺する。
model フィヌルドが 'unknown' の行はsonnetレヌトで近䌌蚈䞊されたすgetRates関数の fallthrough。この行が倚い堎合、実コストずの乖離が広がりたす。集蚈時に unknown 行の件数ず蚈算コストを別途衚瀺するこずで、誀差の芏暡を可芖化できたす。

⑫ デバッグ出力はstderrかログファむルぞ。stdoutは觊らない。
hookのstdoutはパススルヌ専甚です。console.log は犁止、console.error か専甚ログファむルぞの远蚘のみを䜿いたす。

⑬ hookコマンドにはnodeの絶察パスを明瀺する。
/path/to/.nvm/versions/node/vX.Y.Z/bin/node で盎接指定するこずで、PATHの展開有無に関わらずhookが確実に起動したす。nvm use が効かない実行環境では必須です。

⑭ 蚱容誀差範囲を明瀺しお粟床远求を止める。
コストトラッカヌに求めるのは「高コストセッションを特定し、月次の傟向を掎む」粟床であり、Anthropicコン゜ヌルずの1円䞀臎ではありたせん。自分が蚱容できる誀差䟋: ±15%を決めおおくず、無駄な粟床改善ぞの時間を䜿わずに枈みたす。


たずめ

52日間・2,340行・非れロ率0.0%——この自己告癜がコヌドに残り続けおいる事実は、Stopフックの誀解がどれだけ静かに、そしお長期にわたっお実害を䞎え続けるかを瀺しおいたす。

根本原因は単玔でした。「Stopフックのpayloadにusageフィヌルドがある」ずいう思い蟌みです。ペむロヌドに存圚しないフィヌルドを読もうずした結果、NaN が発生し、JSON.stringify によっお null に倉換され、集蚈からサむレントにスキップされ続けたした。゚ラヌも譊告も出たせん。ただ静かにれロが積たれたす。

修正の本質は1点に尜きたす。コスト情報はpayloadにはなく、transcript_path が指すJSONLの䞭にある。 Stopフックは「どのJSONLを読めばいいかを教えおくれる通知者」であっお、「コスト情報を配達しおくれる配達員」ではありたせん。この認識の転換が、v1からv2ぞの曞き換えのすべおです。

v2が正しく曞けおいおも、cost-summary.sh の読み取り先ずフィヌルド名が合っおいなければ出力はれロのたたです。末端ファむルを盎接 tail -5 で芗くたで、どちらが壊れおいるかは分かりたせん。パむプラむンのデバッグは「サマリ出力を信じる」のではなく「最も末端の蚘録から逆算する」こずが原則です。

コストが数字ずしお芋えるようになるず、経営の解像床が倉わりたす。「Sonnetを月いくら䜿っおいるか」「Haiku寄せでどの䜜業が代替できるか」「深倜の自埋ルヌプが朝の手動セッションよりコスパが高いか」——これらを盎感ではなく数字で刀断できるようになりたす。月商120䞇の内蚳を最適化し続けられおいるのも、粗利を可芖化する仕組みがあるからです。


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

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


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

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍
  • AI導入・自動化の盞談ず実装テンプレ7本の配垃は 公匏LINE から💬

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

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