Windows: desktop settings and window-state writes can fail with EPERM on rename, silently dropping persisted state · Issue #51470 · anomalyco/opencode · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Windows: desktop settings and window-state writes can fail with EPERM on rename, silently dropping persisted state #51470
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 — writeFileSync to file.tmp, then rename over the target
packages/desktop/src/main/windows/window-state.ts — the same, in the window closed handler
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
Desktop scoped-store cleanup can race later writes on Windows #45154 (open) is adjacent but a different mechanism: it describes a scoped-store cleanup racing later writes. The failure reported here is one layer up — the three IPC handlers call electron-store directly, so a transient rename failure (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 plain store-set failing on the final rename.
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-place writeFileSync fallback, so the user's change is never silently discarded). It is currently blocked pending a maintainer workflow approval, not on any code issue.
changed the title [-]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[/+]on Oct 11, 2026
store-set no 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.ts and windows/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 .tmp either way.
A Windows-only bun:ffi test reproduces the failure against the pre-fix helper with the same EPERM, and the dwShareMode measurements are in the PR. CI is action_required pending approval.
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/EBUSY caused 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:
const d = "C:/Users/<user>/ocdata/";
fs.writeFileSync(d + "b1.tmp", "x");
fs.renameSync(d + "b1.tmp", d + "b1.json");
// Node: EXDEV: cross-device link not permitted
// shell mv: succeeds
Same directory, same volume, same session. fs.renameSync fails 100% of the time; mv on the identical pair succeeds. Once the destination file already exists, the error changes from EXDEV to EPERM — which suggests the syscall is being intercepted rather than the filesystem genuinely being cross-device.
Things I ruled out:
OPENCODE_CONFIG_DIR pointed at unrelated directories — same failure
Node's own fs.renameSync with no wrapper involved — same failure
rename targets across several different directories (including a freshly created one) — all fail
I have not identified the exact cause. I checked for third-party filesystem filters: FltMgr reports 0 registered minifilters, and the only non-Windows kernel driver present is Sandboxie's SbieDrv. 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.
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 --standalone as 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:
Don't fail hard when the service config can't be persisted. Fall back to in-process/standalone operation instead of exiting, so the CLI stays usable.
Treat EXDEV as "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.json and 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 .config as documented — but I never got a successful service.json to appear in either location, so I couldn't verify the final layout.
Environment
opencode v2.0.29 (installed via npm, @opencode/cli)
Windows 10 Pro, 22H2, 10.0.19045.6466
Terminal: Windows Terminal / Windows PowerShell 5.1
Workaround that works today: opencode --standalone (and opencode run --standalone)
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)