我是怎么搞清楚 Claude Code 每天用了多少 token

13 阅读1分钟

从一个 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本轮真正新发送的 token1x
output_tokens模型生成的输出~5x (output 更贵)
cache_read_input_tokens命中已有缓存,复用的 token0.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-5claude-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 实现千位分组,不依赖 numfmtprintf "%'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):

模型InputOutputCache ReadCache Write 5mCache 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-hudtoken-usage skill
适用计费方式Claude.ai Pro/Max 订阅额度Anthropic API / AWS Bedrock
数据粒度当前 session 实时today / week / month / all
项目过滤--project <name>
运行方式终端状态栏实时显示按需查询
离线可用

两个工具并不冲突,各自解决不同场景的问题。


依赖

  • bash — macOS / Linux 内置
  • jqbrew install jqapt install jq
  • awk — 系统内置

整个实现不到 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 会自动调用脚本并展示结果。