Skip to content

Snapshot.track() silently swallows git add failures, causing data loss on /undo #12719

Closed

Description

@Mikhail164th

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 named nul in 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

  1. 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'
  2. Continue working in OpenCode — make edits across multiple files
  3. Observe in logs: every service=snapshot entry 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
    
  4. Invoke /undo to revert to a previous message
  5. 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 failed

Impact

  • Data loss: All file edits in the session are lost when /undo is 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

  1. github-actions commented on Feb 8, 2026

    @github-actions
    Contributor

    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.

  2. github-actions commented on Apr 10, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 90 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions