Git under an agent: what undo actually covers
By the end you can
- State which changes a checkpoint can take back and which only git can
- Predict what a code restore does when the session is old or a tracked path is a link
- Say what Claude Code puts in the prompt about your git state and what it stamps on your commits
- Name the moment a worktree destroys work, and what counts as work worth keeping
- Recognise the repository writes no permission mode will auto-approve
Ninety minutes into a repository you did not write. Claude has edited eleven
files through its file tools, run a codemod across forty more in one Bash call,
and handed the imports in a dozen others to a subagent. Something in the middle
of it was wrong. You press Esc twice and pick a checkpoint from an hour ago.
Eleven files come back. Fifty-two do not, and nothing on screen separates them.
Lesson 3.4 gave you the rewind menu and named the exclusions; Lesson 4.5 argued for committing at the seams. This is the version of both that survives contact with a large repository, where the changes are numerous enough that "did that come back?" is not answerable by looking.
The undo surface, in one table
The fact this lesson exists to install: what a rewind can restore is decided by the channel a change travelled through, not by how big it was.
| How the change was made | What takes it back |
|---|---|
Claude's file editing tools (Edit, Write) | /rewind |
A foreground forked skill — context: fork with background: false | /rewind, as usual |
| Any other subagent, including a background forked skill | git |
A /code-review --fix run | depends on where the review ran — see below |
A Bash command — rm, mv, cp, a sed -i, a codegen script | git |
| Your own manual edits, or another concurrent session | git |
| A tracked path that is a symlink or hard link | git — /rewind skips it and says so |
The documentation states the boundary twice, once as a limitation and once as a remedy:
Checkpointing does not track files modified by bash commands. — Checkpointing
Any other subagent: rewinding doesn't restore the edits. Use git to revert them. — Checkpointing
The two forked-skill rows are one distinction with a date on it. A context: fork skill runs in the background by default, so its edits land outside your
checkpoints and git is the undo; background: false holds the fork inside the
turn that invoked it, and those edits are checkpointed like any other. That key
"Requires Claude Code v2.1.218 or later", and before it "forked skills always
blocked the turn until they finished"
(Skills) — every fork was a foreground
fork, and a background forked skill was not something you could have. Which is
the uncomfortable part: across that upgrade the same skill's edits left the
checkpoint without a character of the skill file changing. Frontmatter is not
the whole answer either: Claude Code waits for the fork even without
background: false in non-interactive mode with -p or the Agent SDK, when
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS is 1, when an earlier invocation of the
same skill is still running, and when a scheduled task fires with the skill as
its prompt. A fork that runs inside your turn is checkpointed like any other
edit, so what decides your undo surface is where the fork actually ran.
/code-review --fix is the row that moves under you. A background review applies
its fixes outside your session's checkpoints, so /rewind does not undo them and
git is the way back; a foreground review edits your working tree during your own
turn, so /rewind restores its edits as usual. The review runs in the background
by default — "before v2.1.218, it ran inside your conversation"
(Code review) — with a short list
of exceptions that put it back in the foreground, which Lesson 11.5 spells out.
So treat those fixes as outside your checkpoints unless you know you hit one of
the exceptions. A habit formed before that upgrade, when a rewind always took
them back, is now wrong in the expensive direction.
The concurrent-session row is narrower than people repeat it. External edits are normally not captured unless they happen to modify the same files as the current session — so a second session's edit to a file yours also touched can be swept up in your restore. That is not protection, it is a coin toss you did not know you were making. Lesson 8.6's answer, one checkout per session, is why that row should never come up.
The restore that reports success and restores nothing
Two clocks run on the snapshots behind those checkpoints. The first is a count: Claude Code keeps file snapshots for the 100 most recent checkpoints in a session, and discarding an older checkpoint deletes the snapshot files no remaining checkpoint references — except each file's first snapshot, which the VS Code extension keeps as the baseline for its session diffs. The second clock is the retention sweep:
Claude Code deletes a session's file snapshots in the retention sweep, by default about 30 days after the session last saved one. — Checkpointing
Resume that session in week five and /rewind still lists its checkpoints —
Claude Code saves checkpoints with the conversation, and the conversation is
still there. Restoring can then fail with No files were restored, and the
documented recovery is to undo the change another way: ask Claude to reverse its
edits, or restore the files from version control. With the backups gone, running
/rewind again fails the same way. Raise cleanupPeriodDays, default 30, if
you want a longer window.
The link row carries the same shape of caveat. Current documented behaviour is
that a code restore skips any tracked path that is, or became, a symlink, hard
link or other non-regular file — and also one whose directory changed since the
checkpoint, or whose backup cannot be safely read — warns Restored the code, but skipped N files, and leaves those files with their current contents. That is
what Lesson 3.4 listed, and it arrived in v2.1.216; before it, /rewind wrote
and deleted through links at tracked paths and did not report a partial restore
(Errors). The upgrade turned a silent
write-through into a visible skip, which is the better failure — and still leaves
you holding a file whose contents are the ones the session put there. If you did
not create that link yourself, inspect the path before you trust it: something
replaced the file after the checkpoint.
The advice that assumes the boundary
The best-practices guide actively encourages risk-taking on the strength of checkpoints — tell Claude to try something risky, and if it doesn't work, rewind and try a different approach. Good advice with an unstated precondition, which the same page supplies in a warning three lines later: checkpoints only track changes made through Claude's file editing tools, changes made through Bash commands or external processes are not captured, and this isn't a replacement for git (Best practices).
What Claude knows about your repo, and what it stamps on it
Two git-related things are true at session start without your arranging them:
At session start, Claude Code adds two git-related pieces to Claude's prompt: its built-in instructions for how to write commits and pull requests, in the Bash tool's description, and a git status snapshot of your repository in the system prompt, meaning the current branch, the main branch,
git statusoutput, and recent commits. — Settings reference
Both leave together with includeGitInstructions: false — the documented example
is a project that uses its own git workflow skills — and
CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS takes precedence over the key for one
session. Cloud sessions never include the snapshot. And note what the snapshot
is: a picture taken at session start. An hour of edits later, a fresh
git status costs one command and tells you something the prompt cannot.
The other stamp is on the way out. Unless you change it, a commit Claude writes
carries a Co-Authored-By trailer naming the session's active model, such as
Claude Sonnet 5 — Claude alone when Claude Code recognises the model but not
its exact version, Claude Code when it cannot match the model ID to any Claude
model. attribution.commit replaces or empties that text, attribution.pr does
the same for pull request descriptions, and attribution.sessionUrl: false drops
the claude.ai session link that cloud and Remote Control sessions add
(Settings reference).
Decide it deliberately: once a branch is merged, that trailer is usually the only
mark left separating a change an agent wrote from one you typed.
Worktrees: where uncommitted work goes to die
Lesson 8.6 built worktrees as isolation — one checkout per session so parallel writers cannot collide. Read the same mechanism as a recovery surface and a different set of facts matters.
The recovery path still works inside one. A worktree gets its own files and
branch, but it shares the repository's .git directory, and "sandboxing allows
those writes, so commands such as git commit work from inside a worktree with
the sandbox enabled" (Worktrees).
Isolation does not suspend "commit before the risky thing". It raises the stakes
on it, because of the next fact.
Exit is the destructive moment. Claude checks the worktree for work that removal would delete — changed or untracked files, and new commits — and then:
The worktree has work in it: Claude prompts you to keep or remove the worktree. Keeping preserves the directory and branch so you can return later. Removing deletes the worktree directory and its branch, along with all the work in them — Worktrees
"Along with all the work in them" includes commits. A branch you never pushed is deleted by answering one prompt at the end of a long day.
| Situation | What happens to the work |
|---|---|
| Interactive exit, worktree clean, session unnamed | Worktree and branch removed automatically; there was nothing in them |
| Interactive exit, worktree clean, session named | You are prompted first, so you can keep it for later |
| Interactive exit, worktree has work | You are prompted; remove deletes the directory, the branch and the work |
A -p run ends | No exit prompt, so Claude cleans up nothing and leaves its creation lock in place — remove it yourself with git worktree remove |
A backgrounded session's worktree ages past cleanupPeriodDays | Swept, unless it still holds changed files, untracked files or unpushed commits — or unless one of the sweep's other exemptions applies, such as a worktree you created yourself with git worktree add |
| You resume a session whose worktree directory is gone | The session resumes in your launch directory, says the worktree is gone, and clears the binding |
Two readings. The sweep spares a worktree that still holds work, and unpushed commits count as work — pushing gives the commits somewhere else to live, which is the commit argument one step further out. And the last row is a session-continuity fact: resuming a worktree session normally returns the session to its worktree, so the day you land in the launch directory instead, read the message — for this row it says the work is gone rather than misplaced.
Both readings have a version behind them, and both are newer than the mechanism they describe.
The sweep's exemption for a worktree you made yourself rests on a marker Claude
Code writes into the git metadata of every worktree it creates; the sweep keeps
any worktree without one. That check is recent: "Before v2.1.246, the sweep
didn't check for the marker, and could remove a worktree you created yourself
when an old background-session record pointed at it"
(Worktrees). So a hand-made
git worktree add is protected now, and was not always. The exemption that never
depended on a version is the first one — changed files, untracked files, or
unpushed commits — which is the same instruction as everywhere else on this page:
the work has to be somewhere the sweep can see it, and pushed beats committed.
The resume reading holds in non-interactive mode too, which was not always so:
"Before v2.1.212, a non-interactive resume stayed in the starting directory and
ExitWorktree reported that there was no active worktree session to exit"
(Worktrees). A -p run with
--continue or --resume, and the Agent SDK, now return to the worktree the way
an interactive resume does. That is what gives the last table row its meaning in
a script as well as at a prompt: landing in your launch directory used to be
routine there, and now it means something specific happened. Which thing is on
screen, not in the directory. Interactively there are four answers: a deleted
worktree; a worktree that fails Claude Code's check that it is still a checkout
separate from the main one; a launch directory it cannot vouch for the worktree
from; and --fork-session, which starts the forked session where you launched
from and leaves the original's worktree untouched. Under -p and the Agent SDK
the list is shorter, because Claude Code stops "the resume with a stderr error
for every refusal except a gone worktree"
(Worktrees) — so a script that is
running at all and not inside its worktree either lost the worktree or forked.
The other two refusals do not leave you in the launch directory; they end the
run. Read the message.
The repository writes nothing auto-approves
One class of write is held back in every mode but bypassPermissions and the
planning session that has bypass permissions available — Lesson 10.4's survival
list, quoted there. Know the protected list by shape: .git and .config/git,
.gitconfig and .gitmodules, toolchain and editor directories such as
.vscode, .idea and .husky, and .claude — with a carve-out for
.claude/worktrees, where Claude stores its own worktrees. Which of the six
modes prompts for such a write, routes it to the classifier or denies it outright
is Lesson 10.1's table, unchanged here. What this lesson adds is what your own
settings can do about it.
Lesson 4.5 listed the git commands auto mode's classifier blocks by default — the
ones it presumes would discard uncommitted changes, an --amend over a commit
the session did not create, force push. One mechanism behind them belongs here,
because it explains why the same command sometimes sails through:
Claude Code also runs
git statusitself before a command that would discard uncommitted work, such asgit reset --hardorrm -rf, and shows the classifier whether staged, modified, or untracked work is present. — Permission modes
The classifier is not only reading the command; it is told whether your tree
has anything to lose. Which closes the loop from the other end: commit, and
there is nothing for reset --hard to throw away, nothing for the sweep to
protect, and nothing that check has to report.
The point
Every recovery mechanism here is bounded by something with nothing to do with how important the work was: the tool that made the edit, a count of a hundred, a thirty-day sweep, a version number, an answer to a prompt at the end of a session, a directory that no longer exists. Git is bounded by none of them. At repository scale that stops being tidiness — the operations big enough to be worth automating are exactly the ones nothing else holds.
Checkpoint
-
A session edits six files with the Edit tool, runs a formatter over the whole package in one Bash call, then hands the imports to a subagent. You rewind to before all three. Which came back, and what single action taken beforehand would have covered all of them?
Show answerAnswer
Only the six files edited with
Edit. Checkpointing does not track files modified by bash commands, so the formatter's sweep over the whole package stayed exactly as it left it, and rewinding does not restore the edits of any other subagent — git is the way back for both. Note what decided that: the channel each change travelled through, not how big it was. And nothing on screen separates the files that came back from the ones that were never in a checkpoint.One action would have covered all three: commit before the run. Ten seconds, and
git checkoutbecomes the undo for exactly the class of change rewind cannot touch. That is the trap in miniature — the cheapest ways to make a large change, a Bash sweep and a subagent, are the two channels with no checkpoint behind them. -
You resume a five-week-old session, choose Restore code, and get
No files were restored: 1 file failed (backup missing, or the file could not be updated). Say why five weeks produces that, what your options are now, and what a colleague on a build older than v2.1.260 would have seen instead.Show answerAnswer
The snapshots were swept. Claude Code deletes a session's file snapshots in the retention sweep, by default about 30 days after the session last saved one — but checkpoints are saved with the conversation, and the conversation is still there, so week five still lists checkpoints it can no longer restore. The list is not evidence that the backups exist.
Your options are the documented recovery: undo the change another way — ask Claude to reverse its edits, or restore the files from version control. Running
/rewindagain fails identically. Checkgit statusafterwards, and raisecleanupPeriodDaysfrom its default of30if you want a longer window next time.On a build older than v2.1.260 your colleague would have seen no message at all: Claude Code silently skipped files whose backups were missing and the rewind appeared to succeed. What the newer message still does not give you is which file — it is a count, like the partial-restore warning. The debug log at
~/.claude/debug/<session-id>.txtnames paths for the other half only, the files skipped for link safety, so/debugdoes not buy you the backup-missing ones. -
A colleague finishes a
--worktreesession, is asked whether to keep or remove the worktree, and picks remove because "the branch is still there". What is wrong with that reasoning, and what would have made it right?Show answerAnswer
The branch is not still there. Removing deletes the worktree directory and its branch, along with all the work in them — and "all the work" includes commits. The prompt itself does not tell you which case you are in: Claude checks for changed or untracked files and new commits before offering the choice, but a named session is prompted even when the worktree is clean, so the answer has to come from looking at the worktree rather than at the dialog. A branch nobody pushed is destroyed by one answer at the end of a long day.
Pushing first is what would have made the reasoning true. Unpushed commits count as work everywhere on this subject — they are also what exempts a worktree from the background sweep — and pushing gives the commits somewhere else to live. Committing inside the worktree is not enough on its own:
git commitworks fine in there, because the worktree shares the repository's.git, but the commit dies with the branch. If pushing is not an option, answer keep, which preserves the directory and branch so you can return. -
Your settings file has an allow rule for
Edit(.claude/**)and Claude is inacceptEdits. It tries to write.claude/settings.json. What happens, and why is that not a bug in your rule?Show answerAnswer
The write is held back rather than auto-approved.
.claudeis on the protected list — alongside.gitand.config/git,.gitconfig,.gitmodulesand toolchain and editor directories such as.vscode,.ideaand.husky— and that class of write is held back in every mode exceptbypassPermissionsand the planning session that has bypass permissions available.acceptEditsis neither. Exactly what you see (a prompt, a routing to the classifier, or an outright denial) is per-mode and is Lesson 10.1's table; in a mode that prompts, the prompt offers Yes, and allow Claude to edit its own settings for this session — approval granted at the moment it is needed and scoped to that session. The one carve-out inside the path is.claude/worktrees, where Claude stores its own worktrees.It is not a bug in your rule because the safety check runs before Claude Code evaluates allow rules from settings, so
Edit(.claude/**)never gets a chance to apply here. Level 10 taught you that deny wins; this is the narrower fact that for these paths, allow does not even play.