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
bug: opencode upgrade fails silently on Windows - binary never replaced #37055
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
Install opencode on Windows via the curl method (.opencode\bin\opencode.exe)
Run opencode upgrade
Observe it reports 'Upgrade complete'
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:
Downloads the zip directly via curl.exe
Extracts to a temp directory
Renames current binary to .old
Spawns a background batch script that waits for the process to exit, then swaps the binaries
This approach is already implemented in PR #37051.
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?
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.
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.
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:
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
:343mv
replaced, exit 0
--binary
:354cp
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.
Activity
cikeyz commentedon Sep 7, 2026
Still reproducing this on 2026-09-08.
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?
loisekk commentedon Sep 10, 2026
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
mvfails 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
loisekk commentedon Oct 10, 2026
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.tskeeps a second link to the running image for the duration of the upgrade, so the installer is free to replaceopencode.exe.updater.tswraps the curl install inretaining()when the binary is at~/.opencode/bin/.Minimal repro I ran, with the binary genuinely executing:
@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 pipefailand itsmvis unguarded, andrunUpgradefails 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.zipfrom the release and dropopencode.exeover 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:200and:326gate onpath.resolve(process.execPath) === curlBinary, a case-sensitive string compare on paths Windows treats case-insensitively. Ifprocess.execPathandos.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.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 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊
loisekk commentedon Oct 11, 2026
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.tsand 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:install.sh:343usesmv, 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:That probe does fail, but only because native Win32 callers don't request
FILE_SHARE_DELETE. Cygwin/MSYS does, somvis permitted. The probe measures the wrong caller.The probe isn't wrong about writing though, and that's where the real bug is:
So the two install paths differ:
:343mv--binary:354cpThat is #51755 exactly:
cpmust open the destination for writing, which a running image refuses, so the documented PowerShell/--binaryroute 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 youunlinka running image, which is real. But the curl installer never callsunlinkon the destination, soretaining()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
bashresolves to the WSL stub the installer never runs against the Windows filesystem at all — plus thecppath above. This issue being closed as V1-only looks right.