CCLearn

Git under an agent: what undo actually covers

verified against Claude Code 2.1.261 · 2026-09-0918 min

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 madeWhat 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 skillgit
A /code-review --fix rundepends on where the review ran — see below
A Bash command — rm, mv, cp, a sed -i, a codegen scriptgit
Your own manual edits, or another concurrent sessiongit
A tracked path that is a symlink or hard linkgit — /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 status output, 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.

SituationWhat happens to the work
Interactive exit, worktree clean, session unnamedWorktree and branch removed automatically; there was nothing in them
Interactive exit, worktree clean, session namedYou are prompted first, so you can keep it for later
Interactive exit, worktree has workYou are prompted; remove deletes the directory, the branch and the work
A -p run endsNo 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 cleanupPeriodDaysSwept, 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 goneThe 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 status itself before a command that would discard uncommitted work, such as git reset --hard or rm -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

  1. 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 answer

    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 checkout becomes 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.

  2. 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 answer

    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 /rewind again fails identically. Check git status afterwards, and raise cleanupPeriodDays from its default of 30 if 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>.txt names paths for the other half only, the files skipped for link safety, so /debug does not buy you the backup-missing ones.

  3. A colleague finishes a --worktree session, 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 answer

    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 commit works 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.

  4. Your settings file has an allow rule for Edit(.claude/**) and Claude is in acceptEdits. It tries to write .claude/settings.json. What happens, and why is that not a bug in your rule?

    Show answer

    The write is held back rather than auto-approved. .claude is on the protected list — alongside .git and .config/git, .gitconfig, .gitmodules and toolchain and editor directories such as .vscode, .idea and .husky — and that class of write is held back in every mode except bypassPermissions and the planning session that has bypass permissions available. acceptEdits is 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.

Sources