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
Windows: pwsh installed as a Store app-execution alias is not resolved, so the shell falls back to Windows PowerShell 5.1 #55255
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
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.
Set "shell": "pwsh" in the config.
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.
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:
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):
Accept APPEXECLINK reparse points as executable candidates in the Windows search (lstat + reparse-tag check)
Fall back to native resolution (where.exe / SearchPath) when which() fails
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
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.
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.
Activity
opencode-agent commentedon Oct 13, 2026
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). Inpackages/core/src/shell/select.ts,findExecutablestill resolvespwshonly throughwhich(), 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:
"shell": "pwsh"ignored, 5.1 used insteadSupernatural-cpp commentedon Oct 17, 2026
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
devmatches what I observe in production.Root cause.
shell/select.tsresolves bare names viawhich()(which@2.0.2→isexe@2.0.0).isexevalidates candidates withfs.statSync(file, { throwIfNoEntry: false }).isFile(). For a Store app-execution alias (%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe, anAPPEXECLINKreparse point, tag0x8000001B):fs.statSync()throwsEACCES— notethrowIfNoEntry: falseonly suppressesENOENT, notEACCESfs.lstatSync()returnsisFile: false(the reparse point itself)So
which("pwsh")→nullwhilewhich("powershell")resolves, andselect()returnswin()[0]→C:\Windows\System32\WindowsPowerShell\v1.0\powershell.EXE. Thespawning processlog confirms exactly this path.It's not ACL — it's Node/libuv stat vs the reparse point.
icaclsshows the user has(I)(F)(full control, inherited). Control experiment: Pythonos.stat()on the same alias succeeds (stat.S_ISREG == True, size 0), andwhere.exe pwshresolves both aliases fine. Launching via the alias works everywhere, sinceCreateProcessunderstandsAPPEXECLINK.Absolute alias paths also fail.
executable()doesstatSync(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):
APPEXECLINKreparse points as executable candidates in the Windows search (lstat + reparse-tag check)where.exe/ SearchPath) whenwhich()failsThis 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.
wafbys commentedon Oct 17, 2026
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:
So in this path the
EACCESfrom the APPEXECLINKstatSyncis not contained by the fallback — it propagates out ofSessionModelRequest.prepareand 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/shelllists onlypowershell,bash,cmd—pwshis not among the candidates at all.Verified workaround (matches the "only working config" noted above): pin
"shell"to the versioned package binaryC:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.7.0_x64__8wekyb3d8bbwe\pwsh.exeThe 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-
ENOENTstat errors (unresolvable candidate → treated as not found) so this can never take downSessionModelRequest.prepare; then land theAPPEXECLINK/where.exeresolution fix as intended in #48968.Hayston1001 commentedon Oct 18, 2026
Same behavior on 2.0.x, with the two calls separated out, in case it helps whoever picks this up:
(Get-Process -Id $PID).Pathfrom 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
whichas the only way in will skip a working Storepwshevery time.