Description
Description
A main-process write to a settings or window-state file can fail with EPERM on the rename, and the change is lost.
Field log, opencode 1.18.30 — kept as the only real trace I have for this:
[2026-09-26 16:26:47.473] [error] Uncaught (in promise) Error: Error invoking remote method 'store-set': Error: EPERM: operation not permitted, rename '...\opencode.workspace.QTpc5pel6K-t.1gjthy7.dat.tmp-0411206450bd66b2' -> '...\opencode.workspace.QTpc5pel6K-t.1gjthy7.dat'
V2 replaced that path with SQLite and dropped store-set, but the same write survived in two main-process files that still keep electron-store-compatible JSON — and this time with no retry at all:
packages/desktop/src/main/storage/store.ts—writeFileSynctofile.tmp, thenrenameover the targetpackages/desktop/src/main/windows/window-state.ts— the same, in the windowclosedhandler
In V1, atomically retried that rename for a second before throwing. V2 does not retry, so one transient lock loses the write.
The lock is any other open handle on the target, which is why a scanner is enough. Measured on Windows 11 with CreateFileW and an explicit dwShareMode:
holder dwShareMode |
rename over it | create-for-write |
|---|---|---|
| 0 (exclusive) | EPERM | EBUSY |
| 1 / 4 / 5 | EPERM | EPERM |
| 2 / 3 / 6 / 7 (grants FILE_SHARE_WRITE) | EPERM | ok |
So read+write sharing — the usual AV / indexer pattern — blocks the rename while still allowing the update to be written through the open handle.
These are the only two writers of this shape in the tree: renameSync appears in four files, and the other two are packages/cli/vite.node.config.ts and a TUI test. Not #45154 (a cleanup racing writes) and not #47497 (CLI session.json, closed).
Steps to reproduce
- Windows build, with something reading the file under
%APPDATA%\ai.opencode.desktop(indexer / AV / backup) - Any main-process settings write, or closing a window
Evidence boundary
The mechanism above is measured against the OS, not observed in a V2 build — I have no V2 field log for this path.
Plugins
none
OpenCode version
1.18.30 (desktop) for the log; current dev for the code paths
Operating System
Windows 11 Home, 10.0.22000
Terminal
Desktop app (Electron), not applicable
Activity
github-actions commentedon Sep 26, 2026
This issue might be related to existing issues. Please check:
EPERMerrors.EPERM, dropping persisted state.waterundman commentedon Oct 7, 2026
Keeping this one open — it's the issue tracked by PR #51474, which is still awaiting a maintainer to approve the workflow run.
Two notes for anyone arriving here from the duplicate-detection comment above.
Why this isn't a duplicate of #45154 / #47497:
session.json. This report is about the Desktop app's persisted renderer state (store-set/store-delete/store-clearover IPC).electron-storedirectly, so a transientrenamefailure (EPERM/EBUSY/EACCES) is thrown straight back into the renderer with no retry at all, and the write is silently dropped. The log line in the description is a plainstore-setfailing on the finalrename.They are complementary rather than overlapping: making the handlers retry (PR #51474) does not replace fixing the cleanup race in #45154, and vice versa. Linking the two threads here so that's visible.
Status of the fix: PR #51474 —
fix(desktop): retry store writes on Windows EPERM— addresses this report (retry with backoff, then an in-placewriteFileSyncfallback, so the user's change is never silently discarded). It is currentlyblockedpending a maintainer workflow approval, not on any code issue.[-]Windows: store-set can fail with EPERM on rename, silently dropping persisted state[/-][+]Windows: desktop settings and window-state writes can fail with EPERM on rename, silently dropping persisted state[/+]waterundman commentedon Oct 11, 2026
V2 port: #54530.
store-setno longer exists in V2 (renderer state went to SQLite), so this is aimed at the two main-process writers that still keep JSON with the same temporary-file rename -storage/store.tsandwindows/window-state.ts. Both now share one helper: retry the rename, then write through the open handle if the target is still locked, and remove the.tmpeither way.A Windows-only
bun:ffitest reproduces the failure against the pre-fix helper with the sameEPERM, and thedwShareModemeasurements are in the PR. CI isaction_requiredpending approval.finfanger commentedon Oct 15, 2026
Related to #51470, but with a different failure mode — the errno is wrong, and the failure is deterministic rather than a race.
What I'm seeing
OpenCode v2.0.29 fails to start at all on my machine, because the background service bootstrap can't persist its config:
Starting background server...
Error: Server process exited with code 1
PlatformError: Unknown: FileSystem.rename (C:\Users<user>.config\opencode\service.json.tmp)
at MA (B:/~BUN/root/chunk-2qa1wxmc.js:2:956)
...
[cause]: Error: EXDEV: cross-device link not permitted, rename
'C:\Users<user>.config\opencode\service.json.tmp'
-> 'C:\Users<user>.config\opencode\service.json')
at service.ensure (B:/~BUN/root/chunk-r5wq91nb.js:3:732)
at cli.server-connection.resolve
Note the path: source and destination are in the same directory, so
EXDEV("cross-device link not permitted") is semantically wrong. This is not a cross-volume move.Key difference from the existing reports: it's 100% reproducible
#51470 / #47497 / #45154 describe
EPERM/EBUSYcaused by a transient lock from a scanner or indexer. That is a race — retrying usually works.On this machine it never works. Minimal repro, no OpenCode involved:
Same directory, same volume, same session.
fs.renameSyncfails 100% of the time;mvon the identical pair succeeds. Once the destination file already exists, the error changes fromEXDEVtoEPERM— which suggests the syscall is being intercepted rather than the filesystem genuinely being cross-device.Things I ruled out:
OPENCODE_CONFIG_DIRpointed at unrelated directories — same failurefs.renameSyncwith no wrapper involved — same failurerenametargets across several different directories (including a freshly created one) — all failI have not identified the exact cause. I checked for third-party filesystem filters:
FltMgrreports 0 registered minifilters, and the only non-Windows kernel driver present is Sandboxie'sSbieDrv. So it may be something else entirely — possibly a kernel-mode filter I can't enumerate, or a low-level hook. I'm posting the observable behavior in case it's useful, since the signature (same-directory rename always failing, errno flipping between EXDEV/EPERM) is quite specific.Why this needs a different fix than #54530
A retry-with-backoff strategy won't help here — the operation is not transient, so every retry fails and the CLI never becomes usable. That leaves
--standaloneas the only working option, which is fine for me but means the shared background service is unavailable.Two suggestions that would make this class of environment usable:
EXDEVas "retry via copy+unlink" rather than a fatal error. That's the conventional POSIX meaning of EXDEV, and handling it that way would make this work transparently — from the caller's perspective a same-directory move is a case where copy+delete is a valid, if slower, substitute.Small doc discrepancy
The troubleshooting docs say the private service config lives at
~/.config/opencode/service.jsonand the shared server registers at~/.local/state/opencode/service.json. On v2.0.29 the failing temp file is~/.config/opencode/service.json.tmp, so the write path is under.configas documented — but I never got a successfulservice.jsonto appear in either location, so I couldn't verify the final layout.Environment
@opencode/cli)opencode --standalone(andopencode run --standalone)opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up and the share-mode measurements. I couldn't reproduce this here. The failure needs Windows file locking and the desktop app, and this sandbox is Linux, where renaming over an open file just succeeds. A maintainer with a Windows machine will need to confirm it.
On the latest
dev(86ce19d), both writers you point to still write tofile.tmpand thenrenameSyncit over the target, with no retry:createSettingsStore().write()inpackages/desktop/src/main/storage/store.tsclosedhandler inpackages/desktop/src/main/windows/window-state.tsIn the settings store, the in-memory value is updated before the write. So a failed rename would make the call throw and leave the old contents on disk, as you describe.
Related:
EPERMEPERM-on-rename pattern, but for the CLI'ssession.jsonIf you get a V2 desktop log showing this error, please post it here.
fix(tui): retry a state write that loses its rename to a Windows lock
fix(tui): retry a state write that loses its rename to a Windows lock