"Claude Code Checkpoints: How Rollback Works in Automations"

In interactive Claude Code, checkpoints are on by default, and you undo with /rewind or by pressing Esc twice when the prompt is empty. Headless runs (claude -p or the Agent SDK) have nobody to press that key. You have to turn file checkpointing on, save the first user-message UUID and the session ID, and have your script call rewindFiles() / rewind_files() or run claude -p --resume <session-id> --rewind-files <checkpoint-uuid>. Rewind only restores edits made with Write, Edit and NotebookEdit. It doesn't cover Bash, most subagent edits or directories, so git stays your main safety net.
Which option is right for you?
Your setup depends on what the job does: rewind fully covers only jobs that edit files through Claude's own edit tools.
| Your unattended job mostly… | Main safety net | Role of checkpoint rewind |
|---|---|---|
| Edits files (refactors, doc updates, lint fixes made with Edit/Write) | Rewind, with a git commit underneath | Primary undo. Fast, and it can go back to a middle turn |
Runs shell commands that change files (formatters, codemods, npm install, sed -i, mv) | git, or a worktree you can throw away | Partial at best. Bash changes are not tracked |
| Uses subagents or background forked skills | git, or a worktree | Assume it won't restore their edits |
| Touches things outside the repo (APIs, databases, deploys) | Permission limits and dry runs | None. Rewind only covers local files |
If you're not sure, start every run on a clean git commit or in a worktree, and treat rewind as a convenience on top.
A detail people often miss: --permission-mode acceptEdits also auto-approves common filesystem commands such as mkdir, touch, mv and cp (Headless docs). Those run through Bash, so rewind can't undo them even in a job you thought "only edits files."
What a checkpoint is and what it isn't
A checkpoint is a snapshot of files Claude edited with its file tools. It is not a full backup of your working tree. In interactive mode, Claude Code takes one each time you send a prompt that starts a turn. It keeps file snapshots for the 100 most recent checkpoints in a session and saves them with the conversation, so /rewind still works after you resume (Checkpointing docs). The rewind menu can restore code and conversation, only the conversation, or only the code. It can also summarize from or up to a chosen point (same source).
These limits matter more when nobody is watching:
- Bash changes are not tracked. Rewind can't undo
rm,mv,cp,sed -iorecho > filewhen they run through Bash (Checkpointing docs; SDK docs). - Subagent edits usually aren't restored. The only exception is a skill with
context: forkthat runs in the foreground. Forked skills run in the background by default, and you setbackground: falseto run one in the foreground. For every other subagent, the docs say to revert with git (Checkpointing docs). - Directories aren't undone. Rewind deletes files Claude created and restores files it modified. It doesn't reverse creating, moving or deleting directories, and it doesn't track remote or network files (SDK docs).
- Links and some other paths are skipped. Since v2.1.216, rewind skips symlinks, hard links, other non-regular files, files whose parent directory no longer resolves to where it was at checkpoint time, and files whose backup can't be read safely.
RewindFilesResult.skippedLinkscounts all of these. Before v2.1.216, rewind wrote and deleted through links (SDK docs). - Snapshots expire. By default, the retention sweep deletes a session's snapshots about 30 days after the session last saved one. After that, a rewind can fail with "No files were restored." To keep snapshots longer, set
cleanupPeriodDays(Checkpointing docs; Settings reference). - Checkpoints don't replace version control. Anthropic describes them as "quick, session-level recovery" and recommends git for permanent history (Checkpointing docs).
The fileCheckpointingEnabled setting turns the interactive /rewind snapshots on or off. When it is unset, snapshots are on (Settings reference).
How headless checkpointing differs
In the Agent SDK, checkpointing is opt-in, and it takes two options (SDK docs):
| Purpose | Python | TypeScript |
|---|---|---|
| Track file changes | enable_file_checkpointing=True | enableFileCheckpointing: true |
| Get checkpoint UUIDs in the stream | extra_args={"replay-user-messages": None} | extraArgs: { "replay-user-messages": null } |
With both set, each user message in the response stream carries a uuid you can use as a restore point. Rewinding to the first user message's UUID returns tracked files to their original state. Rewinding files doesn't rewind the conversation, which stays as it was (SDK docs).
The official examples also set permission_mode="acceptEdits" so edits go through without a prompt (SDK docs). Always pass a permission mode explicitly. A -p run that sets none starts in a built-in default mode, and that default can be auto (Headless docs).
Pattern 1: Rewind inside the run when validation fails
Use this pattern when your script can tell during the run that something has gone wrong. Examples: a test command fails, a protected file changes, or the diff is much larger than expected.
The docs' "checkpoint before risky operations" pattern keeps a user-message UUID and calls rewind_files() as soon as your own condition is true, while the stream is still open (SDK docs). Here is a shortened Python version. should_revert() is a placeholder for your own check:
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None},
)
first_checkpoint = None
async with ClaudeSDKClient(options) as client:
await client.query("Update dependencies and fix type errors")
async for message in client.receive_response():
if isinstance(message, UserMessage) and message.uuid and not first_checkpoint:
first_checkpoint = message.uuid
if should_revert(message) and first_checkpoint: # your own check
await client.rewind_files(first_checkpoint)
break
You need to make two decisions:
- Pick which checkpoint to rewind to. The docs' version overwrites its variable every turn, so it keeps only the latest checkpoint and undoes only the last step. An unattended job usually wants to throw away the whole attempt, so the example above keeps the first UUID. To keep some turns and undo others, store every UUID. The docs call this "Multiple restore points" (SDK docs).
- Call rewind before the loop ends. If you call
rewindFiles()after you've finished iterating, you get "ProcessTransport is not ready for writing," because the connection closes when the loop completes (SDK docs).
Pattern 2: Record IDs now, rewind later
Use this pattern when you only find out a run was bad after it ends, for example when CI goes red an hour later or someone reviews the diff the next morning.
- Save
checkpoint_idandsession_idfor every run. A small JSON log next to the job output is enough. The docs show how to capturesession_idfrom theResultMessage(SDK docs). - Rewind later by resuming with an empty prompt. Open a new client with
enable_file_checkpointing=Trueandresume=session_id, send"", and callrewind_files(checkpoint_id)inside the response loop (SDK docs). - Or rewind from the shell, without SDK code:
CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true \
claude -p --resume <session-id> --rewind-files <checkpoint-uuid>
--rewind-files doesn't appear in claude --help, but the CLI accepts it. When it works, it prints Files rewound to state at message <checkpoint-uuid> and exits without sending a prompt. Without the environment variable, the bare CLI fails with "File rewinding is not enabled" (SDK docs).
Run the rewind on the same machine that ran the job. Checkpoints belong to the session that created them (SDK docs), and the snapshots live in local Claude Code state (Checkpointing docs). --resume <id> can find a session in other projects on the same machine (CLI reference). Running the rewind from the original project directory is still the safest choice.
Let the run finish before you depend on its checkpoints. "No file checkpoint found" can mean the session wasn't properly completed (SDK docs), and a claude -p run stopped with SIGTERM leaves its current turn unfinished (Headless docs). If your supervisor has to stop a job, send SIGINT or call the SDK's interrupt() first, as the headless docs recommend.
Using only the CLI, without the SDK
You can do this from a shell script. --replay-user-messages requires both --input-format stream-json and --output-format stream-json. You can also give the session a known ID up front with --session-id <uuid> so you don't have to parse it from the output (CLI reference).
The SDK sets CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING internally only when checkpointing is enabled, and the bare CLI never sets it (SDK docs). "No file checkpoint found" can mean checkpointing was off on the original session, so the safe reading is to set the variable on the original run as well as on the rewind. The docs don't describe this CLI-only flow end to end, so this is an inference. Test it on a throwaway repo before you rely on it.
Here is an example launch. The headless page doesn't show the stream-json input message format, so check it against the Agent SDK docs before use:
SID=$(uuidgen | tr 'A-Z' 'a-z')
echo '{"type":"user","message":{"role":"user","content":"Fix lint errors in src/"}}' | \
CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true claude -p \
--session-id "$SID" \
--input-format stream-json --output-format stream-json --verbose \
--replay-user-messages --permission-mode acceptEdits \
> run.jsonl
CHECKPOINT=$(jq -r 'select(.type=="user" and .uuid) | .uuid' run.jsonl | head -1)
echo "$SID $CHECKPOINT" >> agent-runs.log
Don't use --no-session-persistence on runs you might want to rewind. It stops the session from being saved to disk, so the session can't be resumed (CLI reference), and a later rewind depends on resuming.
Rewind vs. git vs. worktrees: which to use when
Use all of them together. Each one covers gaps the others leave.
| Mechanism | Covers | Misses | Best for |
|---|---|---|---|
| Checkpoint rewind | Write/Edit/NotebookEdit changes to local files (SDK docs) | Bash changes, most subagent edits, directories, links, network files | Fast, precise undo of the agent's own edits, including going back to a middle turn |
| Git commit/branch before the run | Everything in the repo that git tracks | Untracked/ignored files unless you include them; anything outside the repo | The baseline you can always reset to |
--worktree / -w | Runs in an isolated worktree at <repo>/.claude/worktrees/<name> (CLI reference) | Side effects outside the repo (APIs, databases) | Recurring jobs that should never touch your main checkout |
| Permission limits | Blocks risky actions up front, e.g. --disallowedTools "Bash(rm *)", --permission-prompts none (v2.1.259+) (CLI reference; Headless docs) | Mistakes made with allowed tools | Keeping the damage within what rewind and git can undo |
How to decide:
- If you need to undo a run days later, check that the snapshots are still there (about 30 days by default) and raise
cleanupPeriodDaysif needed (Checkpointing docs). - If the job runs where other people's settings could load (CI, shared machines), consider
--bare. The docs recommend it for scripted calls. In bare mode Claude Code skips hooks, plugins, MCP servers and CLAUDE.md from the host, and it needsANTHROPIC_API_KEYinstead of a subscription login (Headless docs).
A practical setup checklist
- Start each run on a clean git state, or in a worktree with
claude -w <name>(CLI reference). - Turn on checkpointing (
enableFileCheckpointing/enable_file_checkpointing, or the env var for the bare CLI) andreplay-user-messages(SDK docs). - Set the permission mode explicitly (Headless docs).
- Save the first user-message UUID and the session ID for every run.
- Limit runaway runs with
--max-turnsand--max-budget-usd, which work in print mode only. The budget cap uses a client-side cost estimate that can differ from your bill, and spend can go past it, so leave headroom (CLI reference). - Add a validation step (tests, lint, diff-size check) and rewind automatically when it fails.
- After any rewind, run
git status. Anything still changed came from Bash, a subagent or a skipped path. Reset it with git. - Read
skippedLinksinRewindFilesResultand alert when it isn't zero, because those files kept their current contents (SDK docs).
Troubleshooting quick reference
Most rewind failures come from a missing option on the original run or from calling rewind at the wrong time.
| Symptom | Likely cause | Fix |
|---|---|---|
message.uuid missing | replay-user-messages not set | Add it to extraArgs / extra_args (SDK docs) |
| "No file checkpoint found for this message" | Checkpointing was off on the original session, or the session didn't finish | Enable it on the original run and let the session finish before resuming (same) |
| "File rewinding is not enabled" | Bare CLI without the env var, or a resumed SDK session without the option | Set CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true, or enable the option on the resumed session (same) |
| "ProcessTransport is not ready for writing" | Rewind called after the loop ended | Resume with an empty prompt, then rewind (same) |
| "No files were restored" | Snapshots removed by retention cleanup | Raise cleanupPeriodDays; use git for this run (Checkpointing docs) |
| Files changed after a "successful" rewind | Bash edits (including mv/cp allowed by acceptEdits), subagent edits, or skipped paths | git status, then git checkout/git clean the leftovers |
| Options not recognized | Old SDK | pip install --upgrade claude-agent-sdk or npm install @anthropic-ai/claude-agent-sdk@latest (SDK docs) |
FAQ
Are checkpoints on by default in claude -p or the Agent SDK?
No. Interactive sessions take snapshots by default. In the Agent SDK you have to enable file checkpointing and replay-user-messages, and with the bare CLI you set CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true (SDK docs).
Can I skip git if I use checkpoints?
No. Anthropic says checkpoints are for quick, session-level recovery and recommends git for permanent history. Rewind also misses Bash changes, most subagent edits and directory operations (Checkpointing docs).
Does rewinding files also rewind the conversation?
Not in the SDK. rewindFiles() / rewind_files() restores files on disk and leaves the conversation history as it was. The interactive /rewind menu lets you choose code, conversation, or both (SDK docs; Checkpointing docs).
How long can I wait before rewinding a run?
By default, about 30 days after the session last saved a snapshot. After that, cleanup can remove the snapshots. You can change this with cleanupPeriodDays (Checkpointing docs).
Can I rewind on a different machine, such as a fresh CI runner?
Not reliably. Checkpoints are tied to the session that created them, and the snapshots live in local Claude Code state (SDK docs; Checkpointing docs). On short-lived runners, rely on git or worktrees instead.
Next steps
Pick the job type that matches your automation and try only the checklist items for that type today. Each list takes one throwaway repo and one run.
If your job mostly edits files (Edit/Write):
- Copy the Pattern 1 code. Replace
should_revert()with one check, such as "tests fail." - Run it once on a throwaway repo. Make the check fail on purpose and confirm the files go back.
- Run
git statusafterwards. If something is still changed, find which command caused it.
If your job mostly runs Bash commands:
- Wrap the run in
claude -w <name>, or commit before each run. - Write a one-line revert script:
git reset --hard && git clean -fd, run inside the worktree only. - Add
--disallowedTools "Bash(rm *)"and--max-turns, then do one test run.
If your job uses subagents or forked skills:
- Assume rewind won't restore their edits, and run the job in a worktree.
- Save the session ID and first UUID anyway (Pattern 2), so the main agent's own edits can still be rewound.
- After a test run, compare
git diffwith the rewind result to see what the subagents changed.
If you'd rather not build this yourself: a freelance developer who knows the Agent SDK and your CI can usually set up a worktree, validation and revert script for you. You pay for their time on top of your usual Claude usage. Ask for a fixed-scope quote, because rates vary too much to give a reliable figure here. This suits you if you have one important recurring job and little time. It's a poor fit if your jobs change every week, because you'll keep paying for changes. Another route is to run the job in GitHub Actions or GitLab CI with Claude Code (GitHub Actions docs, GitLab CI/CD docs). Every run then starts from a clean checkout, and you undo it by not merging its branch, so you don't depend on checkpoints at all.
Related on Headless Notes: look for our guides on running claude -p on a schedule, choosing permission modes for unattended runs, and using git worktrees for agent jobs. They cover the layers that this article treats as "use git instead."
Sources
Checked on 2026-10-15.