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
cli: Windows curl upgrade cannot replace the running opencode.exe (locked by the TUI and background service) #52025
On Windows, the V2 curl upgrade path must replace %USERPROFILE%\.opencode\bin\opencode.exe, but that file is the running executable and is write-locked by every live opencode process. Even with a correct MINGW/Git Bash (i.e. after the shell-selection fix in #51038), the install script's final mv cannot overwrite it, so the upgrade still fails unless the user stops every opencode process first.
While the TUI and a long-lived background service were running, a direct write-lock probe on the installed binary failed:
> [System.IO.File]::Open("C:\Users\<user>\.opencode\bin\opencode.exe",'Open','ReadWrite','None')
The process cannot access the file '...\.opencode\bin\opencode.exe' because it is being used by another process.
Three opencode.exe processes were running from that same path: the TUI, a second TUI, and a background service process started days earlier. Windows allows renaming a running executable but not overwriting or deleting it, so a coreutils mv that replaces the destination fails.
This is independent of the shell-choice bug: C:\Program Files\Git\bin\bash.exe runs the downloaded installer successfully (uname -s = MINGW64_NT-10.0-26200, uname -m = x86_64), so with #51038 the spawn would succeed and the replacement would still hit the lock.
Related: #51755 (V2 in-place upgrade blocked by the locked executable) and #37055 (V1, same class, scheduled-replace fix proposed in PR #37051). Filed separately to record the direct lock evidence and the fact that it persists after the shell-selection fix.
Workaround that worked: stop every opencode process (TUI plus background service), then run the installer from Git Bash. Because Windows permits renaming the running binary, the updater could adopt the same rename-then-swap-after-exit strategy to update in place.
Plugins
None found in global or project config.
OpenCode version
2.0.16, channel latest (attempting to upgrade to 2.0.19).
Steps to reproduce
On Windows, install OpenCode V2 with the curl installer so the binary is %USERPROFILE%\.opencode\bin\opencode.exe.
Start the TUI. A separate background service opencode.exe process also stays resident and runs from the same path.
Attempt to replace the binary the way the install script does: mv tmp/package/bin/opencode.exe "$INSTALL_DIR/opencode.exe".
The replacement fails; the running version is unchanged.
Screenshot and/or share link
No share link. The write-lock probe output is in Description.
Operating System
Windows 11 Pro build 26200 (Windows_NT 10.0.26200, x64).
Terminal
cmd.exe / PowerShell 7 (launched from C:\WINDOWS\system32\cmd.exe).
Went looking for the mechanism behind this and think the premise needs re-testing, because mv may not hit the lock you expect.
Your probe is accurate for a native Win32 caller:
[System.IO.File]::Open('...opencode.exe','Open','ReadWrite','None')
→ being used by another process
But the installer's final step isn't a Win32 open. install.sh:343 is mv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR/$installed_binary", and under MINGW64 coreutils mv that succeeds against a running executable. Ran the real installer against a sandboxed HOME with the install-dir binary live:
running install-dir binary: pid=1185 nlink=1
hash before: e4224d18c3c96826
$ HOME=$SANDBOX bash install.sh --version 2.0.19 --no-modify-path
installer exit status : 0
hash after : 100c90202ec358bd
VERDICT: BINARY WAS REPLACED
Same-fs and cross-fs both replace, verified by hash change rather than exit code:
The reason your probe and the installer disagree is sharing mode, not locking. Native Win32 opens don't request FILE_SHARE_DELETE, so the probe is refused; Cygwin/MSYS does request it, so mv is allowed. Both results are correct for their own caller — the probe measures .NET, the failure you predicted comes from mv, which isn't .NET.
The claim I'd push back on is "Windows allows renaming a running executable but not overwriting or deleting it, so a coreutils mv that replaces the destination fails." Renaming a running exe does work, and mv is a rename — MSYS2 gets to rename because of FILE_SHARE_DELETE, so the lock you're reasoning about doesn't apply to it.
Your rename-then-swap suggestion works, and it's the right shape for the path that genuinely is broken — but for mv it isn't needed.
Where a real failure does happen is the cp path, install.sh:354, used by --binary and effectively by anyone following the PowerShell route in #51755:
cp must open the destination for writing, which is refused even with FILE_SHARE_DELETE on the caller. That needs rename-aside, and it explains #51755's .old workaround exactly.
Also relevant to the retained-image.ts question: the hard link it creates does allow unlink of a running image (I measured EPERM at nlink=1, OK at nlink=2), but the curl installer never unlinks the destination, so retaining() isn't what rescues that path.
Two caveats on all of this. I used ping.exe as the stand-in running binary rather than opencode.exe itself — same Windows semantics, but it's not the same image. And it was a single process; I didn't try to reproduce your multi-process TUI-plus-service setup, so if the number of concurrent holders is what tips it over, my run wouldn't have caught that.
If you still see it fail with a real Git Bash bash.exe, the installer's stderr would settle it — opencode upgrade currently discards it, and --print-logs --log-level DEBUG didn't surface it for me either. Surfacing installer stderr on failure looks like the more useful fix than the locking theory.
Thanks for the detailed report and the lock-probe output. I think this is a duplicate of #51755, which reports the same problem on v2: the Windows in-place upgrade can't replace the running %USERPROFILE%\.opencode\bin\opencode.exe. It also uses the same rename-aside workaround.
Possible duplicates and related issues, most likely first:
cli: v2 Windows PowerShell install cannot be upgraded in place #51755: v2 in-place upgrade on Windows blocked by the running opencode.exe. Same problem and same rename-then-replace workaround. A commenter there argues that the installer's mv does succeed under Git Bash because MSYS opens files with FILE_SHARE_DELETE, unlike the PowerShell File.Open(..., 'None') probe. Your evidence that it still fails with Git Bash and several live processes would help that discussion.
It would help to add your probe results and a note on whether the real installer mv failed under Git Bash (with its exact error) to #51755. The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
We haven't heard back in the last day, so I'm closing this as a duplicate of #51755. That issue tracks the same v2 problem: the Windows in-place upgrade can't replace the running %USERPROFILE%\.opencode\bin\opencode.exe.
Your write-lock probe, and the note that the lock remains after the #51038 shell-selection fix, would be useful on #51755. There is a discussion there about whether the installer's mv actually fails under Git Bash. If your problem turns out to be different, reply here and we can reopen it. Thanks for the detailed report!
Activity
github-actions commentedon Sep 29, 2026
Thanks for updating the issue with the required details and reproduction steps!
loisekk commentedon Oct 11, 2026
Went looking for the mechanism behind this and think the premise needs re-testing, because
mvmay not hit the lock you expect.Your probe is accurate for a native Win32 caller:
But the installer's final step isn't a Win32 open.
install.sh:343ismv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR/$installed_binary", and under MINGW64 coreutilsmvthat succeeds against a running executable. Ran the real installer against a sandboxedHOMEwith the install-dir binary live:Same-fs and cross-fs both replace, verified by hash change rather than exit code:
The reason your probe and the installer disagree is sharing mode, not locking. Native Win32 opens don't request
FILE_SHARE_DELETE, so the probe is refused; Cygwin/MSYS does request it, somvis allowed. Both results are correct for their own caller — the probe measures .NET, the failure you predicted comes frommv, which isn't .NET.The claim I'd push back on is "Windows allows renaming a running executable but not overwriting or deleting it, so a coreutils
mvthat replaces the destination fails." Renaming a running exe does work, andmvis a rename — MSYS2 gets to rename because ofFILE_SHARE_DELETE, so the lock you're reasoning about doesn't apply to it.Your rename-then-swap suggestion works, and it's the right shape for the path that genuinely is broken — but for
mvit isn't needed.Where a real failure does happen is the
cppath,install.sh:354, used by--binaryand effectively by anyone following the PowerShell route in #51755:cpmust open the destination for writing, which is refused even withFILE_SHARE_DELETEon the caller. That needs rename-aside, and it explains #51755's.oldworkaround exactly.Also relevant to the
retained-image.tsquestion: the hard link it creates does allowunlinkof a running image (I measuredEPERMat nlink=1,OKat nlink=2), but the curl installer neverunlinks the destination, soretaining()isn't what rescues that path.Two caveats on all of this. I used
ping.exeas the stand-in running binary rather thanopencode.exeitself — same Windows semantics, but it's not the same image. And it was a single process; I didn't try to reproduce your multi-process TUI-plus-service setup, so if the number of concurrent holders is what tips it over, my run wouldn't have caught that.If you still see it fail with a real Git Bash
bash.exe, the installer's stderr would settle it —opencode upgradecurrently discards it, and--print-logs --log-level DEBUGdidn't surface it for me either. Surfacing installer stderr on failure looks like the more useful fix than the locking theory.opencodejr commentedon Oct 28, 2026
Thanks for the detailed report and the lock-probe output. I think this is a duplicate of #51755, which reports the same problem on v2: the Windows in-place upgrade can't replace the running
%USERPROFILE%\.opencode\bin\opencode.exe. It also uses the same rename-aside workaround.Possible duplicates and related issues, most likely first:
opencode.exe. Same problem and same rename-then-replace workaround. A commenter there argues that the installer'smvdoes succeed under Git Bash because MSYS opens files withFILE_SHARE_DELETE, unlike the PowerShellFile.Open(..., 'None')probe. Your evidence that it still fails with Git Bash and several live processes would help that discussion.opencode upgradefails silently on Windows - binary never replaced #37055: the same kind of failure on v1 (closed).It would help to add your probe results and a note on whether the real installer
mvfailed under Git Bash (with its exact error) to #51755. The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.opencodejr commentedon Oct 29, 2026
We haven't heard back in the last day, so I'm closing this as a duplicate of #51755. That issue tracks the same v2 problem: the Windows in-place upgrade can't replace the running
%USERPROFILE%\.opencode\bin\opencode.exe.Your write-lock probe, and the note that the lock remains after the #51038 shell-selection fix, would be useful on #51755. There is a discussion there about whether the installer's
mvactually fails under Git Bash. If your problem turns out to be different, reply here and we can reopen it. Thanks for the detailed report!