- Book: Event-Driven Architecture Pocket Guide
- The series: Pocket Guides for Developers — 8 short books on agents, RAG, prompts, observability, events, databases and system design — the whole series
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
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
-uoption that pulls in untracked files belongs topushandsave, 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-treeturns that tree into a commit whose parent (-p) is the previous snapshot. The first snapshot of a session hangs offHEAD. That chain is what makes bisect possible. -
git update-refmovesrefs/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 0on 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_DIRstill points at the original project root whilecwdfollows the worktree. That is why the scriptcds tocwd.
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.

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.

Top comments (0)