Skip to content

fix(windows): zap crashes on PowerShell 7.6 at startup - #327

Open
Q-xuan wants to merge 5 commits into
zerx-lab:mainfrom
Q-xuan:q-xuan/fix-pwsh76-bootstrap
Open

fix(windows): zap crashes on PowerShell 7.6 at startup#327
Q-xuan wants to merge 5 commits into
zerx-lab:mainfrom
Q-xuan:q-xuan/fix-pwsh76-bootstrap

Conversation

@Q-xuan

@Q-xuan Q-xuan commented Jul 28, 2026

Copy link
Copy Markdown

zap fails to start PowerShell 7.6 on Windows — every new tab shows "Shell process exited prematurely!" within ~1s and the log records shell pty child exited: exit_code=0.

Why

zap spawns pwsh with -NoProfile (app/src/terminal/local_tty/shell.rs, PowerShell branch). On PS 7.6 PSReadline is no longer auto-loaded into the runspace under -NoProfile, so the unconditional

Remove-Module -Name PSReadline

on line 2 of pwsh_init_shell.ps1 throws a terminating error. The whole init script is passed via -Command, so this kills the script before the prompt function (which emits the InitShell OSC) ever runs. pwsh exits 0, zap never gets the hook, marks the shell as dead.

Repro outside zap:

$ pwsh -NoProfile -Command "Remove-Module -Name PSReadline"
Remove-Module: No modules were removed. Verify that the specification of modules
to remove is correct and those modules exist in the runspace.

Fix

-ErrorAction Ignore. One line:

-Remove-Module -Name PSReadline
+Remove-Module -Name PSReadline -ErrorAction Ignore

Ignore over SilentlyContinue: suppresses the error record entirely — no $Error pollution, no Write-Error leaking into the PTY.

Verified

With the fix the InitShell OSC (\e]9278;d;<hex>\a) fires and pwsh stays interactive instead of exiting. zap.log no longer shows the premature exit.

Safe on other PS versions

-ErrorAction Ignore only changes behavior when PSReadline is not loaded — exactly the broken case. When it is loaded (older PS 7.x, normal interactive launches) the module is still removed as before. Same line exists verbatim in warpdotdev/warp (app/assets/bundled/bootstrap/pwsh_init_shell.ps1, commit 32d21d15c), so this will hit more users as PS 7.6 adoption grows.

彭宇 added 5 commits July 27, 2026 22:53
On PS 7.6 + -NoProfile (which zap always uses to spawn pwsh), PSReadline
isn't auto-loaded into the runspace anymore. The unconditional

    Remove-Module -Name PSReadline

on line 2 of pwsh_init_shell.ps1 then throws a terminating error, the
init script dies before the prompt function runs, zap never gets the
InitShell hook, and pwsh exits with code 0 — so every new tab shows
"Shell process exited prematurely!".

Repro:
    pwsh -NoProfile -Command "Remove-Module -Name PSReadline"
    -> Remove-Module: No modules were removed. ...

Add -ErrorAction Ignore so it's a no-op when PSReadline isn't loaded.
Verified locally: with the fix the InitShell OSC fires and pwsh stays
interactive. Safe on older PS versions too (no-op when the module *is*
loaded, which is the normal interactive case).
The Remove-Module fix in the previous commit wasn't enough. The real
crash on PS 7.6 is a parser error:

    ParserError:
      $global:_warpSessionId = [int64]\"$epoch$random\"
                                            ~~~~~~~~~~~
      Unexpected token '$random' in expression or statement.

zap's append_quoted (windows/mod.rs) backslash-escapes " → \" per the
Windows argv-splitting convention, which is correct for cmd.exe-style
consumers. But PowerShell's -Command parser does NOT honour \": on
PS 7.6 the stray \" tokens trip the parser, the whole init script
fails to parse, pwsh exits with code 0 before emitting the InitShell
OSC, and zap shows "Shell process exited prematurely" with an empty
Warpify output panel.

PS 7.5 and earlier tolerated this; PS 7.6's stricter tokenizer doesn't.

Switch the PowerShell branch to -EncodedCommand (UTF-16LE base64). The
payload is opaque to both the Windows argv splitter and the PS
tokenizer, so the quoting bug is sidestepped entirely on every PS
version. Cost is ~3-4x larger argv (~4.4k chars for the current init
script), still far under CreateProcess's 32k limit.

Verified locally: -EncodedCommand + -NoExit keeps pwsh interactive and
the init script parses cleanly (no ParserError). The Remove-Module
-ErrorAction Ignore from the previous commit is still needed (PS 7.6
also stopped auto-loading PSReadline under -NoProfile, so the
unconditional Remove-Module throws if not silenced).
Diagnose PS 7.6 exit-1 crash. Writes the exact command line
(plus per-arg breakdown) passed to CreateProcessW to
%TEMP%\zap_shell_command.log on every shell spawn.
The UTF-16LE blob passed to pwsh -EncodedCommand had a trailing
NUL character (U+0000) appended via .chain(std::iter::once(0)).
PowerShell 7.6 treats this NUL as script content, causing the
init script to error at runtime and pwsh to exit with code 1.

Verified: base64 without the NUL → pwsh exit 0 (success);
base64 with the NUL → pwsh hangs/crashes.

Also removes the temporary debug logging in shell_command()
that wrote the command line to %TEMP%\zap_shell_command.log.
jwp2987 pushed a commit to jwp2987/phosphor that referenced this pull request Jul 29, 2026
PowerShell 7.6's script-block parser chokes on some quoting shapes passed
via -Command, killing the shell process immediately after launch ("Shell
process exited prematurely"). Build the script as base64 UTF-16LE and pass
it via -EncodedCommand instead, which sidesteps the parser entirely.

Also make the PSReadline unload in the init script tolerant of the module
not being loaded (-ErrorAction Ignore instead of a hard failure).

Ports upstream zerx-lab#327.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant