Windows: desktop settings and window-state writes can fail with EPERM on rename, silently dropping persisted state · Issue #51470 · anomalyco/opencode · GitHub
Skip to content

Windows: desktop settings and window-state writes can fail with EPERM on rename, silently dropping persisted state #51470

Open

Description

@waterundman

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 — 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

  1. Windows build, with something reading the file under %APPDATA%\ai.opencode.desktop (indexer / AV / backup)
  2. 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

  1. github-actions commented on Sep 26, 2026

    @github-actions
    Contributor

    This issue might be related to existing issues. Please check:

  2. waterundman commented on Oct 7, 2026

    @waterundman
    Author

    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:

    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.

  3. 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
  4. waterundman commented on Oct 11, 2026

    @waterundman
    Author

    V2 port: #54530.

    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.

  5. finfanger commented on Oct 15, 2026

    @finfanger

    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.

    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 --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:

    1. 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.
    2. 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions