Repository navigation
Snapshot.track() silently swallows git add failures, causing data loss on /undo #12719
Description
Summary
Snapshot.track() in packages/opencode/src/snapshot/index.ts uses .nothrow() on git add ., which silently swallows fatal errors. When git add . fails, write-tree returns a stale tree hash. All subsequent snapshots in the session reference this same stale hash. When the user invokes /undo, files revert to the stale state — losing all work done in the session.
Root Cause
// packages/opencode/src/snapshot/index.ts — track()
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`
.quiet()
.cwd(Instance.directory)
.nothrow() // ← swallows fatal errors silently
const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree`
.quiet()
.cwd(Instance.directory)
.nothrow() // ← returns stale tree when add failed
.text()When git add . encounters a fatal error (e.g. a file that cannot be indexed), .nothrow() suppresses the non-zero exit code. The subsequent write-tree then operates on the old index and returns a stale tree hash. Since track() returns this hash without validation, every snapshot in the session points to the same stale state.
Reproduction
Trigger condition
Any file in the worktree that causes git add . to fail fatally. Confirmed example:
- Windows reserved device name (
nul,con,aux,prn, etc.) — a file namednulin the project root causes:error: short read while indexing nul fatal: adding files failed
This can happen when a shell command accidentally creates such a file (e.g. redirecting output to nul on Windows via MSYS2/Git Bash).
Steps to reproduce
- Create a file that breaks
git add .in any project tracked by OpenCode:# On Windows (Git Bash / MSYS2): echo "test" > nul # Creates a file named 'nul'
- Continue working in OpenCode — make edits across multiple files
- Observe in logs: every
service=snapshotentry shows the same hash:INFO service=snapshot hash=e68154af tracking INFO service=snapshot hash=e68154af tracking # same hash, different step INFO service=snapshot hash=e68154af tracking # still the same - Invoke
/undoto revert to a previous message - Result: Files revert to the state captured in the stale snapshot (before any edits in the session), not to the expected message boundary
Evidence from production logs
2026-02-06T17:52:56 +1690ms INFO service=snapshot hash=e68154af3b63404727d9681148292156dd537269 tracking
# ... (all snapshots in session have identical hash)
2026-02-06T17:52:56 +1690ms INFO service=snapshot file=...\browser_use\config.py hash=e68154af reverting
2026-02-06T17:52:56 +1690ms INFO service=snapshot file=...\browser_use\agent\views.py hash=e68154af reverting
# 5 files reverted to stale state, losing ~2 hours of work
Verification that git add . was failing silently:
$ git --git-dir <snapshot-repo> --work-tree <project> add --dry-run .
error: short read while indexing nul
fatal: adding files failedImpact
- Data loss: All file edits in the session are lost when
/undois invoked - Silent failure: No error is logged, no warning shown to user —
track()returns a hash that looks valid - Session-wide: Once the snapshot becomes stale, it stays stale for the entire session (every
track()call returns the same hash) - Not limited to Windows reserved names: Any condition that causes
git add .to fail fatally will trigger this bug (corrupt files, permission issues, filesystem errors, etc.)
Suggested Fix
Check the exit code of git add . before proceeding to write-tree:
export async function track() {
if (Instance.project.vcs !== "git") return
const cfg = await Config.get()
if (cfg.snapshot === false) return
const git = gitdir()
if (await fs.mkdir(git, { recursive: true })) {
await $`git init`
.env({ ...process.env, GIT_DIR: git, GIT_WORK_TREE: Instance.worktree })
.quiet()
.nothrow()
await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow()
log.info("initialized")
}
// Check exit code instead of using .nothrow()
const addResult = await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`
.quiet()
.cwd(Instance.directory)
.nothrow()
if (addResult.exitCode !== 0) {
log.warn("git add failed, snapshot may be stale", {
exitCode: addResult.exitCode,
stderr: addResult.stderr.toString().slice(0, 500),
})
// Return undefined to signal callers that snapshot is unreliable
return undefined
}
const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree`
.quiet()
.cwd(Instance.directory)
.nothrow()
.text()
log.info("tracking", { hash: hash.trim(), cwd: Instance.directory, git })
return hash.trim()
}The same pattern should be applied to patch() and diff() which also use .nothrow() on git add ..
Callers of track() already handle undefined return (the function signature is Promise<string | undefined>), so this change is backwards-compatible.
Environment
- OpenCode v1.1.53 (npm
opencode-ai) - Windows 11 x64, Git Bash (MSYS2)
- Git 2.47.1.windows.2
Activity
github-actions commentedon Feb 8, 2026
This issue is a duplicate of #10589, which describes the identical bug: Snapshot.track() uses .nothrow() on git add, causing silent failures that lead to stale snapshot hashes and data loss during /undo and /redo operations.
Both issues reference:
Please check #10589 for additional context and implementation notes. The two issues can be consolidated.
github-actions commentedon Apr 10, 2026
To stay organized issues are automatically closed after 90 days of no activity. If the issue is still relevant please open a new one.