[Bug] Misleading "Executable not found in $PATH" when a hook targets a Windows App Execution Alias (pwsh.exe, python.exe)
Summary
On Windows, pointing an exec-form hook at a binary in%LOCALAPPDATA%\Microsoft\WindowsApps\ fails with an error that names the wrong
cause and the wrong mechanism:
SessionStart:startup [C:\Users\<user>\AppData\Local\Microsoft\WindowsApps\pwsh.exe] failed
Failed with non-blocking status code: Error occurred while executing hook command:
Executable not found in $PATH: "C:\Users\<user>\AppData\Local\Microsoft\WindowsApps\pwsh.exe"
The refusal itself is correct — that path genuinely cannot be spawned (details
below). But the message says $PATH when an absolute path was supplied, and says
"not found" for a file that exists and that ls/dir shows plainly. That sends you
looking for a PATH or installation problem that isn't there.
This is a diagnostics request, not a request to support aliases.
Environment
- Claude Code 2.1.225
- Windows 11 Enterprise 10.0.26200
- Node v24.18.0
- PowerShell 7.6.4 installed as an MSIX/Store package (so the only
pwsh.exeon
PATH is the WindowsApps alias)
Repro
- On a Windows box where PowerShell 7 came from the Store/winget MSIX, confirm
where pwsh resolves to %LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe.
- Add an exec-form hook in
~/.claude/settings.jsonpointing at that path:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "C:\\Users\\<user>\\AppData\\Local\\Microsoft\\WindowsApps\\pwsh.exe",
"args": ["-NoProfile", "-File", "C:\\path\\to\\script.ps1"]
}
]
}
]
}
}
- Start a session. The hook never runs and reports the error above.
Why the message is wrong
Files in WindowsApps\ are not binaries. They are IO_REPARSE_TAG_APPEXECLINK
reparse points. Measured on this machine with Node 24.18.0:
| path | existsSync | stat | lstat |
|---|---|---|---|
| WindowsApps\pwsh.exe (alias) | false | EACCES | 85 bytes |
| WindowsApps\python.exe (alias) | false | EACCES | 108 bytes |
| System32\WindowsPowerShell\v1.0\powershell.exe | true | 454656 | 454656 |
| %LOCALAPPDATA%\Python\pythoncore-3.14-64\python.exe | true | 106208 | 106208 |
So stat fails with EACCES — the reparse point cannot be traversed outside
package activation — and existsSync therefore returns false. The existence
check concludes "missing" when the accurate statement is "present, but not
directly executable."
Confirming the refusal is right and not just overcautious:
spawnSync(alias, ["-NoProfile", "-Command", "1"], { shell: false })
// -> error ENOENT, status null
so there is no "skip the check and let CreateProcess sort it out" alternative.
Worth noting Git Bash resolves the reparse data itself and treats the alias as a
symlink, so the identical path works when tested from bash. A manual pipe-test
of the hook command therefore passes while the real hook has never once fired —
which is how this went unnoticed here for three days.
Suggested fix
When the executable check fails, distinguish the cases. Roughly:
- absolute path supplied → don't say
$PATH lstatsucceeds butstatgivesEACCES, and the path is under
\Microsoft\WindowsApps\ → say so, e.g.
"C:\...\WindowsApps\pwsh.exe" is a Windows App Execution Alias and cannot be
executed directly. Point the hook at the real executable, or use "shell": "powershell".
Even just dropping in $PATH when the input was absolute would remove most of the
confusion.
Related: the same error string is overloaded
#73971 reports a different Windows hook failure — shell auto-detection walks $PATH,
hits the C:\Program Files\Git\bin entry, and tries to spawn the directory — and
it surfaces through this same emitter:
Executable not found in $PATH: "C:\Program Files\Git\bin"
Failed to run: EFTYPE: inappropriate file type or format, uv_spawn
Note the printed path has no trailing \bash.exe. So in that report the message is
also describing the symptom rather than the cause, for a completely different reason.
Distinct bug, distinct root cause — not filing this as a duplicate — but it suggests
the fix belongs in how this message is constructed rather than in either call site:
one string is currently covering "not on PATH", "is a directory", and "is a reparse
point that can't be traversed."
Scope
Not specific to pwsh. On this box the same alias trap applies to python.exe,python3.exe, py.exe, pythonw.exe (Store PythonSoftwareFoundation.PythonManager),winget.exe, wt.exe, and bash.exe — the last being the WSL launcher stub, which
is its own well-known Windows footgun for anyone whose hook says plain bash.
Pointing a hook at pwsh.exe is a natural thing to do on Windows, since on an
MSIX install that alias is the only pwsh on PATH.
Workaround
Use a real executable at a stable path — C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
for PowerShell 5.1. Note that substituting the alias's own target
(C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe)
is a trap: it is version-pinned and breaks again on the next Store update.