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
| Tracked | Not tracked |
|---|---|
| Edits through Write, Edit and NotebookEdit | Anything done through Bash (sed -i, mv, echo > file) |
Edits by a skill with context: fork running in the foreground | Edits made by subagents |
| Files created by those tools (rewind deletes them) | Directory creation, moves and deletion |
| Local files | Remote 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:
| Purpose | TypeScript | Python |
|---|---|---|
| Track edits | enableFileCheckpointing: true | enable_file_checkpointing=True |
| Put UUIDs on user messages in the stream | extraArgs: { "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
- Run the session with checkpointing on.
- Save the
uuidof the first user message. Rewinding to it returns tracked files to their original state. - Save the
session_idif you will rewind after the stream has finished. - 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:
- Create a small
pricing.pywith a few functions and no comments. - Run a script with checkpointing on and the prompt
Add docstrings to pricing.py. - Open the file in your editor and watch the docstrings appear.
- Have the script ask "Rewind? (y/n)" and, on
y, resume and call the rewind method with the first UUID. - Watch the file snap back to the original.
Run Python with python script.py or TypeScript with npx tsx script.ts.
Limitations
| Limit | Detail |
|---|---|
| Three tools only | Write, Edit and NotebookEdit. Bash changes are not captured. |
| Subagents | Their edits are not tracked or restored (except a foreground context: fork skill). Use git. |
| One session | Checkpoints belong to the session that made them. |
| Content only | Directory operations are not undone. |
| Local only | Remote 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.