Claude Code の仕組み — ハーネスの動作と Claude API
本記事は Claude Code が動作する仕組みについてまとめたものです。
過去に調査を断片的に行い、記事にしてきました。
- Claude Code のドキュメントを読んで気になったことを検証してみた
- Claude Code が LLM に渡すコンテキストの中身を調査する
- Claude Code のセッション、コンテキストサイズとトークン消費量の関係性
今回はそれらを一つにまとめて、なるべく体系的に書いています。長い文章になりますが、よければお付き合いください。
検証環境: macOS (Darwin 24.3.0) / Claude Code v2.1.220 / 2026年7月28日時点。セッションログのフォーマットは公式に「内部仕様でありバージョン間で変わり得る」と明言されている 1 ため、本記事の実測値・構造はバージョン依存であることにご注意ください。
1. 全体アーキテクチャ — 3層構造
最初に、本記事全体を貫くメンタルモデルを提示します。Claude Code は次の3層構造で動いています。
各層の役割は明確に分かれています。
| 層 | 実体 | 役割 |
|---|---|---|
| Claude API | Anthropic のサーバー | モデルによる推論(思考・応答生成・ツール使用の判断)だけを行う。状態は一切持たない |
| ハーネス | ローカルで動く Claude Code 本体 | ツールの実行、コンテキストの組み立て、権限管理、セッションの永続化。すべての状態管理を担う |
| セッションログ | JSONL ファイル | 会話の永続化層。ハーネスが API リクエストを組み立てるとき、会話履歴をここから毎回読み出す |
「ハーネス」は公式の呼び名です。ドキュメントは Claude Code を "agentic harness"(エージェント用の馬具・骨組み)と呼んでいます 2。
Claude Code serves as the agentic harness around Claude: it provides the tools, context management, and execution environment that turn a language model into a capable coding agent.
大原則: Messages API はステートレス
この3層構造を理解する上で最も重要な原則が、Claude API(Messages API)はステートレスであるという事実です 3。
- API サーバーは会話履歴を一切保持しません
- ハーネスは API を呼ぶたびに、システムプロンプト・過去の全メッセージ・今回の入力をすべて含めて送信します
- 「コンテキスト」とは、この毎回送信される payload 全体のことです
リクエスト payload は次の形式です 3。ポイントは messages 配列で、2ターン目のリクエストには1ターン目の user / assistant メッセージがまるごと含まれます。
POST /v1/messages
{
"model": "claude-...",
"system": [ /* システムプロンプト */ ],
"tools": [ /* ツール定義 */ ],
"messages": [
{"role": "user", "content": "..."}, // 1ターン目の発言
{"role": "assistant", "content": [ ... ]}, // 1ターン目の応答
{"role": "user", "content": "..."} // 今回 (2ターン目) の発言
]
}
つまり「会話が続いている」ように見えるのは、ハーネスが毎回過去ログを再構築して送り直しているからです。この「再構築の元ネタ」がセッションログ(JSONL)であり、「再構築時に何が足されるか」が第4章のコンテキスト注入の話になります。
ステートレスの代償を消す仕組み: プロンプトキャッシュ
「毎回全量送信」を計算コストの面で成立させているのが、サーバーサイドのプロンプトキャッシュです 4。
モデルは応答を書き始める前に、payload 全体を先頭から読みます。サーバーはこの「読んだ結果」を一時的に保存しておき、次のリクエストの先頭が前回と同じであれば、その部分は読み直さずに保存済みの結果を使います。Claude Code のエージェントループ(2章で詳述)のリクエストは「前回の payload の末尾に数ブロック足しただけ」の形なので、ほぼ全体が読み直し不要になります。
注意点は2つあります。
- 会話を覚えているわけではありません。サーバーが保存するのは「読んだ結果」だけなので、ハーネスは毎回 payload 全体を送る必要があります。ステートレスの原則は崩れていません
- 一致判定は先頭からの完全一致です。payload の途中が1バイトでも前回と変わると、そこから後ろのキャッシュはすべて使えなくなります
キャッシュを使うかどうかはサーバーが勝手に決めるのではなく、ハーネスが payload 内で指示します。キャッシュしたい範囲の末尾のブロックに、cache_control という目印を付けます。
{
"tools": [ /* ツール定義 */ ],
"system": [
{"type": "text", "text": "(システムプロンプト)",
"cache_control": {"type": "ephemeral", "ttl": "1h"}}
],
"messages": [
{"role": "user", "content": "..."},
{"role": "assistant", "content": [ ... ]},
{"role": "user", "content": [
{"type": "text", "text": "(今回の発言)",
"cache_control": {"type": "ephemeral", "ttl": "1h"}}
]}
]
}
- payload は
tools→system→messagesの順に連結され、目印を付けたブロックまでの範囲がキャッシュされます。この例の目印は2個で、「tools + system までの安定部分」と「会話全体」がそれぞれキャッシュ対象です - 目印は1リクエストに最大4個まで置けます。次のリクエストでは、伸びた会話の新しい末尾に目印を付け直します。過去の位置のキャッシュも有効に残るため、会話が伸びるほどヒット範囲が積み上がります
- キャッシュの保持期間(TTL)は5分(デフォルト)または1時間から選びます。キャッシュから読んだ分は通常入力の約0.1倍のコスト、新規に書き込む分は約1.25倍(5分)〜2倍(1時間)です。Claude Code は1時間を指定しています(実測)
会話が進むたびに payload の末尾へメッセージが積み上がり、「キャッシュヒット / 新規」の境界と目印の位置が下へ移動していきます。
2. Agentic Loop と thinking の二重構造
Claude Code は自律的にツールを使いながらタスクを進めます。この自律性は、性質の異なる2つの階層でできています。行動を繰り返すループ(ハーネス側)と、1回の応答の内側での推論(サーバー側)です。ループは外側の1つしかありません。
2.1 ハーネス側のループ(外側のループ)
ユーザーが1回発言(= 1ターン)すると、裏側では複数回の API 呼び出しが走ります。
ポイントは4つです。
-
ループを回す主体はハーネスです。モデルは「次に何をすべきか」を判断して
tool_useブロックを返すだけで、実際にファイルを読んだりコマンドを実行したりするのはローカルのハーネスです -
ツールの実行結果は、
tool_resultブロックを含むuserロールのメッセージとして API に返送されます -
ループの継続条件は、モデルが
tool_useを返すことだけです。API 応答には生成を止めた理由(stop_reason)が付いていて、それがtool_useの間はハーネスがツール実行と再呼び出しを繰り返し、tool_useを含まない応答(end_turn)が返るとターンが終わります - したがって 1ターン ≠ 1 API 呼び出しです。複雑なタスクでは1ターンの裏で数十回の API 呼び出しが走ることもあります
図の①〜③に対応する実物を、本記事の執筆セッションのログから抜粋します(内容は一部省略)。tool_use と tool_result は id で対応づけられています。
① モデル → ハーネス: API 応答。Bash ツールの実行を要求し、stop_reason: "tool_use" で生成を止める
{
"role": "assistant",
"content": [{
"type": "tool_use",
"id": "toolu_01Rfm7vm2AAe8YpTdN3t1cvU",
"name": "Bash",
"input": {"command": "ls -la /Users/yuusuke-kawatsu/Desktop/tmp/qiita_0728/", ...}
}],
"stop_reason": "tool_use"
}
② ハーネス → モデル: ハーネスがローカルで ls を実行し、その結果を user メッセージとして messages 末尾に追加して再呼び出し
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": "toolu_01Rfm7vm2AAe8YpTdN3t1cvU",
"content": "total 0\ndrwxr-xr-x@ 2 yuusuke-kawatsu ..."
}]
}
③ モデル → ハーネス: ツール結果を踏まえた応答。text で完結し、stop_reason: "end_turn" でターンが終わる
{
"role": "assistant",
"content": [{
"type": "text",
"text": "(ls の結果を踏まえた応答テキスト)"
}],
"stop_reason": "end_turn"
}
公式はこのループを「gather context(情報収集)→ take action(行動)→ verify results(検証)の3フェーズの繰り返し」と説明しています 2。
2.2 サーバーサイドの thinking(内側の推論)
一方、1回の API 呼び出しの内側でも、モデルは推論を行っています。これが thinking です。Claude Code のターミナルに灰色で流れる思考テキストの実体は、API 応答に含まれる thinking ブロックです。
図のとおり、thinking は1回の連続したテキスト生成の前半部分です。モデルは thinking トークンを書き終えると、そのまま続けて出力トークンを生成します。サーバー内に 2.1 のようなループはありません。thinking で応答が遅くなるのは、回答の前に数百〜数万トークンの推論テキストを生成するためで、この分も output トークンとして課金されます。
有効化 — thinking を有効にするのはハーネスで、リクエスト payload の thinking パラメータで指定します 5。
extended thinking(Claude 4.5 世代以前)— 思考トークンの予算をハーネスが明示する
{
"model": "claude-sonnet-4-5",
"thinking": {"type": "enabled", "budget_tokens": 10000},
"messages": [ ... ]
}
adaptive thinking(Claude 4.6 世代以降)— 考えるかどうかはモデルが判断し、深さは effort で制御する
{
"model": "claude-opus-4-6",
"thinking": {"type": "adaptive"},
"output_config": {"effort": "high"},
"messages": [ ... ]
}
応答での現れ方 — content の先頭に thinking ブロックが挿入されます。thinking 無効時との違いは、このブロックの有無だけです(本文はイメージ例)。
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "ユーザーはビルドエラーの原因を尋ねている。エラーメッセージは型の不一致を示していて、直前の diff を見ると引数の順序が入れ替わっている。これが原因だ。結論から伝えよう。",
"signature": "CAIS..."
},
{
"type": "text",
"text": "エラーの原因は、`login()` に渡す引数の順序です。..."
}
]
}
interleaved thinking(交互思考)では、1回の assistant ターンの中でツール呼び出しの合間にも thinking ブロックが現れます。ツール結果を見て「次はどうするか」を考えてから次の tool_use を出す動きで、adaptive thinking では自動で行われます。
ハーネスの責務 — 有効化のほかにハーネスが担うのは、届いたブロックの描画とセッションログへの記録(3.1 節)、そしてツール使用ループ中の返送です。ツール結果を返すときは、直前の応答に含まれていた thinking ブロックを無改変のまま含めることが API 仕様上の義務で、欠落や改変は 400 エラーで拒否されます 5。
まとめると、thinking はループではなく、1回の API 応答内で完結する推論です。ツールを実行して結果を得て次を決めるループを回すのは、常にハーネス側です。
2.3 二重構造のまとめ
| 外側のループ | 内側の推論 | |
|---|---|---|
| 主体 | ハーネス(ローカル) | モデル(サーバー) |
| 単位 | 1ターン = N回の API 呼び出し | 1回の API 応答内 |
| やること | ツール実行、結果の返送、履歴管理 | 思考(thinking)、出力生成 |
| 状態 | 持つ(会話履歴、セッション) | 持たない(ステートレス) |
3. セッションの仕組み(JSONL)
3.1 2章の「1ターン」は、ログにどう積み上がるのか
第2章のループは、ローカルのセッションログにそのまま痕跡として残ります。この記事の執筆に使っているセッション自身の JSONL を素材に確認します。
セッションログは 1行 = 1エントリの JSONL ファイルとして、次の場所に保存されています 1(デフォルト30日で自動削除)。
~/.claude/projects/<プロジェクトパスのスラッグ>/<セッションID>.jsonl
<プロジェクトパスのスラッグ> は、作業ディレクトリのパスの非英数字を - に置換したものです(例: /Users/foo/myapp → -Users-foo-myapp)。
ファイルの中身は、1行1オブジェクトの並びで、末尾に行を追記して伸びていきます。各行(= エントリ)は parentUuid フィールドで1つ前のエントリを指しており、セッションログの実体は、追記で伸びていく連結リスト(linked list)です。この1行が、後述の表の1行に対応します。
{"type": "user", "uuid": "e1", "parentUuid": "...", "message": { ... }}
{"type": "attachment", "uuid": "e2", "parentUuid": "e1", ... }
{"type": "assistant", "uuid": "e3", "parentUuid": "e2", "message": { ... }}
1エントリを展開した構造です(実物から抜粋、一部省略)。
{
"type": "assistant", // エントリの種類(user / assistant / system ほか)
"uuid": "0794013f-...", // このエントリの ID
"parentUuid": "05c0ddff-...", // 直前のエントリの ID(→ 3.3 節)
"sessionId": "a726f404-...",
"timestamp": "2026-07-28T06:41:07.481Z",
"message": { // API とやりとりしたメッセージ本体
"id": "msg_011CdTzL...", // API 応答の ID
"role": "assistant",
"content": [ // content は「配列」
{ // 要素は「ブロック」オブジェクト
"type": "text", // ブロックの種類(thinking / text / tool_use ...)
"text": "..."
}
],
"usage": { ... }
}
}
会話系のエントリは、API とやりとりしたメッセージを message フィールドにほぼそのまま保持します。content は配列で、その要素が content ブロック(オブジェクト)です。2章に登場した tool_use や thinking、通常の応答テキストの text は、すべてこのブロックの type の種類です。この後の表の「ブロック」列は、この type を指しています。
この前提で、あるターン(ユーザー発言 → ツール実行1回 → 応答)がログにどう積み上がるかを示します。エントリの構造・並び・チェーンは実測どおりで、中身と uuid は説明用の例です。
| # | type | ブロック | 中身(例) | uuid | parentUuid |
|---|---|---|---|---|---|
| 1 | user | — | 「ビルドが失敗する原因を調べて」 | e1 |
-- |
| 2 | attachment | — |
task_reminder(メタデータ系) |
e2 |
e1 |
| 3 | assistant | thinking |
「まずビルドを実行してエラーを確認しよう。」 | e3 |
e2 |
| 4 | assistant | text |
「ビルドを実行して原因を確認します。」 | e4 |
e3 |
| 5 | assistant | tool_use |
Bash {"command": "npm run build"} |
e5 |
e4 |
| 6 | user | tool_result |
Error TS2345: Argument of type 'string' ... |
e6 |
e5 |
| 7 | attachment | — |
hook_success(メタデータ系) |
e7 |
e6 |
| 8 | assistant | text |
「原因は login() に渡す引数の型不一致です。…」 |
e8 |
e7 |
attachment は、ハーネスが管理情報を記録するメタデータ系エントリです(種類は 3.2 節)。parentUuid のチェーンは、会話系エントリだけでなくメタデータ系エントリも経由して1本につながっています。
このログには、2章のループの仕組みがそのまま現れています。読み取るべき仕様は3つです。
① ツール実行結果は user エントリとして記録される
エントリ6は user タイプですが、ユーザーの発言ではなく、ハーネスがツール実行結果(tool_result)を user ロールとして API に返送した記録です。2.1 のポイント2がそのままログに現れています。
② 1回の API 応答は content ブロックごとに複数エントリへ分割される
エントリ3〜5は3行に分かれていますが、実は同一の API 応答です。JSONL には API 呼び出しを表す階層構造はなく、すべてのエントリが同じ階層に並びます。どのエントリが同じ応答由来かは、message.id の一致で表現されています。
| エントリ | ブロック | message.id |
|---|---|---|
| 3 | thinking |
msg_abc123 |
| 4 | text |
msg_abc123(= 同じ API 応答) |
| 5 | tool_use |
msg_abc123(= 同じ API 応答) |
Claude Code は API 応答の content ブロック1個につき JSONL 1エントリとして記録します。つまり1回の API 応答は、thinking + text だけでも2エントリ、ツールを使えばさらに多くのエントリに分かれて記録されます。
③ usage はエントリ単位ではなく API 呼び出し単位の値
assistant エントリには usage フィールドが付きますが、この値は「そのエントリ単体」ではなく「その API 呼び出し全体」のトークン数です。このセッションの実測値です。
{
"input_tokens": 2,
"cache_creation_input_tokens": 379,
"cache_read_input_tokens": 50803,
"output_tokens": 372,
"cache_creation": {
"ephemeral_1h_input_tokens": 379,
"ephemeral_5m_input_tokens": 0
}
}
入力側3項目の合計(input_tokens + cache_creation + cache_read)が、この1回の API 呼び出しで送信されたコンテキストの全量で、約5.1万トークンです。うち5万トークン超はキャッシュから読まれ、新規に処理されたのは 381 トークンだけです。1章で述べた「毎回全量を送信し、大部分はキャッシュで処理を省く」が、この数字にそのまま表れています。
3.2 エントリの種類 — 会話系とメタデータ系
エントリの type は会話系だけではありません。v2.1.220 の実セッションで観測されたタイプを分類すると:
| 分類 | type | 内容 |
|---|---|---|
| 会話系(API payload の再構築に使われる) | user |
ユーザー発言、または tool_result |
assistant |
thinking / text / tool_use(1ブロック1エントリ) | |
| メタデータ系(ローカル管理用。API には送られない) | system |
compact 境界、ターン所要時間などのイベント記録 |
attachment |
hook の実行結果、スキル一覧の差分など | |
file-history-snapshot |
チェックポイント(→ 5.3 /rewind) |
|
ai-title, last-prompt, mode, permission-mode ほか |
セッション名、入力履歴、モード状態など |
このように、JSONL の全エントリが API に送られるわけではありません(対応関係は 3.4 で図解します)。
3.3 parentUuid によるツリー構造
3.1 で見た連結リストは、正確には分岐可能なツリーです。単純なリストではなくツリーになっている理由は、会話の巻き戻しと分岐をサポートするためです(具体的な枝分かれは 5.3 の /rewind で見ます)。「現在の会話」として payload に再構築されるのは、最新エントリから parentUuid を遡って辿れる1本のパスだけです。
セッションログは基本的に追記専用(append-only)で、過去の行を書き換えません。後述する /compact ですら、履歴の削除ではなく「境界エントリと要約の追記」で実現されています(5.2 節)。この追記専用設計とツリー構造の組み合わせが、巻き戻し・分岐・復元のすべてを支えています。
3.4 セッションログ → API payload の再構築
ここまでを踏まえて、3層構造の「対応関係」を図にします。ハーネスは API を呼ぶたびに、JSONL から会話系エントリを読み出し、その他の要素を組み合わせて payload を組み立てます。
では、組み立てられた payload の中には何がどう入っているのか。1章で見た system / tools / messages の3区画に、それぞれ次の要素が配置されます 6。
このうち会話が進むにつれて単調増加するのは messages 配列(会話本体)だけで、これがコンテキスト膨張の主原因です。自分のセッションの内訳は /context コマンドでいつでも確認でき、カテゴリ別のトークン消費が表示されます。
システムプロンプトや CLAUDE.md は、セッションログには保存されません。これらは API を呼ぶたびにハーネスが disk から読み直して生成します。だからこそ、セッションを翌日再開しても最新の CLAUDE.md が反映されるわけです。
4. ハーネスによる思考制御(コンテキスト注入)
3.4 の図で「ハーネスが毎回生成・注入」と書いた部分を掘り下げます。ハーネスは JSONL の会話を素朴に messages 配列へ変換するだけではなく、モデルの思考を制御するための情報をあちこちに注入しています。
個々の注入の実測は過去記事②に譲り、ここでは体系だけを整理します。
4.1 注入の主なチャネル
| チャネル | 実体 |
|---|---|
| system パラメータ | payload トップレベルのシステムプロンプト。ペルソナ・ツール使用の原則・応答形式の中核指示(1章の payload 例を参照) |
<system-reminder> タグ |
user メッセージ内に差し込まれる万能注入タグ。モード状態の通知、スキル一覧、hook からの追加コンテキストなど |
| 合成履歴 | 「モデルがツールを呼んだことにした」偽の履歴 |
| ツール結果のラップ | tool_result の整形・退避 |
<system-reminder> の実例(実測)— 空の CLAUDE.md を Read したときのツール結果です。ファイル内容の代わりに、ハーネスがこの注釈を注入していました。
{
"type": "tool_result",
"tool_use_id": "toolu_01...",
"content": "<system-reminder>Warning: the file exists but the contents are empty.</system-reminder>"
}
合成履歴の実例(構造は実測どおり、内容は例)— ユーザーが @README.md とメンションすると、モデルが Read ツールを呼んだかのような tool_use / tool_result のペアが payload に合成されます。
{"role": "assistant", "content": [
{"type": "tool_use", "id": "toolu_x1", "name": "Read", "input": {"file_path": "README.md"}}
]},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_x1", "content": "(README.md の内容)"}
]}
ツール結果のラップの実例(実測)— WebFetch の出力が大きすぎたとき、tool_result の中身が退避ファイルへの参照と冒頭プレビューに置き換えられていました。
<persisted-output>
Output too large (71.1KB). Full output saved to:
~/.claude/projects/<プロジェクト>/<セッションID>/tool-results/toolu_01NC....txt
Preview (first 2KB): ...
</persisted-output>
4.2 pull 型の設計
ハーネスは「モデルが必要としそうな情報」をすべて注入(push)するわけではありません。TodoList の状態や Plan ファイルの本体は messages に丸載せせず、モデルが必要になったときにツール呼び出しで取得させる(pull)設計です。
TodoList を例にすると(内容は例):
push 型なら — 毎リクエストの payload にリスト全文が入り続ける
<system-reminder>現在のタスク: 1. ビルド修正(進行中) 2. テスト追加 3. ...(全文)</system-reminder>
pull 型(実際の設計) — 小さな通知だけを注入し、中身はモデルが取りに行く
<system-reminder>タスクリストが更新されています。TaskList ツールで確認できます。</system-reminder>
モデルが必要と判断したときだけ、TaskList の tool_use で全文を取得します。
コンテキスト消費を抑えるための設計判断と考えられます。
4.3 揮発性 vs 永続性 — JSONL に残らない注入がある
3章で見た「JSONL ⇄ payload」の対応関係を踏まえると、注入には重要な非対称性があります。注入されるコンテキストには、JSONL に永続化されるものと、送信時にだけ生成されて記録に残らないものがあるのです。
| 分類 | 例 | 挙動 |
|---|---|---|
| 永続的な注入 |
@メンション の合成履歴、hook の追加コンテキスト |
JSONL に記録され、以後のすべての API 呼び出しで再送される |
| 揮発性の注入 | モード状態の通知(Auto mode 等)、一部のリマインダー | ターン送信時にその場で生成。JSONL には残らず、次のターンでは最新の状態が改めて生成される |
揮発性の注入は「常に最新の状態だけをモデルに見せたい情報」(現在のモード、現在時刻など)に使われています。つまりセッションログは「会話の記録」であって、「payload の完全な記録」ではありません。
5. セッション操作コマンドの内部動作
3章(JSONL)と4章(payload 組み立て)の知識が揃うと、セッションを操作するコマンド群が「JSONL に何をするか」×「次の API payload がどうなるか」の2軸で統一的に説明できます。
まず全体を一覧します。
| コマンド | JSONL(ローカルの記録) | 次の API payload(コンテキスト) | セッション ID |
|---|---|---|---|
/clear |
新しいファイルを作成。旧ファイルはそのまま残る | 空の会話から再スタート | 新規発行 |
/compact |
同一ファイルに compact_boundary と要約エントリを追記
|
過去の messages が要約1個に置き換わる | 変わらない |
/rewind(会話の復元) |
過去のエントリを親とする新しい枝が生える | 選択時点までの履歴に巻き戻る | 変わらない |
/btw |
何も書かれない | 現在のコンテキスト + 質問(1回だけ・使い捨て) | 変わらない |
--continue / --resume
|
既存ファイルに追記を再開 | 保存済みの履歴から再構築 | 変わらない |
/branch / --fork-session
|
履歴を新ファイルにコピーして以後そちらに追記 | コピー時点までの履歴を引き継ぐ | 新規発行 |
以下、コマンドごとに内部動作を説明します。
5.1 /clear — 新しいセッションを切る
/clear は「今のセッションログを消す」コマンドではありません。実際には:
- 新しいセッション ID を発行し、新しい JSONL ファイルへの記録を開始します
- 旧セッションのファイルは無傷で残り、
/resumeでいつでも復帰できます 7 - payload 的には messages 配列が空になるので、コンテキストは初期状態(システムプロンプト + CLAUDE.md 等)に戻ります
5.2 /compact — 履歴を要約に置き換える
/compact(およびコンテキスト逼迫時の自動 compaction)は /clear と対照的に、同じセッション・同じファイルの中で messages だけを圧縮します。実際の JSONL には次の2エントリが追記されます。
① compact_boundary(system エントリ) — 実物から抜粋:
{
"type": "system",
"subtype": "compact_boundary",
"parentUuid": null,
"logicalParentUuid": "e58c52b2-...",
"compactMetadata": {
"trigger": "auto",
"preTokens": 984519,
"postTokens": 6384
}
}
このエントリから読み取れる仕様は3つです。
-
parentUuid: null— ここで parentUuid のチェーンが物理的に切断されます。payload を再構築するとき、これより前のエントリは辿られません -
logicalParentUuid— ただし「論理的には旧チェーンの続き」という接続情報は別フィールドで保持されます(表示や/rewindのため) -
preTokens: 984519 → postTokens: 6384— この例では約98万トークン分の履歴が約6千トークンの要約に圧縮されたことが記録されています
② 要約本体(user エントリ、isCompactSummary: true):
{
"type": "user",
"isCompactSummary": true,
"message": {
"role": "user",
"content": "This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation. Summary: 1. Primary Request and Intent: ..."
}
}
チェーン上ではこうなります。
つまり compact 後の payload は「要約という名の user メッセージ1個 + それ以降の会話」として再構築されます。モデルから見ると「長い前置きを読まされてから会話を続ける」形です。
なお、compact で失われるのは messages 内の情報だけです。何が生き残るかは公式に整理されています 8。
| 要素 | compaction 後 |
|---|---|
| システムプロンプト | 影響なし(そもそも messages ではない) |
| プロジェクトルートの CLAUDE.md / Auto memory | disk から再注入される |
paths: 付きの rules / サブディレクトリの CLAUDE.md |
一旦失われる(該当ファイルを再度読むまで) |
| 発動済みスキルの本文 | 再注入される(1スキル5,000トークン・合計25,000トークンの上限付き) |
messages を要約でいくら潰しても payload の骨格が壊れないのは、システムプロンプトや CLAUDE.md が JSONL ではなく disk から毎回再生されるからです(3.4 節)。
5.3 /rewind — チェックポイントへの巻き戻し
/rewind(または空のプロンプトで Esc 2回)は、会話とコードを過去の時点に戻すコマンドです。これを支えるのがチェックポイント機構で、実は2つの独立した仕組みの組み合わせです 9。
① 会話の巻き戻し = parentUuid ツリーの枝分かれ
3.3 で「セッションログはツリー構造」と説明しました。会話を巻き戻して新しい指示を出すと、選択した時点のエントリを親とする新しい枝が生えます。
旧い枝(図の点線側)は削除されるのではなく、payload の再構築対象から外れるだけです 9。
② コードの巻き戻し = ファイルスナップショット
ユーザーがプロンプトを送るたびに、ハーネスは編集対象ファイルのスナップショットを取ります。JSONL には file-history-snapshot エントリが記録され(どの user メッセージ時点か messageId で紐づく)、ファイル実体は別ディレクトリに退避されます。
{
"type": "file-history-snapshot",
"messageId": "71cf4a77-...",
"snapshot": {
"trackedFileBackups": {
"storyline.md": { "backupFileName": "ecc7d8cb5d6d337a@v2", "version": 2 }
}
}
}
~/.claude/file-history/<セッションID>/
├── 6334ef34043b337c@v1 ← コンテンツハッシュ@バージョン
└── ecc7d8cb5d6d337a@v2
/rewind メニューで「Restore code and conversation」「Restore conversation」「Restore code」の3つが選べるのは、この2つの機構が独立しているからです。
制限も、この2機構の構造で決まっています 9。
- Bash コマンドによるファイル変更は巻き戻せない(スナップショットは編集ツール経由の変更だけを追跡するため)
- サブエージェントの編集は原則対象外(別コンテキストで動くため)
- チェックポイントは直近100個まで。git の代替ではなく「セッション内 undo」という位置づけ
また /rewind メニューには「Summarize from here / up to here」という部分 compact も統合されています。全履歴を要約する /compact に対し、指定時点の前後どちらかだけを要約に置き換える、と整理できます。
5.4 --continue / --resume — 保存済みログからの再開
セッションは常時 JSONL に保存されているので、「再開」とは既存ファイルを読み込んで payload を再構築し、同じファイルへの追記を再開することです 1。
-
claude --continue: カレントディレクトリの最新セッションを再開 -
claude --resume: セッションピッカーを開く(名前・ID 指定も可) - セッション ID は変わらず、同じファイルに追記が続きます
再開時に復元されるのは会話履歴だけではありません。使用モデル、エージェント設定、パーミッションモード(ただし plan と bypassPermissions は復元されない)、未完了のスケジュールタスクなども JSONL 由来で復元されます。一方、--mcp-config や --add-dir などの起動フラグは復元されないので再指定が必要です。
なお、同じセッションを2つのターミナルで同時に再開すると、両方のメッセージが1つの JSONL に混ざって記録されます(公式も明言 1)。チェーンが交錯して片方から見えなくなる危険があるため、並行作業なら次の fork を使うべきです。
5.5 /branch / --fork-session — 履歴のコピーで分岐
/branch(CLI では --continue --fork-session 等)は、会話履歴を新しいセッション ID のファイルにコピーし、以後はそちらに追記する操作です 1。
- 元のセッションは無傷で残る(
/resumeで戻れる) - payload 的にはコピー時点までの履歴をそのまま引き継ぐ
-
/rewindの会話復元が「同一ファイル内の枝分かれ」なのに対し、/branchは「ファイルごと分岐」
「別のアプローチを試したいが今の状態も残したい」とき、rewind ではなく branch を使う理由がこの構造の違いです。
5.6 /btw — どこにも残らないサイドクエスチョン
最後に、最も特殊な /btw です。これは「今の作業について、会話を汚さずにちょっと聞きたい」ためのコマンドで、公式ドキュメントの説明が仕組みをよく表しています 10。
Side questions have full visibility into the current conversation, so you can ask about code Claude has already read, decisions it made earlier, or anything else from the session. The question and answer are ephemeral: they appear in a dismissible overlay and never enter the conversation history.
本記事の枠組みで整理すると:
- payload: 現在のコンテキスト全体 + 質問、で API を1回だけ呼ぶ。親会話と先頭部分が一致するので、プロンプトキャッシュ(1章)がそのまま効き、追加コストは小さい
- JSONL: 質問も回答も一切記録されない。完全に揮発性
- ツール: 使えない(コンテキスト内の知識だけで回答する)
公式はこれを「サブエージェントの逆」と表現しています。
/btwis the inverse of a subagent: it sees your full conversation but has no tools, while a subagent has full tools but starts with an empty context.
/btw(サイドクエスチョン) |
サブエージェント | |
|---|---|---|
| コンテキスト | 親会話の全部が見える | 空から開始 |
| ツール | 使えない | フルに使える |
| 会話履歴への記録 | 残らない(揮発性) | 結果の要約だけが本会話に戻る |
| 用途 | 「今の会話について」聞く | 「新しく調べてきて」と頼む |
なお、/btw の回答から f キーで fork すると、その問答を本物の履歴として含む新セッションが作られます。揮発性の問答を後から永続化する唯一の経路です。
おわりに
この様な長文記事を最後まで読んでいただき、ありがとうございました。
-
Manage sessions — "The entry format is internal to Claude Code and changes between versions" ↩ ↩2 ↩3 ↩4 ↩5
-
Messages API — "The Messages API can be used for either single queries or stateless multi-turn conversations." ↩ ↩2
-
Thinking(総説)および Extended thinking(旧方式のリファレンス) ↩ ↩2
-
Explore the context window — 何がいつロードされるかを対話的に確認できる公式ページ ↩
-
Commands —
/clear: "Start a new conversation with empty context. ... Resume the previous conversation with/resume" ↩ -
Explore the context window — "What survives compaction" の節 ↩
-
Interactive mode — "Side questions with /btw" の節 ↩