こんにちは、もんすんです。
今回は、Claude Code を使っていて、自分がどれだけ使ったかをログから確認してみたい人向けの記事です。
Claude Code のログを1行ずつ足していくと、使ったトークンが2倍以上に水増しされます。
私のログでは、出力トークンが 2.49 倍になりました。
原因は、ログの1行を「AI の返事1回」だと思って数えてしまうことです。
数える単位は行ではなく、メッセージの識別子 message.id でした。
この記事で分かることは、次の3つです。
- ログの1行が、なぜ返事1回にならないのか
- 「最初の行」「最後の行」「最大値」のどれで数えると合うか。私のログでの実測
- 自分のログで確かめられる Python のコード
トークンは、AI が文章を数える単位です。
また、JSONL は、1行が1つの JSON になっているファイルです。
JSON は、項目と値を組にして書くデータの形式です。
この記事では、AI が1回返した応答を「メッセージ」と呼びます。
Claude Code のバージョンは 2.1.310 です。
私の手元のログには、2.1.261 から 2.1.310 までの8種類のバージョンが記録されていました。
仕様は変わるので、最新は公式で確認してください。
古いログは消えるので、再実行すると行数は変わります。
もし、同じように試す方はこの点に注意してください。
何を作っていて、なぜ計測したのか
私は Claude Code に頼んで、使ったトークンの累計を「経験値」にして、画面にゲームのレベルのように出す自作のプラグインを作ってもらいました。
| インプット待ち | Claude Code稼働中 |
|---|---|
![]() |
![]() |
トークンの累計を出すには、過去の使用量を数えなくてはなりません。
そこで同じ日の2つ目のコミットで、ログから過去分を集計するスクリプトを足してもらいました。
この累計が水増しされていたら、経験値の意味がなくなります。
そこで、ログがどういう形で残っていて、どう数えれば合うのかを確かめました。
Claude Code は、会話をパソコンの ~/.claude/projects/ の下に JSONL で保存します1。
公式のドキュメントには、30日の保存期間を変える設定 cleanupPeriodDays が載っています1。
私のパソコンには134ファイルが残っていて、いちばん古い行は 2026/09/26 でした。
ログの1行は、メッセージ1つではない
ログの中で AI の返事にあたる行には、使ったトークンの記録 usage が入っています。
公式の説明では、入力側は3つに分かれます2。
| 項目 | 公式の意味 |
|---|---|
input_tokens |
キャッシュの区切りより後ろの入力 |
cache_creation_input_tokens |
キャッシュに書き込んだ分 |
cache_read_input_tokens |
キャッシュから読み出した分 |
output_tokens |
出力。AI の内部の考えである思考トークンも含まれる3 |
キャッシュは、前の会話の内容を使い回して、処理を軽くする仕組みです。
キャッシュの区切りは、ここまでを使い回す、という目印の位置です。
この usage を、そのまま行ごとに足せばよさそうに見えます。
ところが、同じメッセージの行を、中身は表示せず数字だけで取り出すと、次のようになりました。
行 128: thinking 入力 2 / キャッシュ書き込み 1455 / キャッシュ読み出し 71374 / 出力 492
行 129: text 入力 2 / キャッシュ書き込み 1455 / キャッシュ読み出し 71374 / 出力 492
行 130: tool_use 入力 2 / キャッシュ書き込み 1455 / キャッシュ読み出し 71374 / 出力 492
3行とも、同じ数字です。
1回の返事は、考えた内容の thinking、文章の text、コマンド実行などの呼び出しの tool_use というブロックに分かれ、ブロックごとに1行ずつ書かれます。
そして、どの行にも、返事全体の usage が入っています。
たとえるなら、買い物のレシートが3枚に分かれていて、どのレシートの末尾にも「お会計全体の合計」が書かれているような状態です。
3枚の合計欄を足せば、お会計は3倍になってしまいます。
私のログでは、7,138行がすべて1行に1ブロックでした。
メッセージの識別子は message.id に入っていて、2行以上に分かれたメッセージが2,435件ありました。
そのすべてで、入力側の3項目は全部の行で同じ値でした。
出力だけは、同じ値の行が多いものの、途中の小さな値が入った行もありました。
全行を足すと 2.49 倍
では、実際に足すとどうなるのか。
次のコードで、まず message.id ごとに行をまとめます。
そのうえで、出力トークンを4通りの数え方で合計します。
読み取りだけで、ログは書き換えません。
import glob, json, os
from collections import defaultdict
by_id = defaultdict(list) # message.id -> その id が付いた各行の usage
for path in glob.glob(os.path.expanduser('~/.claude/projects/**/*.jsonl'), recursive=True):
with open(path, errors='replace') as f:
for raw in f:
try:
row = json.loads(raw)
except ValueError:
continue
msg = row.get('message')
if row.get('type') == 'assistant' and isinstance(msg, dict) and msg.get('id') and msg.get('usage'):
by_id[msg['id']].append(msg['usage'])
def out(u):
return u.get('output_tokens', 0)
print(f'行数 {sum(len(v) for v in by_id.values()):,} / メッセージ数 {len(by_id):,}')
for name, total in [
('全行を足す', sum(out(u) for v in by_id.values() for u in v)),
('各 id の最初の行', sum(out(v[0]) for v in by_id.values())),
('各 id の最後の行', sum(out(v[-1]) for v in by_id.values())),
('各 id の最大値', sum(max(out(u) for u in v) for v in by_id.values())),
]:
print(f'{name}: {total:,}')
私の環境での出力は、次のとおりです。
行数 7,138 / メッセージ数 3,224
全行を足す: 11,862,974
各 id の最初の行: 4,214,256
各 id の最後の行: 4,749,461
各 id の最大値: 4,759,462
行数はメッセージ数の2.2倍でした。
同じメッセージが平均2.2回ずつ数えられているということです。
全行を足した合計は、最大値で数えた合計の 2.49 倍でした。
2.2倍より大きいのは、行が多く分かれたメッセージほど出力が大きいためです。
出力の平均は、1行のメッセージで426、2行で1,630、3行以上で2,100トークンでした。
では、どの行を取れば合うのか
「全行を足す」がだめなことは分かりました。
残る候補は、各 message.id の「最初の行」「最後の行」「最大値」の3つです。
| 数え方 | 出力トークン | 最大値との差 |
|---|---|---|
| 全行を足す | 11,862,974 | +7,103,512 |
| 各 id の最初の行 | 4,214,256 | −545,206 |
| 各 id の最後の行 | 4,749,461 | −10,001 |
| 各 id の最大値 | 4,759,462 | 0 |
最初の行は外れます。
最初の行が最大値より小さいメッセージが307件ありました。
たとえば、7行に分かれているある1つのメッセージでは、出力が先頭の4行で 7、後ろの3行で 795 でした。
途中の値が書かれた行が混ざることがあり、その理由は確認していません。
最後の行は、ほぼ合います。
差の10,001は、複数のファイルに同じメッセージが現れた5件だけから生じていました。
そこでは、最後の行だけに小さな値が入っている例があり、たとえば出力の並びが 1711、1711、1711、8 でした。
公式では、/branch で会話の履歴がコピーされると書いています1。
ただし、この5件の原因が /branch かどうかは確認していません。
この5件では、ファイルを読む順番で「最後の行」が変わるため、環境によっては同じ差が出ないことがあります。
行の位置に頼る数え方は、ログの書かれ方に左右されます。
最大値なら、行の順番に関係なく、返事全体の出力を拾えます。
4項目すべてで同じことが起きる
出力だけでなく、入力側の3項目も、全行を足すと膨らみます。
最大値の行を1つ選び、その行の4項目をまとめて使う形にすると、次のコードで比べられます。
上のコードの out の定義までを、そのまま使います。
KEYS = ['input_tokens', 'output_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens']
every = {k: sum(u.get(k, 0) for v in by_id.values() for u in v) for k in KEYS}
once = {k: sum(max(v, key=out).get(k, 0) for v in by_id.values()) for k in KEYS}
for k in KEYS:
print(f'{k}: {every[k]:,} → {once[k]:,} {every[k] / once[k]:.2f}倍')
input_tokens: 14,969 → 6,757 2.22倍
output_tokens: 11,862,974 → 4,759,462 2.49倍
cache_creation_input_tokens: 94,958,487 → 33,496,745 2.83倍
cache_read_input_tokens: 1,376,860,150 → 596,811,061 2.31倍
どの項目も、2.2倍から2.8倍に膨らんでいました。
入力側の3項目は、同じメッセージの行で値が同じなので、どの行を取っても結果は変わりません。
出力だけは行によって値が違うため、最大値の行を選びます。
私の自作プラグインの集計スクリプトも、同じ message.id は1回だけ数え、出力が大きい方の usage を残す作りでした。
ログを直接読むときの注意
まとめ
ログの1行は、返事1回ではありません。
数える単位は、行ではなく message.id です。
| 知りたいこと | 分かったこと | 根拠 |
|---|---|---|
| ログの1行は何か | 返事のブロックごとに1行で、入力側の3項目はどの行も同じ。出力は行で違うことがある | 手元のログ7,138行 |
| 全行を足すとどうなるか | 出力が2.49倍、入力側も2.2倍から2.8倍に膨らむ | 手元のログの集計 |
| どう数えるか |
message.id ごとに1回。出力は最大値の行 |
4通りの数え方の比較 |
| 注意点 | ログの形式は内部仕様で、変わりうる | 公式ドキュメント |
もし試す際は、自分の ~/.claude/projects/ に上のコードを当てて、行数とメッセージ数の差を確かめてください。
倍率が1.0倍に近ければ、単純に足しても大きくは外れません。
参考
-
Anthropic, Claude Code Docs, Manage sessions の Where transcripts are stored、Branch a session、Access conversations from scripts, 2026/10/30 確認 https://code.claude.com/docs/en/sessions ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Anthropic, Claude API Docs, Prompt caching の Tracking cache performance, 2026/10/30 確認 https://platform.claude.com/docs/en/build-with-claude/prompt-caching ↩
-
Anthropic, Claude API Docs, Extended thinking, 2026/10/30 確認 https://platform.claude.com/docs/en/build-with-claude/extended-thinking ↩

