Turn 20. The agent says the refund endpoint is done. You run the suite and test/refund.test.ts fails on a case that passed an hour ago. Somewhere between turn 1 and turn 20, something broke.

git log shows the commit you started from and nothing after it. The agent never committed, so twenty turns of edits sit in your working tree as one diff. git bisect is the tool for "which change broke this", and it needs commits to walk. You have none.

Fix it for the next session. A Claude Code Stop hook writes one commit per turn to a private ref with git plumbing, so your branch, your staging area and your stash never move. A short script then bisects those snapshots in a scratch worktree while your checkout stays where it is. Hermes IDE, which I build, does the snapshot half without the hook.

Why /rewind and git stash create both fall short

Claude Code already has checkpoints. It captures your code before each prompt that starts a turn, keeps file snapshots for the 100 most recent checkpoints in a session, and /rewind (or Esc twice on an empty prompt) restores code, conversation or both. For "undo the last thing", that is the right tool.

The docs say checkpointing does not track files changed by Bash commands, so an rm, an mv, a formatter or a codegen script run by the agent is invisible to it. Restoring also changes your working tree: you can't run the test against turn 7 while keeping turn 20 on disk. The same page says checkpoints are not a replacement for version control.

The obvious git answer is git stash create. It builds a stash commit and prints its id "without storing it anywhere in the ref namespace". Your index and files stay as they are. It still falls short here:

  • It takes only a message. The -u option that pulls in untracked files belongs to push and save, so every file the agent created from scratch is missing from the snapshot.
  • A stash commit's first parent is the commit at HEAD. Twenty snapshots would be twenty siblings hanging off the same commit, and bisect needs a line from good to bad.

So build the commit by hand with the same plumbing, a temporary index, and the previous snapshot as the parent.

Step 1: a Stop hook that commits every turn to a private ref

Save this as .claude/hooks/snap.sh. It needs jq to read the hook input.

#!/bin/sh # .claude/hooks/snap.sh: snapshot the tree after each turn. # Always exits 0. Exit 2 would keep Claude from stopping. input=$(cat) sid=$(printf '%s' "$input" | jq -r .session_id) cd "$(printf '%s' "$input" | jq -r .cwd)" || exit 0 ref="refs/agent-turns/$sid" idx=$(mktemp) || exit 0 cp "$(git rev-parse --git-path index)" "$idx" && tree=$(GIT_INDEX_FILE="$idx" sh -c \ 'git add -A && git write-tree') rm -f "$idx" [ -n "$tree" ] || exit 0 prev=$(git rev-parse -q --verify "$ref") || prev=$(git rev-parse HEAD) [ "$tree" = "$(git rev-parse "$prev^{tree}")" ] && exit 0 c=$(git commit-tree "$tree" -p "$prev" \ -m "agent turn $(date -u +%Y-%m-%dT%H:%M:%SZ)") git update-ref "$ref" "$c" exit 0 

The script copies your real index to a temp file and points GIT_INDEX_FILE at it, so git add -A starts from your current index and your real staging area is never written. git rev-parse --git-path index finds that real index, including inside a linked worktree, and it runs before GIT_INDEX_FILE is set because --git-path takes that variable into account.

git add -A stages new, modified and deleted files and skips anything in .gitignore, so a .env or node_modules you already ignore stays out of the snapshots. Anything untracked and not ignored goes in, and it stays in the snapshot history after you delete the file. Check your .gitignore before you turn the hook on.

Then the tree becomes history:

  • git commit-tree turns that tree into a commit whose parent (-p) is the previous snapshot. The first snapshot of a session hangs off HEAD. That chain is what makes bisect possible.
  • git update-ref moves refs/agent-turns/<session-id> to the new commit. A turn that changed nothing produces no snapshot.

Register it in .claude/settings.json:

{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "\"${CLAUDE_PROJECT_DIR}\"/.claude/hooks/snap.sh" } ] } ] } } 

Then chmod +x .claude/hooks/snap.sh. Per the hooks reference, Stop fires when Claude finishes responding, takes no matcher, and gets JSON on stdin with session_id and cwd among other fields. It also asks you to wrap the placeholder in double quotes in a shell-form command, which keeps a project path with a space working. The rest of the script follows from that page:

  • Exit code 2 from a Stop hook blocks the stop and keeps Claude going. A snapshot script must never do that, hence exit 0 on every path.
  • Stop does not run when you interrupt the turn. Those edits are not lost: they land in the next turn's snapshot.
  • If Claude enters a worktree, $CLAUDE_PROJECT_DIR still points at the original project root while cwd follows the worktree. That is why the script cds to cwd.

The refs live under refs/, and git's worktree docs say refs there are shared by every worktree of the repository. Step 3 depends on that. By default git push sends only the current branch, so these refs stay on your machine unless you push them by name, use --mirror, or set a remote.<name>.push refspec that covers them.

Step 2: read the turns

Each session gets one ref. List them, then walk one:

git for-each-ref refs/agent-turns/ ref=refs/agent-turns/<session-id> git log --oneline "$ref" --not HEAD # one line per turn git log -p "$ref" --not HEAD # every turn's diff git diff "$ref~1" "$ref" # the last turn only 

