Repository navigation
Description
Description
Summary
The session snapshot subsystem pins a single project-level baseline tree (a git TREE object id) on first creation and never rotates it. Every later session's step.started/revert.snapshot reuses the same tree id. A restore/revert then runs a whole-tree checkout (git read-tree + git checkout-index --all --force) with no diff-size guard, reverting the entire worktree to the state of that stale baseline — bulk-overwriting tracked files with old content and resurrecting deleted files. Bookkeeping lives in a hidden shadow repo (~/.local/share/opencode/snapshot///), so the real repo's HEAD / reflog / index stay untouched, making the event look like a silent "workspace rollback".
Evidence (DB, read-only)
All from ~/.local/share/opencode/opencode.db (SQLite, 867MB) + the shadow git repo ~/.local/share/opencode/snapshot/e242e0621b65be4ad71f837f17c59b8baae12a77//.
Across ALL step-start/step-finish part JSON in 25 days, the snapshot field contains exactly 2 distinct TREE ids:
ff71af… (first seen 2026-08-21 21:12, last seen 2026-08-22 18:07)
9660d465… (first seen 2026-08-24 22:11, last seen 2026-09-18 17:03 — 100% of snapshots for 25 days)
One session had a non-empty revert record: session.revert = { messageID: "msg_…", snapshot: "9660d465…", diff: "" } — empty diff = whole-tree rollback, no file-level plan.
The shadow repo index (written today) git diff --cached <9660d465 tree> = empty → index tree == the 8-24 baseline. git diff --cached = 334 files touched / 68,439 deletions → index is the old slim worktree, not current state.
The shadow repo: worktree = /Users/…/codex-workplace (points at the live worktree), objects/info/alternates → real repo .git/objects, HEAD unborn master, empty refs, no reflog. git status --short shows the whole tree staged (A/AM).
Timeline: shadow repo created 08-17; lost-found/ created 08-24 13:20 (= first incident day; fsck ran); baseline switched from ff71af to 9660d465 at 08-24 22:11 (shadow repo re-created via git.repo.create which clones the main repo's index into the shadow index). Incidents observed ~08-24, 08-26, 09-11, 09-18 all restored files to the 08-24 snapshot state. Real repo git log/reflog show no reset/restore — HEAD intact, deleted files resurrected as untracked.
Evidence (source, engine bundle in Desktop app.asar)
Snapshot.capture → Git.tree.capture → per-scope Git.index.refresh (a real git add --all --sparse --pathspec-from-file=- + git rm --cached for ignored/oversized) then write-tree. So capture() is supposed to produce a fresh tree reflecting the current worktree each call.
Defect 1 — baseline never recaptured. SessionRevert.stage: original = session.revert?.snapshot ? reuseExistingTree : snapshot.capture(). Once a revert.snapshot is stored, every later stage/restore reuses the OLD tree; capture is short-circuited forever.
Defect 2 — no guard on whole-tree restore. The restore/checkout path ends in git read-tree + checkout-index --all --force (unconditional full-tree overwrite). No diff-size / file-count check.
Defect 3 — content-addressing self-stabilizing loop. Tree ids are content-addressed. After a restore writes the 8-24 content back into the worktree, the next capture produces the same tree id (identical content ⇒ identical SHA), so the recorded baseline never visibly changes — masking the bug for weeks.
Workaround for users: set "snapshots": false in opencode.json (this is the exact config key checked by the engine: config.latest(entries, "snapshots") !== false).
Suggested fix
Never reuse a stored revert baseline for a new session/step — always call capture() at session/step start (or at minimum cap the reuse to the same session).
Add a guard before any whole-tree checkout-index --all --force: compute the diff size between target tree and current worktree and abort (with a clear error) when it exceeds a sane threshold (e.g. >500 files or >50k lines), requiring explicit confirmation.
Derive the session-revert target from the latest captured tree, and prefer per-file git.tree.restore over whole-tree checkout for session-level reverts.
Consider validating that target tree is reachable/plausible relative to current state before writing.
Versions: Desktop App 1.18.21 / engine bundle app.asar node-CMcqrhBZ.js; TUI 1.18.30.
Plugins
No response
OpenCode version
1.18.21 (1.18.21)
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response
Activity
Dante-dan commentedon Sep 18, 2026
Thanks for the detailed trace. I’m following the capture and restore paths now. I’ll keep the original baseline only while expanding the same staged undo, make new step captures reflect the current worktree, and add a bounded preflight before any whole-tree restore. I’ll verify consecutive captures plus multi-step undo/unrevert behavior before opening a fix.
lasertiger commentedon Oct 6, 2026
See #53560 as well
thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