3行で
- 手元のセッションログ 389 ファイル・166 MB を走査した。
thinkingブロックは 6,258 件。そのうち 6,244 件(99.8%)は本文が空で、暗号署名だけが残っている。 - 署名はログ全体の **9.03%(15.0 MB)**を占める。読める思考の本文は 12,793 字、0.0077%。
- 本文が残っていたのは1つのモデルだけだった。他の5モデルは 0.0%、そのモデルだけ 100%。
ログを読んでも、なぜそうしたかは分からない
私はここ数週間、自分のセッションログを片っ端から集計している。何回シェルを打ったか、何件失敗したか、どのファイルを何回書き直したか。全部行動の記録だ。
判断のほうを読もうとして、初めて気づいた。入っていない。
ログ 389 ファイル / 166.0 MB
thinking ブロック 6258
本文が空 6244 = 99.8%
本文が残っている 14 = 0.2%
thinking ブロックは存在する。件数も 6,258 件ある。だが中身は空文字列だ。
{"type": "thinking", "thinking": "", "signature": "EqQECkYIBxgCKkD..."}
キーは3つ(type / thinking / signature)で、6,258 件すべてこの形をしている。thinking が空で signature が長い。
9% は暗号文
署名の総量を測ると、話の大きさが変わる。
ログに占める割合
署名 15.0 MB = 9.03%
本文 12,793 字 = 0.0077%
ファイル単位の署名比率 中央値 5.0% / 最大 53.0%
署名の長さ 中央値 1,116 / 最大 252,928
166 MB のログのうち 15.0 MB が署名だ。中央値で見るとファイルの 5.0%、最も偏ったファイルでは 53.0% — 半分以上が読めない文字列で埋まっている。
署名 1 件の長さは中央値 1,116 字、最大 252,928 字。1つの thinking ブロックが 4 分の 1 メガバイトある。
つまり**推論はログの中に在る。**消えているのではなく、読めない形で入っている。ディスクを食っているのに、grep にも jq にも何も返さない。
私は「思考はログに記録されない」と思っていたが、正確には違った。記録はされていて、復元できないだけだ。この2つは、ディスク使用量の話としても、監査の話としても、意味がまったく違う。
モデルで割ったら、きれいに割れた
バージョン別に見てもばらつかなかったので、モデル別に割った。
モデル別
claude-opus-5 4816 / 本文あり 0 = 0.0%
claude-opus-4-8 664 / 本文あり 0 = 0.0%
claude-fable-5 416 / 本文あり 0 = 0.0%
claude-sonnet-5 255 / 本文あり 0 = 0.0%
claude-opus-4-7 93 / 本文あり 0 = 0.0%
claude-sonnet-4-6 14 / 本文あり 14 = 100.0%
**5 モデルが 0.0%、1 モデルが 100.0%。**中間が無い。
これは「たまに残る」ではなく、モデルごとに挙動が切り替わっていることを示している。バージョンで割ったときは 2.1.216 にだけ 14 件が出て、CLI のバージョンの問題に見えた。だがその 14 件は全部同じモデルのもので、同じバージョンの他のモデルは 0 件だった。バージョンは相関していただけで、原因ではなかった。
ただし 14 件は 2 セッション分しかない。「このモデルは常に本文を残す」と言える量ではまったくない。言えるのは、0% と 100% を分ける何かがモデル側にあるということまでだ。
何が困るか
1. 事後にログから理由を復元できない。 「なぜこのファイルを消したのか」「なぜこの方針にしたのか」は、ログのどこにも無い。残っているのは、消したという事実と、Bash に渡したコマンド文字列だけだ。
2. 行動から推測するしかない。 私がこれまで書いてきた集計が全部「何をしたか」なのは、私がそう選んだからではない。それしか入っていなかったからだ。エージェントの挙動を分析しようとすると、自動的に行動分析になる。
3. 監査の粒度が決まってしまう。 「エージェントが何をしたか」は完全に追える。「なぜそうしたか」は一切追えない。この境界は、ログの設計によって最初から引かれている。
4. 保存容量の 9% が読めないもので埋まる。 ログを長期保管するなら、これは無視できない比率だ。行動だけを残したいなら、署名は落とせる。
理由が要るなら、**エージェント自身に書かせて残すしかない。**コミットメッセージ、決定ログ、## なぜこうしたか のセクション。それらは行動の記録として残る。思考の記録は残らない。
これは前に「拘束力の階層」で書いた話と同じ構造をしている。残ってほしいものは、残る形式で明示的に書かせないと残らない。
測るスクリプト
#!/usr/bin/env python3
"""セッションログに「思考」がどれだけ残っているかを測る。
python3 thinking_opacity.py <ログのルート>
thinking ブロックは本文(thinking)と署名(signature)を持つ。
本文が空で署名だけが残っている場合、そのログから推論の中身は読めない。
"""
import sys, os, json, glob, collections, statistics as st
def main(root):
files = glob.glob(os.path.join(root, "**", "*.jsonl"), recursive=True)
total_bytes = sig_bytes = body_chars = 0
blocks = empty = 0
by_model = collections.Counter()
by_model_body = collections.Counter()
sig_lens = []
ratios = []
for f in files:
try:
size = os.path.getsize(f)
except OSError:
continue
total_bytes += size
file_sig = 0
for ln in open(f, errors="replace"):
try:
d = json.loads(ln)
except json.JSONDecodeError:
continue
msg = d.get("message") or {}
content = msg.get("content")
if not isinstance(content, list):
continue
for b in content:
if not (isinstance(b, dict) and b.get("type") == "thinking"):
continue
blocks += 1
model = msg.get("model") or "?"
by_model[model] += 1
body = b.get("thinking") or ""
sig = b.get("signature") or ""
file_sig += len(sig)
sig_lens.append(len(sig))
if body.strip():
by_model_body[model] += 1
body_chars += len(body)
else:
empty += 1
sig_bytes += file_sig
if size:
ratios.append(100 * file_sig / size)
if not blocks:
print("thinking ブロックが1件も無い")
return 1
print(f"ログ {len(files)} ファイル / {total_bytes / 1e6:.1f} MB")
print(f"thinking ブロック {blocks}")
print(f" 本文が空 {empty:5d} = {100 * empty / blocks:5.1f}%")
print(f" 本文が残っている {blocks - empty:5d} = {100 * (blocks - empty) / blocks:5.1f}%")
print()
print("ログに占める割合")
print(f" 署名 {sig_bytes / 1e6:6.1f} MB = {100 * sig_bytes / total_bytes:6.2f}%")
print(f" 本文 {body_chars:9,} 字 = {100 * body_chars / total_bytes:6.4f}%")
print(f" ファイル単位の署名比率 中央値 {st.median(ratios):.1f}% / 最大 {max(ratios):.1f}%")
print(f" 署名の長さ 中央値 {st.median(sig_lens):,.0f} / 最大 {max(sig_lens):,}")
print()
print("モデル別")
for model, c in by_model.most_common():
n = by_model_body.get(model, 0)
print(f" {model:22s} {c:5d} / 本文あり {n:4d} = {100 * n / c:5.1f}%")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else
os.path.expanduser("~/.claude/projects")))
python3 thinking_opacity.py ~/.claude/projects
自分のログの何割が読めない領域なのかは、これで出る。
この記事の限界
- 1人分のログである。389 ファイル・166 MB。設定(拡張思考の有無など)が違えば分布は変わる。
-
本文が残っていた 14 件は 2 セッション分だけ。「
claude-sonnet-4-6は本文を残す」という一般化はできない。0% と 100% がモデル境界で分かれたという観察までが言えることで、その原因は特定していない。 - **署名の中身が推論そのものかは確かめていない。**復元できないので確かめようがない。長さの分布が thinking の長さらしく振る舞うという状況証拠しかない。
- ログの形式は変わりうる。この観察は 2026 年 8 月時点、CLI
2.1.212〜2.1.234のものだ。 - **署名を落として保存する運用を試してはいない。**再開や再送に必要な可能性があるので、消して安全かは検証していない。
まとめ
-
thinkingブロック 6,258 件のうち 99.8% は本文が空。署名だけが残る。 - 署名はログの 9.03%(15.0 MB)。ファイルによっては 53%。読める思考の本文は 0.0077%。
- **5 モデルが 0.0%、1 モデルが 100.0%。**中間が無い。ただし後者は 2 セッション分。
- セッションログから追えるのは行動だけ。理由が必要なら、エージェントに書かせて別途残すしかない。
エージェントのログ分析が「何をしたか」に寄るのは、分析者の趣味ではない。それしか入っていない。
関連
理由がログに残らないなら、規約が効いたかどうかも行動でしか測れません。「読んだ上で従わなかった」と「そもそも届いていなかった」は、ログ上で区別がつかない。
届いているかをどう測るかと、効かなくなる4つのパターンをまとめた本があります。
→ AIコンテキスト設計ガイド ── 規約を配ったのに守られない理由(2,000円)
第3章「規約が効かなくなる4つのパターン」まで無料で読めます。
/compact の後もルートの CLAUDE.md は再注入されるが、サブディレクトリと paths: 付きは再注入されない ── 長いセッションで静かに落ちるのはどちらか、という章です。