从一个 HUD 状态栏工具出发,一路翻日志、扒 JSON、写 bash,最终搞定一套离线 token 用量统计方案。
起点:claude-hud 是个好东西
如果你是 Claude Code 重度用户,大概率见过 claude-hud 这个项目。它把 Claude Code 的实时状态嵌入终端状态栏(支持 tmux/iTerm2/Starship 等),效果大概是这样:
[claude-hud] claude-sonnet-4-6 ↑ 4.2k ↓ 18k cache 87% $0.18
一眼能看到当前 session 用的是哪个模型、输入输出了多少 token、缓存命中率多少,以及本次对话估算消耗了多少 token。对于用 API key 或 AWS Bedrock 的用户来说,这个实时感知非常爽。
我用了一段时间,确实很直观。但慢慢地,一个问题开始困扰我:
"我今天/这周到底花了多少?"
claude-hud 的 usage 进度条是针对 Claude.ai Pro/Max 订阅设计的,统计的是订阅额度消耗;而我用的是 Anthropic API 直连或 AWS Bedrock,它并不知道我实际支出了多少美元。每次要算账,都只能去 AWS 账单页面翻,延迟一天不说,还没法按项目细分。
这让我开始琢磨:Claude Code 把对话记录存在哪里?能不能直接从本地数据统计?
发现本地日志:.claude/projects/
翻了翻 Claude Code 的文档,找到了答案。所有对话记录都以 JSONL 格式存储在本地:
~/.claude/projects/
├── -Users-alice-fe-dashboard/
│ ├── 3a9f2c1e-5b7d-4e8a-9f1c-2d3e4f5a6b7c.jsonl
│ └── 8b1d3e5f-7a9c-4b2d-8e1f-3c5d7e9f1a2b.jsonl
├── -Users-alice-fe-api-service/
│ └── 2f4a6c8e-1b3d-5f7a-9c1e-4b6d8f0a2c4e.jsonl
└── -Users-alice-scripts/
└── 9e1c3a5b-7d9f-4a2c-8e0b-1d3f5a7c9e1b.jsonl
目录命名规则很直接:把工作路径里的 / 换成 -,就是目录名。每个目录对应一个工作路径,目录下的每个 .jsonl 文件是一个对话 session。
打开一个 JSONL 文件,每一行是一条消息记录。找到 type == "assistant" 的行,里面有关键的 usage 字段:
{
"type": "assistant",
"timestamp": "2026-03-20T09:14:32.451Z",
"message": {
"model": "claude-sonnet-4-6",
"usage": {
"input_tokens": 1024,
"output_tokens": 8192,
"cache_read_input_tokens": 487321,
"cache_creation_input_tokens": 62144,
"cache_creation": {
"ephemeral_5m_input_tokens": 62144,
"ephemeral_1h_input_tokens": 0
}
}
}
}
数据很完整。但看到这几个字段,新的问题来了——
等等,cache 这几个字段是什么意思?
如果你直接把 input_tokens + output_tokens 乘以单价,算出来的数字会让你困惑:明明用了很久,怎么费用这么低?
秘密就在 cache_read_input_tokens。
Claude Code 每次 API 调用都会携带完整的对话历史 + 系统 prompt,这个 context 动辄几十万 token。如果每次都重新传输计费,费用会爆炸。Anthropic 的 Prompt Cache 机制正是为此设计的:
第 1 轮对话:写入缓存(cache_creation)
第 2 轮对话:命中缓存(cache_read)← 只收 0.1x 价格
第 3 轮对话:命中缓存(cache_read)← 还是 0.1x 价格
...
在一个典型的 Claude Code 工作 session 里,cache_read 占总输入 token 的 85~90%,实际费用比"全量 input"计费便宜约 70~80%。
所以字段拆解如下:
| 字段 | 含义 | 计费倍率(相对 input) |
|---|---|---|
input_tokens | 本轮真正新发送的 token | 1x |
output_tokens | 模型生成的输出 | ~5x (output 更贵) |
cache_read_input_tokens | 命中已有缓存,复用的 token | 0.1x |
cache_creation.ephemeral_5m_input_tokens | 写入 5 分钟短效缓存 | 1.25x |
cache_creation.ephemeral_1h_input_tokens | 写入 1 小时长效缓存 | 2x |
注意 cache_creation_input_tokens 是两种写入的总和,但计费要分开算——5m 和 1h 单价不同,必须用细分字段。
架构:一个 bash 脚本搞定
理解了数据结构之后,整个方案就很清晰了:
~/.claude/projects/**/*.jsonl
│
│ find + xargs jq
▼
原始 CSV 流(每行一条 assistant 消息)
model, input, output, cache_hit, cache_5m, cache_1h, count=1
│
│ awk 分组聚合
▼
按模型分组的汇总数据
│
│ bash 函数查价格表
▼
per-model 费用 → awk 浮点加法累加
│
▼
格式化输出
整个逻辑用一个不到 160 行的 bash 脚本实现,无需任何服务,离线运行。
脚本处理细节
1. 时间范围过滤
case "$PERIOD" in
today)
SINCE=$(date -u +"%Y-%m-%dT00:00:00")
;;
week)
# macOS 用 -v-7d,Linux 用 -d "7 days ago",两者兼容
SINCE=$(date -u -v-7d +"%Y-%m-%dT00:00:00" 2>/dev/null \
|| date -u -d "7 days ago" +"%Y-%m-%dT00:00:00")
;;
all)
SINCE="1970-01-01T00:00:00"
;;
esac
时间比较直接利用 ISO 8601 字符串的字典序(timestamp >= $since),jq 里字符串比较就够用,不需要转时间戳。
2. jq 提取:一次扫描,输出 CSV 流
RAW=$(echo "$FILES" | xargs jq -r --arg since "$SINCE" '
select(
.type == "assistant"
and .message.usage != null
and .timestamp != null
and .timestamp >= $since
) |
[
(.message.model // "unknown"),
(.message.usage.input_tokens // 0),
(.message.usage.output_tokens // 0),
(.message.usage.cache_read_input_tokens // 0),
(.message.usage.cache_creation.ephemeral_5m_input_tokens // 0),
(.message.usage.cache_creation.ephemeral_1h_input_tokens // 0),
1
] | @csv
' 2>/dev/null)
几个细节值得注意:
// 0是 jq 的 alternative 运算符,字段缺失时取 0,避免 null 污染后续 awk 计算- 最后一列固定为
1,用来在 awk 里直接累加计数,不需要额外的 NR 逻辑 @csv自动处理模型名里可能含逗号的情况(用引号包裹字符串字段)2>/dev/null静默掉损坏的 JSONL 行(被中断的请求可能写了不完整的 JSON)
3. awk 分组聚合
MODEL_LINES=$(echo "$RAW" | awk -F, '
{
gsub(/"/, "", $1) # 去掉 @csv 给模型名加的引号
m = $1
mi[m] += $2 # input tokens
mo[m] += $3 # output tokens
mch[m] += $4 # cache hit
mc5[m] += $5 # cache write 5m
mc1[m] += $6 # cache write 1h
mcalls[m] += $7 # call count
}
END {
for (m in mcalls) print m, mi[m], mo[m], mch[m], mc5[m], mc1[m], mcalls[m]
}
' | sort)
用关联数组(awk 的 array[key])按模型名分组,O(n) 扫描完成聚合。
4. 价格表查询:bash 函数 + case
model_price() {
local m="$1"
case "$m" in
*opus*) echo "5.00 25.00 0.50 6.25 10.00" ;;
*haiku*) echo "1.00 5.00 0.10 1.25 2.00" ;;
*gemini*) echo "0 0 0 0 0" ;;
*) echo "3.00 15.00 0.30 3.75 6.00" ;; # Sonnet / 未知
esac
}
glob 模式匹配(*opus*)可以同时覆盖 claude-opus-4-5、claude-opus-4-6 等版本变体。Gemini 走的是 Google 的计费体系,统一返回 0 不影响总计。
5. 浮点计算:全部交给 awk
bash 本身不支持浮点,费用计算全部通过 awk BEGIN 块完成:
mcost=$(awk -v i="$mi" -v o="$mo" -v ch="$mch" -v c5="$mc5" -v c1="$mc1" \
-v pi="$pi" -v po="$po" -v ph="$ph" -v p5="$p5" -v p1="$p1" '
BEGIN { printf "%.4f", (i*pi + o*po + ch*ph + c5*p5 + c1*p1)/1000000 }')
累加同理,避免用 bc 或 Python(减少依赖):
TOTAL_COST=$(awk -v a="$TOTAL_COST" -v b="$mcost" 'BEGIN{printf "%.4f", a+b}')
6. 数字千位分隔符格式化
fmt() {
printf '%d\n' "$1" | awk '
{
while(length($0)>3) {
s = "," substr($0, length($0)-2) s
$0 = substr($0, 1, length($0)-3)
}
print $0 s
}'
}
纯 awk 实现千位分组,不依赖 numfmt 或 printf "%'d"(macOS 的 printf 不支持 ' 修饰符)。
最终输出效果
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Claude Token Usage — Last 7 days
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
API calls: 1,847
Input tokens: 28,431
Output tokens: 312,058
Cache hit tokens: 21,493,612
Cache write 5m tokens: 1,876,234
Cache write 1h tokens: 0
Total input: 23,398,277
Est. cost (USD): $18.4231
By model:
claude-haiku-4-5 calls=48 $0.2134
claude-haiku-4-6 calls=23 $0.1027
claude-sonnet-4-5 calls=201 $2.8841
claude-sonnet-4-6 calls=1571 $15.1823
claude-sonnet-4-7 calls=4 $0.0406
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
计费公式汇总
每个模型的费用(以 Sonnet 为例):
cost = input_tokens × $3.00 / 1,000,000
+ output_tokens × $15.00 / 1,000,000
+ cache_read_input_tokens × $0.30 / 1,000,000 ← 0.1× input
+ cache_write_5m_tokens × $3.75 / 1,000,000 ← 1.25× input
+ cache_write_1h_tokens × $6.00 / 1,000,000 ← 2× input
total_cost = Σ cost(model_i)
各模型完整定价(per 1M tokens):
| 模型 | Input | Output | Cache Read | Cache Write 5m | Cache Write 1h |
|---|---|---|---|---|---|
| Opus 4.x | $5.00 | $25.00 | $0.50 | $6.25 | $10.00 |
| Sonnet 4.x | $3.00 | $15.00 | $0.30 | $3.75 | $6.00 |
| Haiku 4.x | $1.00 | $5.00 | $0.10 | $1.25 | $2.00 |
| Gemini | — | — | — | — | — |
定价来源:Anthropic 官方文档 — Prompt Caching 模型匹配规则:名称含
opus→ Opus,含haiku→ Haiku,含gemini→ 不计费,其余默认 Sonnet。
后记:这篇文章本身消耗了多少 Token?
这篇文章从构思、探索代码、到写作,全程由 Claude Code 完成。写完之后顺手跑了一下脚本:
由于今天还有其他项目在使用,直接看 today 数据会混入无关的对话。脚本支持的最小粒度是天,所以临时用 jq 过滤最近 1 小时:
SINCE=$(date -u -v-1H +"%Y-%m-%dT%H:%M:%S")
find ~/.claude/projects -name "*.jsonl" | xargs jq -r --arg since "$SINCE" '
select(
.type == "assistant"
and .message.usage != null
and .timestamp >= $since
) |
[(.message.model // "unknown"),
(.message.usage.input_tokens // 0),
(.message.usage.output_tokens // 0),
(.message.usage.cache_read_input_tokens // 0),
(.message.usage.cache_creation.ephemeral_5m_input_tokens // 0),
(.message.usage.cache_creation.ephemeral_1h_input_tokens // 0),
1] | @csv
' | awk -F, '
{
gsub(/"/, "", $1)
input += $2; output += $3; cache_hit += $4; cache_5m += $5; calls += $7
}
END {
cost = (input*3 + output*15 + cache_hit*0.3 + cache_5m*3.75) / 1000000
printf "Calls: %d Output: %d Cache hit: %d Est cost: $%.4f\n",
calls, output, cache_hit, cost
}'
结果:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Claude Token Usage — Last 1 hour
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
API calls: 30
Input tokens: 644
Output tokens: 13,102
Cache hit tokens: 512,694
Cache write 5m tokens: 115,809
Cache write 1h tokens: 0
Total input: 629,147
Est. cost (USD): $0.7866
By model:
claude-sonnet-4-6 calls=30 $0.7866
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
写这篇文章:30 次 API 调用,约 $0.79。
输出了 13,102 个 token 的文章内容,背后是 51 万 cache hit token 在撑着上下文——每轮对话都把整个代码文件、对话历史带进来,命中缓存只收 0.1× 的价格,否则费用会高出数倍。
用这个脚本统计写这篇介绍这个脚本的文章的花费——还挺有递归的意思。
数据局限性说明
这套方案的边界很清楚:
- 只统计本机:其他设备的对话不在
~/.claude/projects/里 - 不含网页版:claude.ai 的对话不写本地日志
- 被中断的请求(约 1~2%)没有
usage字段,不计入 - AWS Bedrock 定价与 Anthropic API 基本一致,部分区域可能有轻微差异
与 claude-hud 的定位差异
| claude-hud | token-usage skill | |
|---|---|---|
| 适用计费方式 | Claude.ai Pro/Max 订阅额度 | Anthropic API / AWS Bedrock |
| 数据粒度 | 当前 session 实时 | today / week / month / all |
| 项目过滤 | 无 | --project <name> |
| 运行方式 | 终端状态栏实时显示 | 按需查询 |
| 离线可用 | 是 | 是 |
两个工具并不冲突,各自解决不同场景的问题。
依赖
bash— macOS / Linux 内置jq—brew install jq或apt install jqawk— 系统内置
整个实现不到 160 行 bash,没有 Python、Node 或其他运行时依赖。
完整代码
#!/usr/bin/env bash
# Claude Code Token Usage Statistics
# Usage: token-stats.sh [today|week|month|all] [--project <name>]
CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
PERIOD="${1:-today}"
PROJECT_FILTER=""
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--project|-p) PROJECT_FILTER="$2"; shift 2 ;;
today|week|month|all) PERIOD="$1"; shift ;;
*) shift ;;
esac
done
# Determine date cutoff
case "$PERIOD" in
today)
SINCE=$(date -u +"%Y-%m-%dT00:00:00")
LABEL="Today"
;;
week)
SINCE=$(date -u -v-7d +"%Y-%m-%dT00:00:00" 2>/dev/null || date -u -d "7 days ago" +"%Y-%m-%dT00:00:00")
LABEL="Last 7 days"
;;
month)
SINCE=$(date -u -v-30d +"%Y-%m-%dT00:00:00" 2>/dev/null || date -u -d "30 days ago" +"%Y-%m-%dT00:00:00")
LABEL="Last 30 days"
;;
all)
SINCE="1970-01-01T00:00:00"
LABEL="All time"
;;
esac
# Find relevant jsonl files
if [[ -n "$PROJECT_FILTER" ]]; then
FILES=$(find "$CLAUDE_DIR/projects" -path "*${PROJECT_FILTER}*" -name "*.jsonl" 2>/dev/null)
else
FILES=$(find "$CLAUDE_DIR/projects" -name "*.jsonl" 2>/dev/null)
fi
if [[ -z "$FILES" ]]; then
echo "No data found."
exit 0
fi
# Aggregate stats via jq, grouped by model
# Fields: model, input, output, cache_hit, cache_write_5m, cache_write_1h
RAW=$(echo "$FILES" | xargs jq -r --arg since "$SINCE" '
select(
.type == "assistant"
and .message.usage != null
and .timestamp != null
and .timestamp >= $since
) |
[
(.message.model // "unknown"),
(.message.usage.input_tokens // 0),
(.message.usage.output_tokens // 0),
(.message.usage.cache_read_input_tokens // 0),
(.message.usage.cache_creation.ephemeral_5m_input_tokens // 0),
(.message.usage.cache_creation.ephemeral_1h_input_tokens // 0),
1
] | @csv
' 2>/dev/null)
# Sum totals across all models
STATS=$(echo "$RAW" | awk -F, '
{
gsub(/"/, "", $1)
input += $2
output += $3
cache_hit += $4
cache_5m += $5
cache_1h += $6
calls += $7
}
END { print input, output, cache_hit, cache_5m, cache_1h, calls }
')
read -r INPUT OUTPUT CACHE_HIT CACHE_5M CACHE_1H CALLS <<< "$STATS"
CACHE_NEW=$((CACHE_5M + CACHE_1H))
TOTAL_INPUT=$((INPUT + CACHE_HIT + CACHE_NEW))
# Per-model pricing (Anthropic API / AWS Bedrock us-east-1, per 1M tokens)
# Returns: "in out hit w5m w1h" for a given model string
model_price() {
local m="$1"
case "$m" in
*opus*) echo "5.00 25.00 0.50 6.25 10.00" ;;
*haiku*) echo "1.00 5.00 0.10 1.25 2.00" ;;
*gemini*) echo "0 0 0 0 0" ;; # Gemini: no Anthropic billing
*) echo "3.00 15.00 0.30 3.75 6.00" ;; # sonnet / unknown
esac
}
# Per-model cost breakdown
MODEL_LINES=$(echo "$RAW" | awk -F, '
{
gsub(/"/, "", $1)
m = $1
mi[m] += $2; mo[m] += $3; mch[m] += $4
mc5[m] += $5; mc1[m] += $6; mcalls[m] += $7
}
END {
for (m in mcalls) print m, mi[m], mo[m], mch[m], mc5[m], mc1[m], mcalls[m]
}
' | sort)
# Compute total cost by summing per-model costs
TOTAL_COST=0
MODEL_COST_LINES=""
while IFS= read -r line; do
m=$(echo "$line" | awk '{print $1}')
mi=$(echo "$line" | awk '{print $2}')
mo=$(echo "$line" | awk '{print $3}')
mch=$(echo "$line" | awk '{print $4}')
mc5=$(echo "$line" | awk '{print $5}')
mc1=$(echo "$line" | awk '{print $6}')
mcalls=$(echo "$line" | awk '{print $7}')
prices=$(model_price "$m")
pi=$(echo $prices | awk '{print $1}')
po=$(echo $prices | awk '{print $2}')
ph=$(echo $prices | awk '{print $3}')
p5=$(echo $prices | awk '{print $4}')
p1=$(echo $prices | awk '{print $5}')
mcost=$(awk -v i="$mi" -v o="$mo" -v ch="$mch" -v c5="$mc5" -v c1="$mc1" \
-v pi="$pi" -v po="$po" -v ph="$ph" -v p5="$p5" -v p1="$p1" '
BEGIN { printf "%.4f", (i*pi + o*po + ch*ph + c5*p5 + c1*p1)/1000000 }')
TOTAL_COST=$(awk -v a="$TOTAL_COST" -v b="$mcost" 'BEGIN{printf "%.4f", a+b}')
MODEL_COST_LINES="${MODEL_COST_LINES} ${m} calls=${mcalls} \$${mcost}\n"
done <<< "$MODEL_LINES"
fmt() { printf '%d\n' "$1" | awk '{while(length($0)>3){s=","substr($0,length($0)-2)s;$0=substr($0,1,length($0)-3)}print $0 s}'; }
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " Claude Token Usage — $LABEL"
[[ -n "$PROJECT_FILTER" ]] && echo " Project: $PROJECT_FILTER"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
printf " %-26s %s\n" "API calls:" "$CALLS"
echo ""
printf " %-26s %s\n" "Input tokens:" "$(fmt $INPUT)"
printf " %-26s %s\n" "Output tokens:" "$(fmt $OUTPUT)"
printf " %-26s %s\n" "Cache hit tokens:" "$(fmt $CACHE_HIT)"
printf " %-26s %s\n" "Cache write 5m tokens:" "$(fmt $CACHE_5M)"
printf " %-26s %s\n" "Cache write 1h tokens:" "$(fmt $CACHE_1H)"
printf " %-26s %s\n" "Total input:" "$(fmt $TOTAL_INPUT)"
echo ""
printf " %-26s \$%s\n" "Est. cost (USD):" "$TOTAL_COST"
echo ""
echo " By model:"
printf "%b" "$MODEL_COST_LINES"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
使用方法
安装依赖
# macOS
brew install jq
# Ubuntu / Debian
apt install jq
下载脚本
mkdir -p ~/.claude/skills/token-usage/scripts
curl -o ~/.claude/skills/token-usage/scripts/token-stats.sh \
https://raw.githubusercontent.com/your-repo/token-stats.sh
chmod +x ~/.claude/skills/token-usage/scripts/token-stats.sh
或者直接把上面的完整代码粘贴到 ~/.claude/skills/token-usage/scripts/token-stats.sh,然后 chmod +x。
运行示例
# 查看今天的用量(默认)
bash ~/.claude/skills/token-usage/scripts/token-stats.sh
# 查看本周
bash ~/.claude/skills/token-usage/scripts/token-stats.sh week
# 查看近 30 天
bash ~/.claude/skills/token-usage/scripts/token-stats.sh month
# 查看全部历史
bash ~/.claude/skills/token-usage/scripts/token-stats.sh all
# 按项目过滤(支持部分匹配)
bash ~/.claude/skills/token-usage/scripts/token-stats.sh week --project api-service
# 缩写形式
bash ~/.claude/skills/token-usage/scripts/token-stats.sh month -p dashboard
加个别名方便调用
在 ~/.zshrc 或 ~/.bashrc 里加一行:
alias token-usage='bash ~/.claude/skills/token-usage/scripts/token-stats.sh'
之后直接:
token-usage # 今天
token-usage week # 本周
token-usage all -p fe-dashboard # 全部历史 × 指定项目
作为 Claude Code Skill 使用
如果你使用 Claude Code,可以把这个脚本注册为 skill,直接用自然语言触发:
- "查一下今天的 token 用量"
- "本周用了多少 token"
- "all time 的费用估算"
- "my-project 项目这周的用量"
Claude 会自动调用脚本并展示结果。