エージェントが妙な変更をしたので「なんでこうしたの?」と聞くと、謝られてしまう。謝ってほしいわけではなく、その変更を選んだ理由が知りたいのですが。
あとから説明してもらうより、作業中の記録を読めば分かるかもしれない。そう思って Claude Code のログを調べたら、思考の要約を入れる thinking 欄は 6,319 件すべて空でした。振り返るための中身が、そもそも残っていませんでした。
設定を渡した対話セッションでは、要約が残りました。ただし -p で対話せずに実行すると、空のまま。これは2026年8月15日、Claude Code 2.1.233で確かめた結果です。現在のバージョンでも同じとは限らないので、まず手元のログに何が残っているかを確認する手順を追記しました。
自分のログに、読める要約があるか調べる
Claude Codeのセッションログは ~/.claude/projects/<プロジェクト>/<セッションID>.jsonl にあります。振り返りたいセッションのファイルを一つ選びます。
件数を数えるjqファイルを thinking-counts.jq という名前で保存し、jqが使える環境で次のコマンドを実行してください。LOG には、選んだファイルの実際のパスを入れます。ログを読み取るだけで、設定や元のファイルは変更しません。
LOG='/path/to/session.jsonl'
jq -s -f thinking-counts.jq "$LOG"
出力は次の形です。これは、空の thinking を3件入れた確認用データで、2026年9月22日に実行した例です。冒頭の6,319件の再集計ではありません。
{
"thinking_blocks": 3,
"empty_blocks": 3,
"with_summary": 0,
"missing_or_nontext": 0
}
| 出力 | 読み取れること |
|---|---|
empty_blocks が thinking_blocks と同じで、0より大きい |
見つかったブロックはすべて空。読み返せる要約はない |
with_summary が1以上 |
文字列の入った要約がある。対象の変更を扱っているか、内容を読む |
thinking_blocks が0 |
そのファイルでは対象が見つからない。セッションやログの形式が合っているかを確認する |
missing_or_nontext が1以上 |
欄がないか文字列以外のブロックがある。空の要約とは分けて調べる |
「対象が見つからない」と「見つかったが空だった」は違います。また、空だったというだけで、AIが考えずに答えたとは判断できません。集計するのは、ログから読める要約があるかどうかです。
要約を残す設定は、短い作業で確かめてから使う
試すきっかけは、@golden_lucky さんの「理由を聞いているのに謝られる」という投稿と、それに続く@Hi_Noguchi さんの投稿でした。後者で、showThinkingSummaries を有効にして要約を残し、振り返る方法が紹介されていました(ともに2026-08-15取得)。
当時は、次のように設定を一時的に渡して対話モードを起動しました。
mkdir -p /tmp/thinking-test
cd /tmp/thinking-test
claude --settings '{"showThinkingSummaries":true}'
「1から20までの素数の和を求めて。方針を2つ比べてから決めて。」と入力して終了すると、215文字の要約が残りました。手で列挙する方法とふるいを使う方法を比べ、小さい範囲なので列挙を選ぶ、という内容です。
| Claude Code 2.1.233での実行 | thinking の結果 |
|---|---|
| 設定を有効にした対話モード | 1件、215文字 |
設定なしで -p を実行 |
1件、0文字 |
同じ設定を渡して -p を実行 |
1件、0文字 |
設定なしで使っていた対話セッションの6,319件も空でしたが、そちらは質問やプロジェクトまで揃えた比較ではありません。確認できたのは、設定を渡した対話セッションでは要約が残り、-p では同じ設定でも空だった、という範囲です。
自分の環境でも、記録を残したい作業を始める前に、短い依頼を一つ出してログを見ます。設定を渡せたことと、要約が保存されたことは別なので、先ほどの集計で確かめます。後から設定しても、過去の空だった分は戻りません。投稿にあった、要約をDuckDBやhooksへ流して振り返らせる運用は試していません。
変更理由は、仕様や差分とも突き合わせる
私が知りたいのは、何を読んで、その変更が必要だと判断したのかです。要約に「こちらが適切だと考えた」と書かれていても、それだけでは確かめられません。参照した仕様、問題を再現した入力、実際の変更箇所が必要です。
依頼に「変更ごとに、その根拠を1行で添えて」と入れるなら、何を根拠として書いてほしいかまで指定したほうが、後で確認しやすそうです。次は、たとえばこう頼みます。
変更ごとに、次の内容を短く残してください。
・直す必要があると判断した仕様、または問題を再現した入力と結果
・それに対応する変更箇所
・修正後に実行した確認と、その結果
ファイルを参照したら、パスと該当箇所も添えてください。
まだ実行していない確認は「未実行」と書いてください。
当時渡した指示の引用ではなく、今回の振り返りから作った依頼例です。この頼み方で確認の手間や誤りが減るかは、まだ測っていません。書かれた説明と、差分・実行結果が合うかを見るためのものです。
要約が読めても、思考そのものの答え合わせはできない
Anthropic の Thinking によると、利用者が読める thinking の文字列は推論の要約です。display: "summarized" なら要約が返り、"omitted" なら欄は空になります(2026-09-22再確認)。
モデルによっては前の思考を次の応答でも保持します。「過去の思考がなくなるから、理由を答えられない」と一括りにはできません。ただ、利用者が読めるのは要約までなので、後から返った説明を元の思考と直接照合することもできません。
Anthropic の「Reasoning models don’t always say what they think」では、ヒントで答えが変わったとき、思考の中でそのヒントに触れた割合を測っています。ヒントの種類をまたいだ平均は、Claude 3.7 Sonnetで25%、DeepSeek R1で39%でした(2026-09-22再確認)。新しいモデルでの測定でも、後から理由を尋ねた実験でもありません。
使った手がかりが、読める記録に全部書かれるとは限らない。理由が一つ書かれていると、それで納得してしまうのは避けたいです。気になる変更があれば、記録を手がかりに、参照したものと実際の差分を開くところまでやります。
6,319件を数えたときの記録
Claude Code のセッションログは ~/.claude/projects/<プロジェクト>/<セッションID>.jsonl にあります。thinking ブロックを取り出し、中身の文字数を数えました。
以下は2026年8月15日の集計と出力です。$LOG は調べたいセッション、$A は当時の設定なしの実験で作られたjsonlのパスを指します。今回追加した件数集計とは別のコマンドです。
$ cd ~/.claude/projects
$ find . -name '*.jsonl' -newermt '2026-08-01' | wc -l
423
$ find . -name '*.jsonl' -newermt '2026-08-01' -print0 | xargs -0 \
jq -r 'select(.message.content? | type=="array") | .message.content[]
| select(.type=="thinking") | (.thinking|length)' | sort -n | uniq -c
6319 0
左が件数、右が文字数。6,319 個すべてが 0 文字でした。ブロックには署名だけが残っています。次の sig_len は .signature の長さを jq で計算した値です。
$ jq -c 'select(.message.content? | type=="array") | .message.content[]
| select(.type=="thinking") | {type, thinking, sig_len:(.signature|length)}' "$A"
{"type":"thinking","thinking":"","sig_len":864}
この記事を書いたセッションでは、ツールの利用記録は残っていました。
$ jq -r 'select(.type=="assistant") | .message.content[]?
| select(.type=="tool_use") | .name' $LOG | sort | uniq -c | sort -rn
57 Bash
4 Edit
3 Read
3 Agent
1 Write
1 Skill
コマンド、読んだファイル、書いた差分は追えます。それだけ記録があっても、肝心の thinking は空でした。
当時の追加確認:設定を読み込んでいたのに、-pでは空だった
-p で実行した場合は、同じ設定でも空のままでした。$WORK は実験用のディレクトリ、$A・$B・$D は各実行のjsonlのパスです。設定は --settings で一時的に渡しました。
$ claude --version
2.1.233 (Claude Code)
$ cd $WORK/exp-A && claude -p "1から20までの素数の和を求めて。方針を2つ比べてから決めて。" --model sonnet
$ jq -r 'select(.message.content? | type=="array") | .message.content[] | select(.type=="thinking") | (.thinking|length)' "$A" | sort -n | uniq -c
1 0
$ cat $WORK/thinking-on.json
{ "showThinkingSummaries": true }
$ cd $WORK/exp-B && claude -p "1から20までの素数の和を求めて。方針を2つ比べてから決めて。" --model sonnet \
--settings $WORK/thinking-on.json
$ jq -r 'select(.message.content? | type=="array") | .message.content[] | select(.type=="thinking") | (.thinking|length)' "$B" | sort -n | uniq -c
1 0
設定を付けても空なら、そもそも --settings を読んでいないのでは。別の設定を渡して確かめました。
# 陰性対照:壊れた設定なら起動が落ちるか → 落ちる(ログも作られない)
$ claude -p "hi" --model sonnet --settings '{not valid json'
Error: Settings file not found: {not valid json
# 陽性対照:別の設定なら効くか → 効く(--model を付けずにモデルが変わる)
$ claude -p "say ok" --settings $WORK/model-haiku.json # {"model":"haiku"}
ok
$ jq -r 'select(.type=="assistant") | .message.model' "$D" | sort | uniq -c
2 claude-haiku-4-5-20251001
存在しない設定ファイルの指定では起動に失敗し、モデルの指定は反映されました。--settings は読み込まれていますが、思考の要約は出ていません。以前の記事でも試したように、変化がないときは、確認の手順が動いているかを見る必要がありました。
--output-format stream-json --verbose --forward-subagent-text、thinkingDisplay の直接指定、MAX_THINKING_TOKENS=8000 の併用も試しましたが、いずれも空でした。ログへ保存する前の標準出力も同じです。
# ログではなく、stream-json の標準出力そのものを保存して長さを取る
$ jq -c 'select(.message.content? | type=="array") | .message.content[] | select(.type=="thinking") | {thinking_len:(.thinking|length), sig_len:(.signature|length)}' exp-E.out
{"thinking_len":0,"sig_len":1112}
インストール済みのバンドルを見ると、出力形式を決める箇所がありました。
$ /usr/bin/grep -a -o 'function [A-Za-z0-9_$]\{2,6\}({explicitDisplay:.\{0,200\}' \
~/.local/share/claude/versions/2.1.233 | head -1
function oNs({explicitDisplay:e,isNonInteractive:t,outputFormat:r,verbose:n}){if(e)return e;
if(!t)return nNs()?"summarized":void 0;if(r==="text"||r==="json"&&!n)return"omitted";return}
t は isNonInteractive、nNs() は showThinkingSummaries??!1 を返す関数です。設定を読む nNs() は、対話モードを表す if(!t) の内側にあります。-p ではこの分岐を通らず、通常のテキスト出力では "omitted" に進んでいました。
このコードは 2.1.233 のものです。識別子は圧縮されているため、バージョンによって変わります。表示用に折り返していますが、もとのコードは 1 行です。
空だった6,319件には、読み返せる要約がありませんでした。次に気になる変更を見つけたときは、まず記録があるかを確かめる。あれば何を読んで何を変えたかを追い、なければ仕様と差分から調べる必要があります。謝られても、その疑問は残るので。