Skip to content

File checkpointing

Record file edits an Agent SDK session makes and roll files back to any earlier user message with rewindFiles or rewind_files.

File checkpointing gives your agent an undo button for the files it touched. With it switched on, the SDK backs up each file before the Write, Edit or NotebookEdit tools change it, and every user message in the stream becomes a restore point. Later you can rewind the files on disk to how they were at any of those points.

I use this in tools where a user reviews an agent's changes before accepting them. "Reject" simply rewinds to the first checkpoint.

What is and is not tracked

TrackedNot tracked
Edits through Write, Edit and NotebookEditAnything done through Bash (sed -i, mv, echo > file)
Edits by a skill with context: fork running in the foregroundEdits made by subagents
Files created by those tools (rewind deletes them)Directory creation, moves and deletion
Local filesRemote or network files

Note: Rewinding restores files only. The conversation is untouched: Claude still remembers making the changes.

On rewind, Claude Code deletes files it created and restores modified files to their content at the checkpoint. From v2.1.216 it skips, rather than writing through:

  • tracked paths that are symlinks, hard links or other non-regular files;
  • files whose parent directory no longer resolves to where it was at checkpoint time;
  • files whose backup cannot be read safely.

Skipped paths are counted in the skippedLinks field of RewindFilesResult. Before v2.1.216, rewinds wrote and deleted through links.

Because Bash and subagent edits are invisible to checkpoints, keep git as your safety net for anything else.

Switching it on

Two options are needed:

PurposeTypeScriptPython
Track editsenableFileCheckpointing: trueenable_file_checkpointing=True
Put UUIDs on user messages in the streamextraArgs: { "replay-user-messages": null }extra_args={"replay-user-messages": None}

Without the second, user messages carry no uuid and you have nothing to rewind to. Most examples also set permissionMode: "acceptEdits" so edits are applied without prompting; see permissions.

The basic flow

  1. Run the session with checkpointing on.
  2. Save the uuid of the first user message. Rewinding to it returns tracked files to their original state.
  3. Save the session_id if you will rewind after the stream has finished.
  4. To rewind later, resume the session with an empty prompt, enable checkpointing on that resumed session too, and call the rewind method from inside the response loop.

Here it is in TypeScript, applied to an agent that upgrades a config format:

import { query } from "@anthropic-ai/claude-agent-sdk";

const opts = {
  enableFileCheckpointing: true,
  permissionMode: "acceptEdits" as const,
  extraArgs: { "replay-user-messages": null }
};

let restorePoint: string | undefined;
let sessionId: string | undefined;

try {
  for await (const msg of query({ prompt: "Convert config/*.ini files to TOML", options: opts })) {
    if (msg.type === "user" && msg.uuid && !restorePoint) restorePoint = msg.uuid;
    if ("session_id" in msg) sessionId = msg.session_id;
  }
} catch (err) {
  // An error result has already been yielded, so the IDs above are captured.
  console.error("Run ended with an error", err);
}

// Later, after a human rejects the change:
if (restorePoint && sessionId) {
  const undo = query({ prompt: "", options: { ...opts, resume: sessionId } });
  for await (const _ of undo) {
    await undo.rewindFiles(restorePoint);
    break;
  }
}

And the Python equivalent, using ClaudeSDKClient:

from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, UserMessage, ResultMessage

opts = ClaudeAgentOptions(
    enable_file_checkpointing=True,
    permission_mode="acceptEdits",
    extra_args={"replay-user-messages": None},
)

restore_point = session_id = None
async with ClaudeSDKClient(opts) as client:
    await client.query("Convert config/*.ini files to TOML")
    async for msg in client.receive_response():
        if isinstance(msg, UserMessage) and msg.uuid and not restore_point:
            restore_point = msg.uuid
        if isinstance(msg, ResultMessage):
            session_id = msg.session_id

# Later:
async with ClaudeSDKClient(ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)) as client:
    await client.query("")
    async for _ in client.receive_response():
        await client.rewind_files(restore_point)
        break

The empty prompt exists only to open a connection to the CLI process so the rewind call has something to talk to.

Rewinding from the command line

If you have the session ID and checkpoint UUID, the claude binary can do the rewind. The SDK sets the required environment variable for you, but a bare CLI call does not:

CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true \
  claude -p --resume 3f2c9d1e-... --rewind-files 8a71b0c4-...

--rewind-files is not listed in claude --help but works. On success it prints Files rewound to state at message <uuid> and exits without sending a prompt.

Patterns

Roll back as soon as something goes wrong

Keep only the latest checkpoint, updating it as each new user message arrives, and rewind inside the loop if your own check fails. No session ID is needed because the connection is still open.

latest = None
async with ClaudeSDKClient(opts) as client:
    await client.query("Upgrade the project to Pydantic v2")
    async for msg in client.receive_response():
        if isinstance(msg, UserMessage) and msg.uuid:
            latest = msg.uuid
        if tests_are_red() and latest:   # your own check
            await client.rewind_files(latest)
            break

Keep every restore point

When the agent works over several turns, store each checkpoint with a label. Then a user can keep the refactor from turn one and discard the tests from turn two.

interface RestorePoint { id: string; label: string; at: Date }
const points: RestorePoint[] = [];

for await (const msg of run) {
  if (msg.type === "user" && msg.uuid) {
    points.push({ id: msg.uuid, label: `Before turn ${points.length + 1}`, at: new Date() });
  }
}
// Later: resume and rewindFiles(points[1].id)

Trying it locally

A quick way to see it working:

  1. Create a small pricing.py with a few functions and no comments.
  2. Run a script with checkpointing on and the prompt Add docstrings to pricing.py.
  3. Open the file in your editor and watch the docstrings appear.
  4. Have the script ask "Rewind? (y/n)" and, on y, resume and call the rewind method with the first UUID.
  5. Watch the file snap back to the original.

Run Python with python script.py or TypeScript with npx tsx script.ts.

Limitations

LimitDetail
Three tools onlyWrite, Edit and NotebookEdit. Bash changes are not captured.
SubagentsTheir edits are not tracked or restored (except a foreground context: fork skill). Use git.
One sessionCheckpoints belong to the session that made them.
Content onlyDirectory operations are not undone.
Local onlyRemote and network files are not tracked.

Troubleshooting

The option or method does not exist. Upgrade: pip install --upgrade claude-agent-sdk or npm install @anthropic-ai/claude-agent-sdk@latest.

message.uuid is missing. You did not pass replay-user-messages in extraArgs / extra_args.

"No file checkpoint found for this message". Checkpointing was off in the original session, or the session did not complete before you resumed. Enable it on the original run, let it finish, then resume with an empty prompt and rewind once.

"File rewinding is not enabled". The session doing the rewind does not have checkpointing on. The SDK only sets CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING when the option is enabled on that session, so set it on the resumed session as well. For a bare claude -p call, set the environment variable yourself.

"ProcessTransport is not ready for writing". You called rewind after the loop finished, when the CLI process had already closed. Resume with an empty prompt and rewind inside the new loop.