Claude Code 跑了 52 天,账单居然全是 0 块钱?
这种“静默失败”比直接报错要命得多。如果你在用 Claude Code 的 MAX 套餐,虽然是包月,但如果不监控每个 Session 消耗了多少 Token,一旦某个自动化任务失控,下个月的额度可能瞬间被吃光。
为什么 Stop hook 会“骗人”
我最初的设计思路很直观:利用 Claude Code settings.json 里的 Stop hook,在每次 Session 结束(比如输入 /exit 或超时)时,自动触发一个脚本来统计 Token 消耗并记入 ~/.claude/metrics/costs.jsonl。
我理所当然地认为,Stop hook 触发时,它的 stdin(标准输入)里应该会带上当前 Session 的 usage(用量)和 model(模型)信息。但实际踩坑后才发现,Claude Code 的 Stop hook payload 里根本没有这些字段。它只给了你 session_id、transcript_path 之类的基础信息。因为我之前的脚本一直在等一个不存在的字段,所以计算结果永远是 0,而日志看起来又非常整齐,完全没引起我的警觉。
现在的实操方案:从“等数据”改为“读日志”
为了解决这个问题,我把逻辑重构成了“从零开始”解析 Transcript 文件。现在的逻辑不再寄希望于 Hook 传参,而是直接去读 Claude Code 实时写入的那个 JSONL 日志文件。
整个工作流的逻辑如下:
1. 触发 Hook:Session 结束,Stop hook 启动。
2. 定位文件:从 stdin 获取 transcript_path。
3. 解析 JSONL:遍历该路径下的日志文件,寻找所有 type: "assistant" 的行。
4. 累加 Token:提取每行里的 usage 对象,把 input_tokens、output_tokens 以及缓存相关的 token 全部加起来。
5. 计算成本:根据模型单价(Rate Table)算出 USD 成本。
6. 写入记录:最后把这一行结果追加到本地的 metrics 文件里。
这里有一个核心的代码逻辑参考,我把计算部分的关键点写出来,大家部署自动化 Agent 时可以参考这种思路:
// 核心逻辑:通过解析 Transcript 文件来获取真实的 Token 使用量
async function sumUsageFromTranscript(transcriptPath) {
const lines = await fs.readFile(transcriptPath, 'utf-8');
const logs = lines.split('\n').filter(line => line.trim());
let totalInput = 0;
let totalOutput = 0;
for (const line of logs) {
const entry = JSON.parse(line);
// 必须精准定位 assistant 回复的 usage 字段
if (entry.type === 'assistant' && entry.message?.usage) {
const { input_tokens, output_tokens } = entry.message.usage;
totalInput += input_tokens;
totalOutput += output_tokens;
}
}
return { totalInput, totalOutput };
}
// 成本计算示例 (基于 1M tokens 的单价)
const estimatedCostUsd = (totalInput * RATE_TABLE.input + totalOutput * RATE_TABLE.output) / 1e6;一点实战经验
在公司环境里跑这种自主运行的 AI Agent,监控一定要做“闭环验证”。千万不要只看脚本有没有跑,一定要定期抽查一下 costs.jsonl 里的数值是不是符合预期。如果发现连续几个 Session 的成本都是 0,或者数值极度异常,那大概率是 Hook 的数据流断了,或者是模型版本更新导致 JSON 结构变了。
这种自动化环境越方便,它“吞噬”资源的速度就越快,建立一套基于 Transcript 的真实数据采集流水线,比什么都重要。