Skip to content

Windows: pwsh installed as a Store app-execution alias is not resolved, so the shell falls back to Windows PowerShell 5.1 #55255

Closed

Description

@Hotragn

Version: OpenCode 2.0.26 (checked against v2 @ 4617210), Windows 11

Description
If PowerShell 7 is installed only from the Microsoft Store/MSIX, pwsh.exe on PATH is an app-execution alias, a zero-byte AppExecLink reparse point in %LOCALAPPDATA%\Microsoft\WindowsApps. Shell resolution doesn't find it and silently uses Windows PowerShell 5.1, even with "shell": "pwsh" set. Commands written for 7.x then fail, for example &&, which throws a ParserError on 5.1.

Evidence (v2 source)

  • packages/core/src/shell/select.ts:59-61: findExecutable only calls which(), which misses the alias.
  • packages/core/src/shell/select.ts:116-123: when executable() returns nothing, select() falls back to win()[0] without a warning.
  • packages/core/src/shell/select.ts:179-181: a configured shell goes through that same select() path.

Repro

  1. On Windows 11, install PowerShell 7 from the Store only. Check that Test-Path 'C:\Program Files\PowerShell\7\pwsh.exe' is False and that pwsh --version works.
  2. Set "shell": "pwsh" in the config.
  3. Ask the agent to run $PSVersionTable.PSVersion. It reports 5.1.x.

An independent repro of the same root cause on 1.19.0 is in #41426.

