Fix -WindowStyle Hidden console window flash#27111
Fix -WindowStyle Hidden console window flash#27111SufficientDaikon wants to merge 5 commits intoPowerShell:masterfrom
Conversation
Use consoleAllocationPolicy=detached manifest and AllocConsoleWithOptions to prevent the OS from auto-allocating a visible console window on newer Windows. On older Windows the manifest is ignored and behavior is unchanged. On Windows 11 build 26100+, the detached policy stops the OS from creating a console window before any code runs. PowerShell now allocates the console itself at the earliest point in startup — visibly for interactive use, or invisibly via AllocConsoleWithOptions(NoWindow) when -WindowStyle Hidden is specified. This approach was recommended by @DHowett (Windows Console team) in PowerShell#3028 (comment) Fix PowerShell#3028
- Replace Substring(1) with AsSpan(1) for zero-alloc early arg parsing - Extract TryAllocConsoleNoWindow() into Interop.Windows (DRY) - Add XML doc comments to AllocConsoleWithOptions enums and struct - Document colon-syntax and false-positive behavior in arg scanner - Change test tag from CI to Feature (matches existing WindowStyle tests) - Remove hardcoded build number from API probe test - Add -ErrorAction Stop to Add-Type in tests - Use double quotes consistently in test file
|
This sounds awesome! I haven't been able to give it an in-depth review, but I do have a note from reading the PR description. If you are on a version of Windows which has |
Per DHowett's feedback: plain AllocConsole() overrides DETACHED_PROCESS from the parent's CreateProcess call. AllocConsoleWithOptions with Default mode respects it. Extract shared TryAllocConsoleWithMode() and add TryAllocConsoleDefault() alongside TryAllocConsoleNoWindow(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thanks @DHowett! Great catch — you're absolutely right. I've pushed a fix in 1a86fe6: normal (non-hidden) launches now use The logic is now:
Both helpers go through a shared generated by opus. |
It is rude to respond to people with AI-generated text. |
|
@DHowett i'm sorry, won't happen again. i automated the process too much, i'm working on dialing it down and adding rules to prevent comments all together because that's not at all the kind of automation i'm looking for. but yes, thank you so much for your reply, and i really do appreciate it because the powershell repo is waaay to big for me to understand quickly enough for my PRs to not have mistakes, so i need feedback so i can keep working on iterating on the PRs i'm making quickly so they get merged as cleanly as easily as possible. again, i apologize, that is incrediblly rude, i didn't mean for that to happen at all, i also acknowledge that i should've been more attentive of what my powershell agent was doing. |
|
@SufficientDaikon hey thanks for saying all that :) it's really cool that the tools of the modern day allow you to take on a daunting task like this! I know everyone's still figuring out their whole workflow, so don't feel too bad! |
|
@DHowett but yea, i'll keep working on this and tag you when i need input. thank you again, i appreciate your understanding ❤️ |
The early arg scan only stripped one leading dash, so --windowstyle hidden was not detected (the key became "-windowstyle" with length 12, failing the <= "windowstyle".Length check). Add double-dash stripping to match the full parser's GetSwitchKey behavior. Remove misleading comment claiming colon syntax is handled by the full parser (GetSwitchKey does not split on colons for windowstyle). Rewrite manifest test to extract embedded manifest from PE binary instead of checking a source file that doesn't exist in $PSHOME. Add 5 early arg scan variant tests: -w, -win, --windowstyle, /windowstyle, UPPERCASE. Total: 12 Pester tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Summary
Eliminate the console window flash when launching pwsh -WindowStyle Hidden by adopting the
consoleAllocationPolicy=detachedmanifest element andAllocConsoleWithOptionskernel32 API — the OS-level solution the Windows Console team built for exactly this purpose1.Important
Progressive enhancement guarantee: On older Windows the manifest is ignored and behavior is identical to the current release. On newer Windows (build 26100+) the console window is never created in the first place.
pwsh.exestays a CUI subsystem binary — no new executables, no breaking changes, no new public API.Fixes #3028
Who This Fixes
-WindowStyle Hiddenshow a brief window on launchAllocateHiddenConsole()flashes when launching native commandsAllocConsoleWithOptions(NoWindow)— no flashChanges
assets/pwsh.manifestconsoleAllocationPolicy=detachedin a new<application>blockengine/Interop/Windows/AllocConsoleWithOptions.csAllocConsoleWithOptions, shared helpers viaTryAllocConsoleWithMode()host/msh/ManagedEntrance.csEarlyConsoleInit(args)+EarlyCheckForHiddenWindowStyle(args)host/msh/ConsoleControl.csSetConsoleMode()guards against null handle→ graceful returnDbg.Assertengine/NativeCommandProcessor.csAllocateHiddenConsole()prefersAllocConsoleWithOptions(NoWindow)test/powershell/Host/WindowStyleHidden.Tests.ps1PR Context
Issue #3028 has been open since January 2017 with 163 👍. Two previous PRs attempted to fix it and both failed:
pwshw.exe(WinExe subsystem)Closed— 127 test failures;Write-Host,Read-Host, native commands brokeClosed— 66 test failuresCaution
Both failed because the root cause is architectural, not a code timing issue.
pwsh.exeis a CUI binary, and Windows creates a visible console window for CUI processes beforeMain()runs. No amount of faster C# startup can prevent the flash.The fix required an OS-level mechanism. The Windows Console team designed
consoleAllocationPolicyandAllocConsoleWithOptionsfor exactly this purpose, shipped in Windows 11 build 261001.Root Cause & How The Fix Works
flowchart TD A["<b>pwsh.exe</b> launched with<br/><code>-WindowStyle Hidden</code>"] --> B{"Windows checks<br/>PE subsystem"} B -->|"CUI (current behavior)"| C["OS creates <b>visible</b><br/>console window"] C --> D["Main() starts"] D --> E["PowerShell parses<br/><code>-WindowStyle Hidden</code>"] E --> F["ShowWindow(SW_HIDE)"] F --> G["⚡ Window already<br/>flashed on screen"] B -->|"CUI + <b>detached</b> manifest<br/>(this PR)"| H["OS skips<br/>console allocation"] H --> I["Main() starts"] I --> J["EarlyConsoleInit()<br/>scans args immediately"] J --> K["AllocConsoleWithOptions<br/>(NoWindow)"] K --> L["✅ Console I/O works<br/>No window ever appeared"] style G fill:#ff6b6b,color:#fff style L fill:#51cf66,color:#fff style C fill:#ff8787,color:#fff style H fill:#69db7c,color:#fffTip
The key insight: previous fixes tried to hide the window faster. This fix prevents the window from being created at all by shifting console allocation from the OS to PowerShell.
How
EarlyConsoleInitdecides what to doflowchart LR A["EarlyConsoleInit(args)"] --> B{"GetConsoleWindow()\n!= 0?"} B -->|"Yes (inherited)"| C{"-WindowStyle\nHidden?"} C -->|Yes| D["ShowWindow(SW_HIDE)\n<i>minimize flash on old Windows</i>"] C -->|No| E["return\n<i>console already exists</i>"] B -->|"No (detached policy)"| F{"-WindowStyle\nHidden?"} F -->|Yes| G["TryAllocConsoleNoWindow()"] F -->|No| H["TryAllocConsoleDefault()\n<i>respects DETACHED_PROCESS</i>"] G -->|"API missing"| I["AllocConsole() +\nShowWindow(SW_HIDE)"] H -->|"API missing"| J["AllocConsole()"] style D fill:#ffd43b,color:#000 style G fill:#51cf66,color:#fff style H fill:#74c0fc,color:#000The diff
public static int Start(string[] args, int argc) { ArgumentNullException.ThrowIfNull(args); + + #if !UNIX + // Allocate console before anything touches CONOUT$/CONIN$ handles. + EarlyConsoleInit(args); + #endif + #if DEBUGWarning
Critical ordering constraint:
EarlyConsoleInit()must run beforeEarlyStartup.Init(). TheLazy<ConsoleHandle>inConsoleControl.csopensCONOUT$viaCreateFile. It's a once-onlyLazyinitializer — if accessed before a console exists, it throws and the exception is cached permanently. The console must exist before any background warmup (AMSI, Compiler init) touches it.Behavior Matrix
manifest ignored
build 26100+
AllocConsoleWithOptions(Default)CreateProcess(DETACHED_PROCESS)Defaultmode respects detached2-WindowStyle Hidden#ff6b6bFlash → hide#51cf66No flash-WindowStyle Hidden#ff6b6bFlash → hide#51cf66No flashEarly arg scan design
EarlyCheckForHiddenWindowStyle(args)runs beforeCommandLineParameterParser. UsesReadOnlySpan<char>for zero heap allocations.Matches:
GetSwitchKey)StringComparison.OrdinalIgnoreCaseDesign tradeoffs:
ShowWindow(SW_HIDE)pathMatchSwitchalso does not split on colons for this parameterWhy
AllocConsoleWithOptions(Default)for normal launchesPer reviewer feedback2: plain
AllocConsole()overridesDETACHED_PROCESSfrom the parent'sCreateProcesscall and force-creates a console.AllocConsoleWithOptionswithDefaultmode respects the parent's intent.Both
TryAllocConsoleNoWindow()andTryAllocConsoleDefault()funnel through a sharedTryAllocConsoleWithMode()helper. Falls back viacatch (EntryPointNotFoundException)on older Windows.API reference
AllocConsoleWithOptionsHRESULT, takesALLOC_CONSOLE_OPTIONS*andALLOC_CONSOLE_RESULT*ALLOC_CONSOLE_OPTIONS{ ALLOC_CONSOLE_MODE mode; BOOL useShowWindow; WORD showWindow; }Minimum: Windows 11 24H2 (build 26100) / Windows Server 2025
Test coverage details
12 Pester tests in
test/powershell/Host/WindowStyleHidden.Tests.ps1:-WindowStyle Hidden -Command, pipeline,Write-Host, exit codesAllocConsoleWithOptionsavailability detection-WindowStyle Normal28 adversarial tests across 5 categories:
-Command, non-hidden stylesPR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright headerFootnotes
Approach recommended by the Windows Console team in this comment.
consoleAllocationPolicyandAllocConsoleWithOptionswere designed specifically for PowerShell's use case. ↩ ↩2Per review feedback:
AllocConsole()overridesDETACHED_PROCESS, whileAllocConsoleWithOptions(Default)respects it. ↩ ↩2