--not HEAD stops the log at the commit the session started from, so you see the agent's turns and nothing older. Because each snapshot's parent is the previous one, git show <snapshot> is exactly what that one turn changed, including files a shell command created or deleted.

To pull one file back from before a given turn without touching anything else:

git restore --source=<snapshot>~1 -- src/refund.ts 

Step 3: bisect the snapshots in a scratch worktree

Save this as find-bad-turn.sh somewhere outside the repo:

#!/bin/sh # find-bad-turn.sh <session-id> <test command...> set -eu ref="refs/agent-turns/$1"; shift good=${GOOD:-$(git rev-parse HEAD)} dir=$(mktemp -d) git worktree add --detach "$dir" "$ref" cd "$dir" git bisect start "$ref" "$good" git bisect run "$@" || true git bisect reset cd - >/dev/null git worktree remove --force "$dir" 

Run it from your repo with the failing test as the command:

~/bin/find-bad-turn.sh <session-id> sh -c \ 'npm ci --silent || exit 125; npm test' 

The scratch worktree is a throwaway folder with a detached HEAD, the case git worktree add --detach is meant for. Bisect state lives in refs/bisect, which the same docs list as one of the exceptions that are not shared between worktrees, so your own checkout never sees a bisect in progress. GOOD defaults to HEAD, the commit the session started from. If you committed during the session, pass the right base with GOOD=<sha>.

The test command follows the git bisect run contract: exit 0 means good, 1 to 127 (except 125) means bad, and 125 means "can't test this one, skip it". Snapshots leave ignored files out, so the worktree has no node_modules. That is why the command installs first and turns a failed install into 125 instead of a false "bad".

Bisect halves the range each step. Twenty turns take about five test runs (log2 of 20 is about 4.3), and it prints the first bad snapshot. Its commit message carries the turn's UTC timestamp, so you can match it to the conversation.

Bisect assumes that once a turn breaks the test, every later turn is broken too. Agents don't always behave that way: turn 9 breaks it, turn 12 fixes it, turn 17 breaks it again. If the result looks odd, scan in order instead, inside the same kind of scratch worktree, with ref and good set as in the script:

for c in $(git rev-list --reverse "$ref" --not "$good") do git checkout -q --detach "$c" npm ci --silent >/dev/null 2>&1 || continue npm test >/dev/null 2>&1 || { echo "first bad: $c"; break; } done 

Once you have the bad snapshot, undo only that turn in your real working tree:

git diff --binary <bad>~1 <bad> | git apply -R 

If a later turn edited the same lines, git apply refuses and tells you which hunk, which is the conflict you want to see before anything is overwritten. When the session is merged and you no longer need its history, delete the refs:

git for-each-ref --format='delete %(refname)' \ refs/agent-turns/ | git update-ref --stdin 

If bisect is new to you, I walked through git bisect run end to end in Let Claude Code Run git bisect in Its Own Worktree While You Keep Coding.

Where Hermes IDE does the snapshot part for you

I build Hermes IDE, so weigh this section with that in mind. The hook above is Claude Code only. Other coding agents each have their own hook system, or none, so you would write and maintain a version per agent.

Hermes saves every agent turn, and you can diff or restore any single turn, with undo (README). Under each session, the Agent replies strip shows one chip per reply with the lines it changed. Click a chip to see its changes or restore your files to that point; a "+N earlier" chip opens every reply with its time, files and lines changed (changelog, 2.3.1). Turn history snapshots from tasks that ran in their own worktree are removed from the repository 14 days after the session closes (v2.2.9 release notes), so that history cleans itself up.

Hermes IDE Review Desk in By turn view for a Claude Code task, listing turns T1 and T2 with turn 1's diff of src/db.ts
The Review Desk (⌘G) in By turn view for a Claude Code task in orders-api: T1 (3 files, +69 −1) and T2 (1 file, +1 −0), turn 1's diff of src/db.ts adding a refunded status, the changed files, and a commit message drafted from the turns.

⌘G (Ctrl+Shift+G on Windows and Linux) opens the Review Desk, which shows changes by turn or by file with risk flags, and sends a line comment back to the agent that wrote the line. Hermes doesn't bisect for you: the README and recent release notes describe reading and restoring turns, not running a test across them. For "which of twenty turns broke this", the script above is still the tool. Hermes is where you read that turn and roll it back without touching the others.

Hermes is free to download, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It is not affiliated with Nous Research, Anthropic, OpenAI or any agent vendor. Download it, and if it saves you a bisect, star it on GitHub.

If this was useful

A chain of snapshots you replay until the state goes wrong is an append-only history, and that idea sits at the heart of event-driven systems too. The Event-Driven Architecture Pocket Guide covers that side at system scale: sagas, CQRS, the outbox, and the traps nobody warns you about. It is one of 8 short books in Pocket Guides for Developers, on agents, RAG, prompts, observability, events, databases and system design: the whole series is here.

Event-Driven Architecture Pocket Guide: Saga, CQRS, Outbox, and the Traps Nobody Warns You About