Fix: #48968

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the code pointers. I couldn't reproduce this here because it needs Windows with PowerShell 7 installed only from the Microsoft Store, and our sandbox runs Linux. It can't create app-execution aliases, and the Windows-only branches of shell selection don't run there.

    What I could check is the code on the latest dev (43bb7ba). In packages/core/src/shell/select.ts, findExecutable still resolves pwsh only through which(), and the fallback to Windows PowerShell you describe hasn't changed there. So the problem very likely still happens. The fix you linked, #48968, is still open. A maintainer with a Windows machine will need to confirm it.

    Related earlier reports, all against v1 and now closed:

  2. Supernatural-cpp commented on Oct 17, 2026

    @Supernatural-cpp

    Update: full root-cause analysis + repro (2.0.32, Node 24.19)

    Still reproducible on 2.0.32. Adding diagnostics to help move this forward — the bot's code review of dev matches what I observe in production.

    Root cause. shell/select.ts resolves bare names via which() (which@2.0.2 → isexe@2.0.0). isexe validates candidates with fs.statSync(file, { throwIfNoEntry: false }).isFile(). For a Store app-execution alias (%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe, an APPEXECLINK reparse point, tag 0x8000001B):

    • fs.statSync() throws EACCES — note throwIfNoEntry: false only suppresses ENOENT, not EACCES
    • fs.lstatSync() returns isFile: false (the reparse point itself)

    So which("pwsh") → null while which("powershell") resolves, and select() returns win()[0] → C:\Windows\System32\WindowsPowerShell\v1.0\powershell.EXE. The spawning process log confirms exactly this path.

    It's not ACL — it's Node/libuv stat vs the reparse point. icacls shows the user has (I)(F) (full control, inherited). Control experiment: Python os.stat() on the same alias succeeds (stat.S_ISREG == True, size 0), and where.exe pwsh resolves both aliases fine. Launching via the alias works everywhere, since CreateProcess understands APPEXECLINK.

    Absolute alias paths also fail. executable() does statSync(shell).isFile() for rooted paths too, so configuring the alias's full path fails identically. The only working config is the real package binary (C:\Program Files\WindowsApps\Microsoft.PowerShell_<ver>_x64__8wekyb3d8bbwe\pwsh.exe), but that versioned dir disappears on every Store update — an auto-update (7.6.6 → 7.6.7) broke a previously working setup mid-day.

    Suggested directions (any one would fix it):

    1. Accept APPEXECLINK reparse points as executable candidates in the Windows search (lstat + reparse-tag check)
    2. Fall back to native resolution (where.exe / SearchPath) when which() fails
    3. Verify candidates by spawning instead of stat'ing

    This is what PR #48968 (and #43323) targeted. #48968 was closed yesterday by automated stale-PR cleanup despite carrying the #43323 review feedback — it would be great to see it revisited. cc @Hotragn

    Environment: opencode 2.0.32, Windows 11 24H2 (26100), PowerShell 7.6.7 (Store/MSIX only), Node 24.19.

  3. wafbys commented on Oct 17, 2026

    @wafbys

    Corroborating evidence from another Windows machine on 2.0.32, with one extra detail: this doesn't only end in a silent fallback to 5.1 — it can also crash the session step.

    Environment: opencode 2.0.32 (portable opencode.exe), Windows 11 26H2 (build 26300.9457), PowerShell 7.6.7 installed from the Microsoft Store only (Test-Path 'C:\Program Files\PowerShell\7\pwsh.exe' → False), no "shell" configured.

    Observed: on fresh session attempts, the model request died during preparation — three occurrences in immediate succession:

    timestamp=2026-10-17T14:17:59.740Z level=ERROR message="Failed to drain Session" cause="Error: EACCES: permission denied, stat 'C:\Users\<redacted>\AppData\Local\Microsoft\WindowsApps\pwsh.exe'
        at statSync (unknown)
        at Qf (B:/~BUN/root/chunk-6hmx3b2s.js:116:6899)
        at Gc (B:/~BUN/root/chunk-6hmx3b2s.js:116:7346)
        at Rc (B:/~BUN/root/chunk-6hmx3b2s.js:116:7621)
        at SessionModelRequest.prepare (B:/~BUN/root/chunk-z20sjgqk.js:382:3343)
        at SessionRunner.runStep
        at SessionRunner.drain"
    

    So in this path the EACCES from the APPEXECLINK statSync is not contained by the fallback — it propagates out of SessionModelRequest.prepare and kills the step. After those failures, later steps ran on the Windows PowerShell 5.1 fallback (every spawn logged …\WindowsPowerShell\v1.0\powershell.EXE), which suggests the resolution result is cached after the first attempt (inference from behavior, not confirmed).

    Also visible via the API: GET /api/config/shell lists only powershell, bash, cmd — pwsh is not among the candidates at all.

    Verified workaround (matches the "only working config" noted above): pin "shell" to the versioned package binary
    C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.7.0_x64__8wekyb3d8bbwe\pwsh.exe
    The shell tool then spawns 7.6.7, and a fresh server + new session completes a request with no EACCES. The downside is exactly #56381: that versioned path dies on every Store update.

    Suggestion: first make shell resolution tolerate non-ENOENT stat errors (unresolvable candidate → treated as not found) so this can never take down SessionModelRequest.prepare; then land the APPEXECLINK/where.exe resolution fix as intended in #48968.

  4. Hayston1001 commented on Oct 18, 2026

    @Hayston1001

    Same behavior on 2.0.x, with the two calls separated out, in case it helps whoever picks this up:

    statSync("%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe")                                   -> EACCES
    statSync("%LOCALAPPDATA%\Microsoft\WindowsApps\Microsoft.PowerShell_8wekyb3d8bbwe\pwsh.exe") -> EACCES
    accessSync(..., X_OK) on both                                                               -> ok
    spawning either                                                                              -> PowerShell 7.6.7
    statSync("C:\Program Files\PowerShell\7\pwsh.exe")                                           -> ok
    

    (Get-Process -Id $PID).Path from the launched alias reports the versioned target, C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.7.0_x64__8wekyb3d8bbwe\pwsh.exe.

    So on Windows a failed stat does not mean "not installed". For an app-execution alias it only means "not a plain file", and a lookup that keeps which as the only way in will skip a working Store pwsh every time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    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