Skip to content

bug: opencode upgrade fails silently on Windows - binary never replaced #37055

Closed as not planned

Description

@loisekk

Bug Description

opencode upgrade reports success on Windows but the binary is never actually replaced. The version remains the same after running the upgrade command.

Root Cause

The upgradeCurl method runs a bash install script from https://opencode.ai/install. This script downloads the new binary and attempts to mv it over the running executable. On Windows, running executables are locked by the OS, so the mv fails silently. The script does not properly detect this failure and reports success.

Steps to Reproduce

  1. Install opencode on Windows via the curl method (.opencode\bin\opencode.exe)
  2. Run opencode upgrade
  3. Observe it reports 'Upgrade complete'
  4. Run opencode --version - still shows the old version

Expected Behavior

The upgrade should either:

  • Replace the binary (requires scheduling replacement after process exit on Windows)
  • Report failure with a clear error message

Environment

  • OS: Windows 10/11
  • Install method: curl (.opencode\bin\opencode.exe)
  • opencode version: 1.17.0 (attempted upgrade to 1.18.1)

Suggested Fix

Add a Windows-specific upgrade path that:

  1. Downloads the zip directly via curl.exe
  2. Extracts to a temp directory
  3. Renames current binary to .old
  4. Spawns a background batch script that waits for the process to exit, then swaps the binaries

This approach is already implemented in PR #37051.

Activity

  1. cikeyz commented on Sep 7, 2026

    @cikeyz

    Still reproducing this on 2026-09-08.

    • Windows 11, PowerShell 7.6.5, curl install at C:\Users\Carl.opencode\bin\opencode.exe
    • \opencode upgrade\ reported 1.18.23 to 1.18.29 plus Upgrade complete, but the exe was untouched (LastWriteTime 25 Aug 2026) and \opencode --version\ still 1.18.23
    • Manual replace from opencode-windows-x64.zip worked, now on 1.18.29

    Noting PR #37051 (the scheduled-replacement fix) is closed unmerged, so nothing has landed. At minimum, upgrade should error instead of printing Upgrade complete when the replace fails. Can this be re-targeted?

  2. loisekk commented on Sep 10, 2026

    @loisekk
    Author

    Thanks for confirming this is still reproducing on 1.18.29, @cikeyz. My original PR #37051 was closed unmerged (it didn't follow the PR template correctly and wasn't linked to this issue). I'm re-submitting the fix now with the proper template and linking it here via Closes #37055.

    The core approach is the same on Windows, the bash install script's mv fails silently because the binary is locked. The fix adds a Windows-specific path that schedules the binary replacement after process exit using a background batch script with a retry loop.

    New PR: #48368

  3. loisekk commented on Oct 10, 2026

    @loisekk
    Author

    Closing the loop on this one. My earlier fix (PR #37051, then #48368) got closed because the branch moved to V2 and V1 PRs aren't being taken anymore — that part's on me for submitting to the wrong place.

    This bug is already fixed on V2, and not with the approach I originally proposed. I verified the mechanism on Windows 11 rather than assuming it works:

    The cause is that Windows refuses to delete the last hard link to a running executable. packages/cli/src/services/retained-image.ts keeps a second link to the running image for the duration of the upgrade, so the installer is free to replace opencode.exe. updater.ts wraps the curl install in retaining() when the binary is at ~/.opencode/bin/.

    Minimal repro I ran, with the binary genuinely executing:

    [1] replace running binary with NO second link:
      unlink target.exe          -> FAIL (EPERM)
      open target.exe for write  -> FAIL (EBUSY)
    [2] same, with a retained hard link:
      after link: ino=21673573209341212 nlink=2 sameInode=true
      unlink target.exe          -> OK
    

    @cikeyz — you asked that at minimum it error instead of printing "Upgrade complete" when the replace fails. That's also handled on V2: the installer runs under set -euo pipefail and its mv is unguarded, and runUpgrade fails the upgrade on a non-zero exit. So you get an actual error rather than a false success.

    So: this issue only affects V1 installs. Nothing to fix here on V2, and I don't think it warrants a backport PR given V1 is frozen. Leaving it open for anyone still on V1 to find.

    For anyone stuck on V1 right now — the manual path that works is to download opencode-windows-x64.zip from the release and drop opencode.exe over the installed one from a separate terminal. That's the process you're already using.

    One small thing I noticed while reading, not worth its own PR but worth recording: updater.ts:200 and :326 gate on path.resolve(process.execPath) === curlBinary, a case-sensitive string compare on paths Windows treats case-insensitively. If process.execPath and os.homedir() disagree on casing, retaining() no-ops and the upgrade fails loudly instead of succeeding. Narrow, and it fails loudly rather than silently, so I'm not chasing it.

  4. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    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 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

  5. loisekk commented on Oct 11, 2026

    @loisekk
    Author

    Correction — my previous comment on this issue was wrong on both counts, and I'd rather retract it clearly than leave it standing.

    Wrong #1: "This bug is already fixed on V2." It isn't closed. #51386, #51755 and #52025 are all open against V2 right now. I read retained-image.ts and its test, saw the hard link, and concluded the bug was handled without ever running the actual installer. That was sloppy reasoning and I should have run it first.

    Wrong #2: I presented a Node-level file-lock result as if it were the installer's behaviour. It isn't. Here's the corrected picture, all measured on Windows 11 with Git Bash.

    The real installer, run against a sandboxed HOME, with the install-dir binary genuinely executing:

    running install-dir binary: pid=1185 nlink=1
    hash before: e4224d18c3c96826
    $ HOME=$SANDBOX bash /tmp/oc-install.sh --version 2.0.19 --no-modify-path
    installer exit status : 0
    hash after            : 100c90202ec358bd
    VERDICT: BINARY WAS REPLACED
    

    install.sh:343 uses mv, and under MINGW64 that succeeds over a running executable. So #52025's premise — "the replacement would still hit the lock" — doesn't hold for the curl path with a real Git Bash. That report infers failure from a .NET probe:

    [System.IO.File]::Open('...opencode.exe','Open','ReadWrite','None')
    

    That probe does fail, but only because native Win32 callers don't request FILE_SHARE_DELETE. Cygwin/MSYS does, so mv is permitted. The probe measures the wrong caller.

    The probe isn't wrong about writing though, and that's where the real bug is:

    $ bash install.sh --binary /tmp/payload.exe
    cp: cannot create regular file '.../opencode.exe': Device or resource busy
    exit=1   NOT REPLACED
    

    So the two install paths differ:

    path line over a running exe
    normal download :343 mv replaced, exit 0
    --binary :354 cp fails, "Device or resource busy"

    That is #51755 exactly: cp must open the destination for writing, which a running image refuses, so the documented PowerShell/--binary route can never succeed while OpenCode is running. The rename-to-.old-then-copy workaround in that report is correct, and it's the only thing that can work on that path.

    And on retained-image.ts: the hard link does let you unlink a running image, which is real. But the curl installer never calls unlink on the destination, so retaining() isn't what makes that path work. It looks aimed at the bun/package-manager replacement path instead.

    Net: I withdraw the claim that this is fixed. What's actually still broken on Windows is shell selection — when bash resolves to the WSL stub the installer never runs against the Windows filesystem at all — plus the cp path above. This issue being closed as V1-only looks right.

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