Agent SDK File Checkpointingでファイル変更を巻き戻す
Claude Agent SDKのenableFileCheckpointingでファイル変更を追跡し、rewindFiles()で任意の時点へ戻す実装手順と5つの制限をまとめます。
Agent SDKのFile Checkpointingとは
File Checkpointingは、Agent SDKのセッション中にWrite・Edit・NotebookEditツールで加えたファイル変更を追跡し、任意の時点へ巻き戻す機能です。enableFileCheckpointing(Python: enable_file_checkpointing)をtrueにすると、この3つのツールがファイルを書き換える直前にSDKがバックアップを作成します。応答ストリームに流れるユーザーメッセージにはチェックポイントのUUIDが付き、そのUUIDをrewindFiles()(Python: rewind_files())に渡すとファイルを巻き戻せます。
会話履歴とファイルは別々に管理されている点が要です。rewindFiles()が戻すのはディスク上のファイルだけで、会話の文脈やコンテキストはそのまま残ります。「実装を1つ試してダメだったので元のファイルに戻すが、会話はそのまま続けたい」という場面にちょうど合う設計です。
Claude Codeを対話CLIとして使っている場合は、/rewindコマンドとsettings.jsonのfileCheckpointingEnabledが同じ仕組みの入り口になります。CLIでの操作や制限はClaude Code checkpointの制限とfileCheckpointingEnabledの無効化にまとめてあります。本記事は、自分のアプリケーションからAgent SDKのAPIとしてチェックポイントを操作する実装に絞ります。
巻き戻すと具体的に何が起きるか
rewindFiles()を呼ぶと、Claude Codeはそのチェックポイント以降に作成したファイルを削除し、変更したファイルをチェックポイント時点の内容に復元します。
巻き戻しで動くものと動かないもの
戻る
Write・Edit・NotebookEditで作成したファイルは削除され、変更したファイルはチェックポイント時点の内容に戻ります。
戻らない
会話履歴とコンテキストは、巻き戻しの前後で変わりません。直後にセッションを続けると、Claudeは巻き戻し前のやりとりを覚えたままです。
たとえばA案を1手番進めたあとでB案を試したいとき、ファイルだけをA案着手前に戻し、「A案は不採用」という文脈は会話に残したまま次の指示を出せます。
ここで注意したいのはforkとの違いです。セッションのforkは会話履歴だけを枝分かれさせる機能で、ファイルシステムは分岐しません。forkした側のエージェントが編集したファイルは、同じディレクトリで動く他のセッションからもそのまま見えます。セッション機能が保存するのは会話であってファイルシステムではありません。ファイルを戻す手段はチェックポイントのほうで、会話を分岐させる手段がforkと考えると取り違えません。
実装前に必要なもの
Agent SDKのインストールとセットアップが済んでいる前提で進めます。Python・TypeScriptどちらのSDKでも、流れは同じです。
実装の流れ
- 1
有効化
オプションに
enableFileCheckpointingとreplay-user-messages - 2
UUIDの保持
ユーザーメッセージのUUIDを復元点として保持
- 3
巻き戻し
空プロンプトで再開し、
rewindFiles()を呼ぶ
ステップ1: チェックポイントを有効にする
enableFileCheckpointingに加えて、extraArgs(Python: extra_args)にreplay-user-messagesを指定します。これを忘れると次のステップでUUIDが取得できません。
import { query } from "@anthropic-ai/claude-agent-sdk";
const opts = {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const,
extraArgs: { "replay-user-messages": null }
};
const response = query({
prompt: "認証モジュールをリファクタリングして",
options: opts
});from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None},
)
async with ClaudeSDKClient(options) as client:
await client.query("認証モジュールをリファクタリングして")permissionMode: "acceptEdits"はファイル編集を確認なしで通す設定です。チェックポイントを試す間はこれで自動承認しておくと、編集が実際にトリガーされて挙動を確認しやすくなります。
ステップ2: チェックポイントUUIDとセッションIDを取得する
replay-user-messagesを設定すると、応答ストリームの各ユーザーメッセージにuuidが付きます。多くの用途では、最初のユーザーメッセージのUUIDだけを覚えておけば、変更前の状態に戻す復元点として使えます。複数の復元点を使い分けたい場合は後述の「複数の復元点を保持する」を参照してください。
let checkpointId: string | undefined;
let sessionId: string | undefined;
for await (const message of response) {
if (message.type === "user" && message.uuid && !checkpointId) {
checkpointId = message.uuid;
}
if ("session_id" in message && !sessionId) {
sessionId = message.session_id;
}
}セッションIDはストリーム完了後に巻き戻したいときだけ必要です。ストリームを処理している最中にrewindFiles()を呼ぶ「リスクの高い操作の前にチェックポイントを打つ」パターン(後述)では、セッションIDの取得自体を省略できます。
ステップ3: セッションを再開してファイルを巻き戻す
ストリームが完了したあとに巻き戻すには、空プロンプトでセッションを再開し、開いた接続の中でrewindFiles()を呼びます。
const rewindQuery = query({
prompt: "",
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(checkpointId);
break;
}async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("")
async for message in client.receive_response():
await client.rewind_files(checkpoint_id)
breakストリームを処理し終えた後にrewindFiles()を呼ぶと、CLIプロセスへの接続がすでに閉じているため"ProcessTransport is not ready for writing"というエラーになります。巻き戻しは必ず、セッションを再開して開いた新しい接続の中で行います。
セッションIDとチェックポイントIDさえ分かれば、CLIから直接巻き戻すことも可能です。この場合はPATH上のclaude実行ファイルが必要で、Claude Codeのインストールで入ります。SDKが内部で使う同梱バイナリとは別に用意する前提です。あわせて、SDKが内部で立てる環境変数CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTINGを自分で指定します。
CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true \
claude -p --resume <session-id> --rewind-files <checkpoint-uuid>--rewind-filesはclaude --helpの出力には表示されませんが、CLIは受け付けます。巻き戻しが成功するとFiles rewound to state at message <checkpoint-uuid>と表示され、プロンプトを送らずに終了します。
CLIの--helpに載っているもの
claude v2.1.289で--helpの出力を確かめると、--rewind-filesは一覧に出ません。一方、extraArgsのキーと同名の--replay-user-messagesは載っています。
claude --version
# 2.1.289 (Claude Code)
claude --help | grep -c rewind-files
# 0
claude --help | grep -A4 -e "--replay-user-messages"
# --replay-user-messages Re-emit user messages from stdin back on
# stdout for acknowledgment (only works
# with --input-format=stream-json and
# --output-format=stream-json)ヘルプの説明は、stdinで受けたユーザーメッセージをstdoutへ返すというもので、stream-jsonの入出力と組み合わせる前提です。SDKではこのフラグをextraArgsのキーとして渡します。--rewind-filesが一覧に出ないので、検索しても見つからないのは正常です。
巻き戻す前に差分を見る
TypeScriptのrewindFiles()はRewindFilesResultを返します。dryRun: trueを渡すと、巻き戻しを実行せずに結果のプレビューを得られます。
const preview = await rewindQuery.rewindFiles(checkpointId, { dryRun: true });
if (preview.canRewind) {
console.log(preview.filesChanged, preview.insertions, preview.deletions);
}RewindFilesResultのフィールドはcanRewind、error、filesChanged、insertions、deletions、skippedLinksです。ドライランではskippedLinksは設定されません。Pythonのrewind_files()は戻り値がNoneなので、同じ確認をしたいときはgit diffなどで差分を見る方法があります。
よく使う2つのパターン
リスクの高い操作の前にチェックポイントを打つ
直近のチェックポイントIDだけを保持し続け、エージェントの手番が進むたびに上書きします。何か問題が起きたら、直前の安全な状態へすぐ戻してループを抜けられます。
let safeCheckpoint: string | undefined;
for await (const message of response) {
if (message.type === "user" && message.uuid) {
safeCheckpoint = message.uuid;
}
if (yourRevertCondition && safeCheckpoint) {
await response.rewindFiles(safeCheckpoint);
break;
}
}yourRevertConditionは、エラー検知やバリデーション失敗など自分のアプリケーション側のロジックに置き換えます。
複数の復元点を保持する
エージェントが複数の手番にまたがって変更を加える場合、最初まで一気に戻すのではなく、特定の手番まで戻したいことがあります。全チェックポイントを配列にメタデータ付きで貯めておけば、セッション完了後にどの時点へでも戻せます。
interface Checkpoint {
id: string;
description: string;
}
const checkpoints: Checkpoint[] = [];
for await (const message of response) {
if (message.type === "user" && message.uuid) {
checkpoints.push({ id: message.uuid, description: `手番${checkpoints.length + 1}後` });
}
}たとえば1手番目でリファクタリング、2手番目でテスト追加が行われた場合、リファクタリングは残してテストだけ取り消す、といった使い分けができます。
5つの制限事項
| 制限 | 内容 |
|---|---|
| Write / Edit / NotebookEditのみ | 内容Bashコマンド経由の変更(sed -iやecho >等)は追跡されない |
| サブエージェントの編集 | 内容サブエージェントが加えた変更は追跡・復元されない。例外はフォアグラウンドで動くcontext: forkのskill。追跡外の変更はgitで戻す |
| 同一セッション限定 | 内容チェックポイントは作成元のセッションに紐づく |
| ファイル内容のみ | 内容ディレクトリの作成・移動・削除は巻き戻しの対象外 |
| ローカルファイルのみ | 内容リモートやネットワーク越しのファイルは追跡されない |
巻き戻し時、Claude Codeはシンボリックリンク・ハードリンク・その他の非通常ファイルになっているパスをスキップします。バックアップ時点からディレクトリ構造が変わっていて解決できないパス、バックアップを安全に読み取れないパスも同様です。スキップされたパスの総数は、TypeScriptのRewindFilesResultのskippedLinksフィールドに入ります。このスキップ処理はv2.1.216以降の挙動で、それより前のバージョンではリンク先まで書いて消していました。
Git・CLIのcheckpointとの使い分け
自分のアプリケーションにファイルの巻き戻しを組み込むとき、SDKのFile Checkpointingとgitのどちらを使うべきか迷う場面があります。
| 用途 | 向いている手段 | 理由 |
|---|---|---|
| エージェントの1手番前まで、会話は続けたまま戻す | 向いている手段Agent SDKのrewindFiles() | 理由会話コンテキストを保ったままファイルだけ戻せる |
| 恒久的な変更履歴として残す・チームで共有する | 向いている手段git | 理由チェックポイントはセッションに紐づき、セッション外では参照できない |
| Bashコマンドで加えた変更を取り消す | 向いている手段git | 理由File Checkpointingの追跡対象外(前節の制限表を参照) |
対話CLIの利用者自身が/rewindで操作する | 向いている手段Claude Codeの/rewindコマンド | 理由Agent SDKのAPI呼び出しが不要で、CLI単体で完結する |
よくあるつまずき
- UUIDが
undefinedのまま:extraArgs(またはextra_args)に{"replay-user-messages": null}を指定し忘れているケースがほとんどです。この設定がないとユーザーメッセージにUUIDが付きません "No file checkpoint found for this message"エラー: 元のセッションでenableFileCheckpointingをtrueにしないまま実行していたか、セッションが完了する前に再開・巻き戻しを試みた場合に起きます。最初のユーザーメッセージUUIDを取得し、セッションを完了させてから、空プロンプトで再開して1回だけrewindFiles()を呼ぶ手順を守ります"File rewinding is not enabled"エラー: 非対話の巻き戻しでチェックポイントが有効になっていない場合に出ます。素のclaude -pに--rewind-filesを付けただけ、あるいは巻き戻しを実行するセッション(再開したセッションを含む)のオプションでチェックポイントを有効にしていない場合です。SDKはenableFileCheckpointingが有効なセッションでのみ内部的に環境変数をセットするため、CLI単体では自分で環境変数を渡す必要があります- 古いSDKバージョン:
enableFileCheckpointingやrewindFiles()が見当たらない場合はSDKが古い可能性があります。Pythonはpip install --upgrade claude-agent-sdk、TypeScriptはnpm install @anthropic-ai/claude-agent-sdk@latestで更新します
まとめ
追跡対象はWrite・Edit・NotebookEditに限られるため、エージェントにBashで大量のファイルを触らせるアプリでは、gitの履歴と併用する構成が前提になります。ファイルだけ戻して会話を続けたい場面なら、rewindFiles()が最短の手段です。