Skip to content

cli: v2 Windows PowerShell install cannot be upgraded in place #51755

Closed

Description

@alexeypolitov

Description

OpenCode V2 installed at %USERPROFILE%\.opencode\bin\opencode.exe cannot be upgraded from native Windows PowerShell. The documented installer is bash-only, and Windows package managers are not supported. The working update is a manual replace from opencode-windows-x64.zip.

bash.exe on this machine is the WSL stub. The only WSL distro is stopped docker-desktop, so curl -fsSL https://opencode.ai/v2/install | bash is not a native Windows path. The V2 installer only treats MINGW / MSYS / CYGWIN as Windows, then mvs the new binary over the running exe. Windows locks that file, so the replace cannot succeed while OpenCode is running.

This is V2 (https://opencode.ai/v2/install, 2.0.15 to 2.0.18, refs/heads/v2). The open V1 report is the same class of bug, and its fix is still unmerged: #37055, PR #48368.

Workaround that worked: download https://opencode.ai/files/bin/2.0.18/opencode-windows-x64.zip, rename the running exe to opencode.exe.old, copy the new opencode.exe into %USERPROFILE%\.opencode\bin\, then restart OpenCode. A new process then reports opencode v2.0.18.

The V2 docs download links still point at the 2.0.6 zips, not 2.0.18.

Plugins

None

OpenCode version

2.0.15, channel latest. Latest published build is 2.0.18.

Steps to reproduce

  1. On native Windows PowerShell, with no Linux WSL distro, install V2 so the binary is %USERPROFILE%\.opencode\bin\opencode.exe.
  2. Run opencode --version. It prints 2.0.15.
  3. Run opencode upgrade, or curl -fsSL https://opencode.ai/v2/install | bash from PowerShell.
  4. The installer is bash-only. PowerShell bash is the WSL stub, and the script mvs onto the locked exe.
  5. Run opencode --version again. It stays on the old build unless the exe is replaced by hand.

Screenshot and/or share link

No share link. Confirmed latest via https://opencode.ai/update/api/latest/cli/npm (version 2.0.18) and by running the extracted zip binary.

Operating System

Windows 11 (10.0.26200, win32 x64). Shell is Windows PowerShell 5.1. Not WSL.

Terminal

Unavailable. Terminal environment variables are not set.

Activity

  1. github-actions commented on Sep 28, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. loisekk commented on Oct 11, 2026

    @loisekk

    Your .old workaround is the correct fix, and the reason it works pins down which part is actually broken.

    There are two different operations in the installer and only one of them fails against a running executable:

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

    Measured with Git Bash coreutils 8.32 on Windows 11:

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

    cp has to open the destination for writing, and a running image refuses that. mv doesn't — it's a rename, and Cygwin/MSYS opens with FILE_SHARE_DELETE so the rename is permitted. Same binary, same lock, opposite outcome, purely because of how each opens the file.

    Which is why rename-to-.old-then-copy works for you: the rename-aside is the only part that a running image allows, and once the name is vacated the cp has nothing to overwrite. Doing rename-aside first is the fix, not just the workaround.

    Against the normal download path, though, the lock isn't the obstacle at all:

    $ HOME=$SANDBOX bash install.sh --version 2.0.19 --no-modify-path
    installer exit status : 0
    hash before: e4224d18c3c96826
    hash after : 100c90202ec358bd   → REPLACED
    

    With the binary running in the install dir the whole time. Same-fs and cross-filesystem both replace it.

    So for your setup the blocker is the one you identified — bash.exe is the WSL stub and there's no usable distro — not the mv. curl -fsSL https://opencode.ai/v2/install | bash from PowerShell has no native Windows path, because install.sh only treats MINGW/MSYS/CYGWIN as Windows and mvs onto a locked exe. On a Git-for-Windows install with bash resolving to C:\Program Files\Git\bin\bash.exe, uname -s is MINGW64_NT-10.0-26300 and that command works in place.

    That's the same shell-selection problem #51038 and #52025 are on. Your OPENCODE_GIT_BASH_PATH check in installerBash() should cover it — worth confirming it's set for your machine, since with it opencode upgrade should take the working path rather than needing the manual replace.

    One more thing on your last line: 2.0.6 in the docs while 2.0.18 is current looks like a real docs bug, separate from this.

  3. loisekk commented on Oct 16, 2026

    @loisekk

    Your bash diagnosis was right, and I think the locking half of it is wrong. Both are worth separating, because the first one is a real trap and the second sends you down a dead end.

    The bash half

    On a machine like yours, where bash resolves to C:\WINDOWS\system32\bash.exe:

    $ where bash
    C:\WINDOWS\system32\bash.exe          # the WSL launcher
    
    $ bash --version
    wsl: Unknown key 'boot.systemd' in ...\.wslconfig:7
    GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu)   <- Linux bash
    $ echo $?
    -1
    

    V1's upgradeScriptShell() in packages/opencode/src/installation/index.ts was:

    const bashVersion = yield* text(["bash", "--version"])
    if (bashVersion) return "bash"
    return "sh"

    It only checks that the command printed something. The stub prints a full GNU bash banner and then exits -1, so the check passes and the installer gets piped into WSL. It runs inside Linux, $HOME/.opencode/bin is a Linux path, and the upgrade writes to a filesystem the Windows install never reads — which is why the UI can report Installing OpenCode 1.19.3… while --version stays put. Ignoring the exit code there is what makes it silent.

    V2 already closes this. installerBash() on dev never returns a bare bash on Windows: it honours OPENCODE_GIT_BASH_PATH, otherwise derives Git Bash from git --exec-path, otherwise fails loudly with "Git Bash is required to update OpenCode". On this machine that derivation resolves:

    git --exec-path : C:/Program Files/Git/mingw64/libexec/git-core
    derived bash    : C:\Program Files\Git\bin\bash.exe   (exists)
    

    So on 2.0.x, either install Git for Windows or set OPENCODE_GIT_BASH_PATH, and the upgrade runs natively. V1 has no equivalent — I checked, GIT_BASH doesn't appear anywhere in the V1 tree.

    The locking half

    "Windows locks that file, so the replace cannot succeed while OpenCode is running" — I couldn't reproduce that, and I think the probe in #52025 is what makes it look true:

    [System.IO.File]::Open('...opencode.exe','Open','ReadWrite','None')
    → being used by another process
    

    That's accurate for a native Win32 caller, which doesn't request FILE_SHARE_DELETE. But install.sh:343 is mv, and under MINGW64 that goes through Cygwin, which does request it. Both observations are correct for their own caller.

    Running the real V1 installer against a sandboxed HOME with the install-dir binary live:

    processes   : 597 598          (TUI + background service)
    hash before : e4224d18c3c96826
    $ HOME=$SB bash install.sh --version 1.19.3 --no-modify-path
    installer exit status : 0
    hash after            : 81bb8ddc61554dd8
    VERDICT: BINARY WAS REPLACED
    

    Same-fs and cross-filesystem both replace it. The one path that genuinely can't work over a running exe is cp at install.sh:354, used by --binary — cp has to open the destination for writing:

    cp: cannot create regular file '.../opencode.exe': Device or resource busy
    exit=1
    

    That's the shape of your .old workaround, and doing rename-aside first is the correct fix for that path rather than a hack.

    Caveats

    I used ping.exe as the stand-in running binary, not opencode.exe itself — same Windows semantics, but not the same image. And I ran under Git Bash invoked by absolute path, which is why I could get a clean result where your bare bash could not. I didn't test with a live WSL distro present, so I can't say exactly what the stub does in your docker-desktop case — only that its banner passes V1's check and its exit code is non-zero, which is enough for the bug to happen.

    The most useful thing here might be surfacing installer stderr on failure. Three separate reports have now misdiagnosed this, and opencode upgrade currently discards it.

  4. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed write-up and the workaround. Here is where each part stands in current v2:

    What's left from your report is a way to install and upgrade from native PowerShell on a machine without Git Bash, for example a PowerShell installer. The updater doesn't support that today, so I've marked this as a feature request for the maintainers to consider.

    Related issues:

  5. neriousy commented on Nov 12, 2026

    @neriousy
    Member

    OpenCode v2 is now available on WinGet. Could you try this from PowerShell? No WSL, Git Bash, or npm needed.

    winget install --id Anomaly.OpenCode --exact

    WinGet installs its own copy rather than replacing the exe you extracted. In a new PowerShell window, run Get-Command opencode -All to see which copy comes first, then opencode --version. For future updates, use winget upgrade --id Anomaly.OpenCode --exact.

    Could you confirm whether PowerShell picks up the WinGet copy? If it still finds the old exe first, please share the paths from Get-Command opencode -All.

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