Fix -WindowStyle Hidden console window flash#27111
Fix -WindowStyle Hidden console window flash#27111SufficientDaikon wants to merge 8 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 <[email protected]>
|
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 <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR aims to eliminate the visible console window “flash” when launching pwsh -WindowStyle Hidden on newer Windows builds by opting into detached console allocation via the app manifest and proactively allocating the console in Main() using AllocConsoleWithOptions.
Changes:
- Add
consoleAllocationPolicy=detachedtopwsh’s embedded manifest to prevent OS auto-allocating a visible console on Win11 24H2+. - Add
AllocConsoleWithOptionsinterop helpers and use them during early console initialization and hidden-console allocation. - Add a new Pester test file intended to validate manifest and
-WindowStyle Hiddenbehavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
assets/pwsh.manifest |
Opts into detached console allocation policy on newer Windows builds. |
src/System.Management.Automation/engine/Interop/Windows/AllocConsoleWithOptions.cs |
Introduces P/Invoke + helpers for AllocConsoleWithOptions. |
src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs |
Adds early console allocation logic prior to EarlyStartup.Init(). |
src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleControl.cs |
Makes SetConsoleMode resilient when no console window handle exists. |
src/System.Management.Automation/engine/NativeCommandProcessor.cs |
Prefers allocating a hidden console via AllocConsoleWithOptions(NoWindow) when needed. |
test/powershell/Host/WindowStyleHidden.Tests.ps1 |
Adds tests for manifest presence and -WindowStyle Hidden behavior. |
| $pwshExe = Join-Path -Path $PSHOME -ChildPath "pwsh.exe" | ||
| $manifest = [System.Reflection.Assembly]::LoadFile($pwshExe).GetManifestResourceStream("pwsh.exe.manifest") | ||
| if ($null -eq $manifest) { |
There was a problem hiding this comment.
[System.Reflection.Assembly]::LoadFile($pwshExe) will throw for pwsh.exe because it’s not a managed assembly in typical builds; this will fail the test before the fallback logic runs. Wrap this in try/catch (e.g., BadImageFormatException) and fall back to mt.exe/source-manifest extraction when the load fails.
| $content | Should -Match "detached" | ||
| } else { | ||
| # If mt.exe is unavailable, check the source manifest as last resort. | ||
| $srcManifest = Join-Path -Path (Split-Path $PSHOME) -ChildPath "assets/pwsh.manifest" |
There was a problem hiding this comment.
The fallback path uses Split-Path $PSHOME to locate assets/pwsh.manifest, but $PSHOME points to the built/published output, not the repo root, so this path will almost always be missing and the test will be skipped. Use a repo-relative path (for example based on $PSScriptRoot) or a known build variable instead.
| $srcManifest = Join-Path -Path (Split-Path $PSHOME) -ChildPath "assets/pwsh.manifest" | |
| $repoRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent | |
| $srcManifest = Join-Path -Path $repoRoot -ChildPath "assets/pwsh.manifest" |
| $content | Should -Match "consoleAllocationPolicy" | ||
| $content | Should -Match "detached" | ||
| } else { | ||
| Set-ItResult -Skipped -Because "cannot extract embedded manifest (mt.exe unavailable)" |
There was a problem hiding this comment.
After calling Set-ItResult -Skipped, the test should return to prevent any further execution in the It block (Pester guidance) and to avoid future edits accidentally running assertions after the skip.
| Set-ItResult -Skipped -Because "cannot extract embedded manifest (mt.exe unavailable)" | |
| Set-ItResult -Skipped -Because "cannot extract embedded manifest (mt.exe unavailable)" | |
| return |
| // Try AllocConsoleWithOptions with NoWindow mode first to avoid the flash | ||
| // that the AllocConsole() + ShowWindow(SW_HIDE) pattern causes. | ||
| bool allocated = Interop.Windows.TryAllocConsoleNoWindow(); | ||
|
|
||
| bool returnValue; | ||
| if (hwnd == nint.Zero) | ||
| if (!allocated) | ||
| { | ||
| returnValue = false; | ||
| } | ||
| else | ||
| { | ||
| returnValue = true; | ||
| // Fallback for older Windows: allocate and then hide. | ||
| Interop.Windows.AllocConsole(); | ||
| hwnd = Interop.Windows.GetConsoleWindow(); | ||
| if (hwnd == nint.Zero) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| Interop.Windows.ShowWindow(hwnd, Interop.Windows.SW_HIDE); | ||
| AlwaysCaptureApplicationIO = true; | ||
| } | ||
|
|
||
| AlwaysCaptureApplicationIO = true; | ||
|
|
||
| if (savedForeground != nint.Zero && Interop.Windows.GetForegroundWindow() != savedForeground) | ||
| { | ||
| Interop.Windows.SetForegroundWindow(savedForeground); | ||
| } | ||
|
|
||
| return returnValue; | ||
| return true; |
There was a problem hiding this comment.
AllocateHiddenConsole() now returns true whenever TryAllocConsoleNoWindow() returns true, without verifying that a console was actually allocated/attached. Previously the code validated by checking GetConsoleWindow() after allocation and returned false if it was still zero. To preserve the method’s contract, consider validating console allocation after the NoWindow call and falling back (or returning false) if no console is present.
| int hr = AllocConsoleWithOptions(ref options, out _); | ||
| return hr >= 0; // S_OK |
There was a problem hiding this comment.
TryAllocConsoleWithMode treats any HRESULT >= 0 as success and discards the AllocConsoleResult. If the API can succeed while returning NoConsole (for example in Default mode when respecting DETACHED_PROCESS), callers may need to distinguish “API succeeded but no console was allocated” from “console allocated/attached”. Consider returning the AllocConsoleResult (or a richer result type) so call sites can make the right decision.
| int hr = AllocConsoleWithOptions(ref options, out _); | |
| return hr >= 0; // S_OK | |
| int hr = AllocConsoleWithOptions(ref options, out AllocConsoleResult result); | |
| return hr >= 0 && result != AllocConsoleResult.NoConsole; // S_OK and console allocated/attached |
| // No console exists. This means the detached policy is active (newer Windows) | ||
| // and we were launched without console inheritance (e.g. from Explorer, Task Scheduler). | ||
| if (EarlyCheckForHiddenWindowStyle(args)) | ||
| { | ||
| // Hidden: allocate an invisible console session so CONOUT$/CONIN$ work | ||
| // (Write-Host, native commands, etc.) but no window is ever shown. | ||
| if (!Interop.Windows.TryAllocConsoleNoWindow()) | ||
| { | ||
| // Fallback (should not happen since we only reach here on newer Windows, | ||
| // but be defensive): alloc + hide. | ||
| Interop.Windows.AllocConsole(); | ||
| nint hwnd = Interop.Windows.GetConsoleWindow(); | ||
| if (hwnd != nint.Zero) | ||
| { | ||
| Interop.Windows.ShowWindow(hwnd, Interop.Windows.SW_HIDE); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
In EarlyConsoleInit, the Hidden path assumes TryAllocConsoleNoWindow() implies usable console I/O, but doesn’t verify that a console was actually created/attached. If the call returns success while allocating no console, later code that opens CONOUT$/CONIN$ can still fail. Consider verifying allocation (or using the AllocConsoleResult out value) and falling back when no console was created.
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
|
||
| Describe "WindowStyle Hidden console flash fix (Issue #3028)" -Tag "Feature" { |
There was a problem hiding this comment.
The top-level Describe is tagged only as "Feature". CI test runs exclude the 'Feature' tag by default (tools/ci.psm1 sets ExcludeTag to Slow/Feature/Scenario for TagSet=CI), so these tests likely won’t run in CI and won’t protect this change. Consider tagging as CI (or CI + Feature) so the coverage runs in standard CI.
| Describe "WindowStyle Hidden console flash fix (Issue #3028)" -Tag "Feature" { | |
| Describe "WindowStyle Hidden console flash fix (Issue #3028)" -Tag @('CI','Feature') { |
CI uses Pester 4.x which does not support -Skip on Context blocks, causing 'A parameter cannot be found that matches parameter name Skip' on all three platforms. Move -Skip:(!$IsWindows) to individual It blocks (standard pattern). Also addresses Copilot review feedback: - Add CI tag so tests run in standard CI (not just Others) - Remove Assembly::LoadFile path (pwsh.exe is native, not managed) - Use repo-relative path via $PSScriptRoot for manifest fallback - Add return after Set-ItResult -Skipped per Pester guidance Co-authored-by: Copilot <[email protected]>
| // Fallback for older Windows: allocate and then hide. | ||
| Interop.Windows.AllocConsole(); | ||
| hwnd = Interop.Windows.GetConsoleWindow(); | ||
| if (hwnd == nint.Zero) |
There was a problem hiding this comment.
The new code doesn't try restoring the foreground windows before return. Could it be possible that the foreground window focus changes even if GetConsoleWindow returns IntPtr.Zero?
| <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> | ||
| </application> | ||
| </compatibility> | ||
| <application xmlns="urn:schemas-microsoft-com:asm.v3"> |
There was a problem hiding this comment.
Is the xmlns="urn:schemas-microsoft-com:asm.v3" attribute required for the <application> node? We have the xmlns="urn:schemas-microsoft-com:asm.v1" at the <assembly> node above.
| // Console already exists (inherited from parent or auto-allocated on older Windows). | ||
| // If -WindowStyle Hidden was requested, hide the window at the earliest possible moment | ||
| // to minimize the flash on older Windows where the detached policy is not supported. | ||
| if (EarlyCheckForHiddenWindowStyle(args)) | ||
| { | ||
| Interop.Windows.ShowWindow(existingConsole, Interop.Windows.SW_HIDE); | ||
| } |
There was a problem hiding this comment.
I'd say in this case, we leave -WindowStyle to be handled in its original code path (within SetConsoleMode), and in this method, we only deal with the scenarios where existingConsole == nint.Zero.
| // Fallback (should not happen since we only reach here on newer Windows, | ||
| // but be defensive): alloc + hide. | ||
| Interop.Windows.AllocConsole(); | ||
| nint hwnd = Interop.Windows.GetConsoleWindow(); | ||
| if (hwnd != nint.Zero) | ||
| { | ||
| Interop.Windows.ShowWindow(hwnd, Interop.Windows.SW_HIDE); | ||
| } |
There was a problem hiding this comment.
I guess this can happen on old Windows when the current process is created by CreateProcess with either the CREATE_NO_WINDOW or DETACHED_PROCESS flag.
Currently (without changes in this PR), when pwsh starts with the DETACHED_PROCESS flag, it silently crashes because it uses the stdin/stdout which doesn't exist in that case. I'd say we stick to that behavior to keep the code simple.
However, with the new manifest change, we cannot tell the difference between "it's a normal start and OS doesn't automatically allocate console session for pwsh" and "it's started by CreateProcess with DETACHED_PROCESS flag". For the latter case, on build 26100 or later, we will end up creating a console session if -WindowStyle hidden is specified, but no console session is created otherwise (TryAllocConsoleDefault will respect DETACHED_PROCESS), which seems inconsistent.
There was a problem hiding this comment.
we cannot tell the difference between "it's a normal start and OS doesn't automatically allocate console session for pwsh" and "it's started by
CreateProcesswithDETACHED_PROCESSflag".
That's what AllocConsoleWithOptions(... DEFAULT ...) does. It gives you a console if you were started in a way where you would have gotten a console on previous versions, and it does not give you a console if you were started DETACHED.
There was a problem hiding this comment.
e.g. AllocConsoleWithOptions literally lets you detect whether "earlier versions of windows" would have given you a console.
There was a problem hiding this comment.
When EarlyCheckForHiddenWindowStyle(args) returns true, ideally, we only want to call AllocConsoleWithOptions(No_Window) if the currently process starts normally (not by CreateProcess with DETACHED_PROCESS). But if we need to call AllocConsoleWithOptions(Default) to determine if it's a "DETACHED_PROCESS" situation, the Console Window would already show up in the normal startup case and there will be no point to call AllocConsoleWithOptions(No_Window), right?
There was a problem hiding this comment.
Ah, I see what you mean. Yes, AllocConsoleWithOptions(... NO_WINDOW ...) will create a console session if the parent requested a detached session.
However: I think that fulfills the contract just fine. The user gave conflicting information: "Spawn powershell with no console and a hidden console window"? The ambiguity should resolve in favor of the thing-hosted-within-PowerShell having access to console APIs.
There was a problem hiding this comment.
(In my opinion, somebody calling CreateProcess("powershell", ... DETACHED_PROCESS) who chooses to pass -WindowStyle is only harming themselves. They chose this. They did that.)
| /// a console for CUI apps. On older Windows, AllocConsole() returns false (no-op). | ||
| /// </summary> | ||
| private static void EarlyConsoleInit(string[] args) | ||
| { |
There was a problem hiding this comment.
Can we check on the build number of Windows here to simply do nothing for build prior to 26100? Let PowerShell fall back to the original code path for WindowStyle handling on old Windows.
There was a problem hiding this comment.
Version checks are always the wrong choice.
APIs can be backported--and this one very nearly was!--to earlier versions. The only correct way to handle this is to check whether it is available either by calling it and having it fail or by looking it up at runtime.
|
(FWIW, as a note for the author -- I'm not just the person who recommended |
| nint existingConsole = Interop.Windows.GetConsoleWindow(); | ||
| if (existingConsole != nint.Zero) |
There was a problem hiding this comment.
@DHowett If the process is started by CreateProcess with the CREATE_NO_WINDOW flag, it may inherit the existing console session but have no console window created for it, right?
If so, existingConsole would be nint.Zero in that case, and we will call AllocConsoleWithOptions in the new code path. I guess AllocConsoleWithOptions(Default) will return that existing console session, but what will AllocConsoleWithOptions(No_Window) do in that case?
Wow, how does one go about designing and architecting something that'll be used by millions? where do you even start so you make sure people's computers don't like... explode? xD @daxian-dbw thank you so much for feedback, very very helpful, you're mentioning things i haven't even thought of. |
4a930c9 to
4050f51
Compare
EarlyConsoleInit: remove early ShowWindow(SW_HIDE) for existing consoles per daxian-dbw — leave -WindowStyle handling to the existing SetConsoleMode code path. EarlyConsoleInit now only handles the no-console case. TryAllocConsoleWithMode: check AllocConsoleResult instead of discarding it. Return false when result is NoConsole (DETACHED_PROCESS respected) so callers know a console was not actually allocated. NativeCommandProcessor: add comment clarifying foreground window restore runs for both NoWindow and fallback paths. Manifest: add XML comment explaining why asm.v3 xmlns is required on the application element (distinct from root asm.v1 namespace). Co-authored-by: Copilot <[email protected]>
4050f51 to
6214139
Compare
Hah, well, the jury is still out on whether I've made any computers explode. :) Honestly though... this has been a problem that bugged me for years and years. I wrote a spec (here), forgot about it, sent it to people to get their opinions, promoted it on Twitter, got a few responses, let it sit for multiple years (it was written in 2020 and completed in 2023!)... and then when it seemed like everything was agreed, started writing the code. Then we had a Microsoft-internal "API review" and the If you want to see all the gory discussion details, they're over here: microsoft/terminal#7337 |
You really didn't have to ask, i love this stuff, looks like I have reading material for tonight, thanks for sharing! I'm always interested in learning how engineers solve the problems they find often times there's a fun story that comes a along with it, and it always fun to see how people even come up with soultions in the first place it been helping me in thinking how to solve my own problems, even none software people have problems to solve and they come up with soultions that I wouldn't even think to consider, so yeah I'm really excited to read that pr. |
|
Nice. |
Idk, I made this for me, but if there's a lot of people still on older versions I don't mind doing the work to backport. Lemme lookup how many powershell users are still on older versions Edit: ooof okay, yea I'll see what I can do xD |
PR Summary
Eliminate the console window flash when launching
pwsh -WindowStyle Hiddenon Windows 11 24H2+ by using theconsoleAllocationPolicy=detachedmanifest element andAllocConsoleWithOptionskernel32 API.Fixes #3028
Important
Progressive enhancement: 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.
On Windows 11 build 26100+, the OS no longer auto-creates a console window for
pwsh.exe— PowerShell allocates it programmatically at the top ofMain():-WindowStyle Hidden—AllocConsoleWithOptions(NoWindow): console I/O works, no window appearsAllocConsoleWithOptions(Default): respectsDETACHED_PROCESSfrom the parent (per DHowett's review)TryAlloc*returnsfalseviacatch (EntryPointNotFoundException)→ no-op (OS already auto-allocated the console)No
AllocConsole()fallback inEarlyConsoleInit— plainAllocConsole()would overrideDETACHED_PROCESSfrom the parent. On older Windows the manifest is ignored, so the OS auto-allocates the console beforeMain()runs andGetConsoleWindow()returns non-zero —EarlyConsoleInitis a no-op.pwsh.exestays a CUI subsystem binary. No new executables, no breaking changes, no new public API.Changes
assets/pwsh.manifestconsoleAllocationPolicy=detachedin a new<application>block (asm.v3 xmlns required)engine/Interop/Windows/AllocConsoleWithOptions.csAllocConsoleWithOptions, sharedTryAllocConsoleWithMode()helper, checksAllocConsoleResult != NoConsolehost/msh/ManagedEntrance.csEarlyConsoleInit(args)+EarlyCheckForHiddenWindowStyle(args)host/msh/ConsoleControl.csSetConsoleMode()guards against null handle — graceful return instead ofDbg.Assertengine/NativeCommandProcessor.csAllocateHiddenConsole()prefersAllocConsoleWithOptions(NoWindow)withAllocConsole()fallback (native commands always need a console)test/powershell/Host/WindowStyleHidden.Tests.ps1Behavior by launch context
AllocConsoleWithOptions(Default)CreateProcess(DETACHED_PROCESS)Defaultmode respects detached intentCreateProcess(CREATE_NO_WINDOW)AllocConsoleWithOptionsreturnsExistingConsole(no-op)DETACHED_PROCESS+-WindowStyle HiddenNoWindowcreates invisible console — user explicitly requested working I/O-WindowStyle Hidden-WindowStyle Hiddenpublic 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
Ordering constraint:
EarlyConsoleInit()must run beforeEarlyStartup.Init(). TheLazy<ConsoleHandle>inConsoleControl.csopensCONOUT$viaCreateFile— it's a once-onlyLazyinitializer, so if accessed before a console exists, theHostExceptionis cached permanently. The console must exist before any background warmup (AMSI, Compiler init) touches it.Root cause and how the fix works
flowchart TD A["pwsh.exe launched with\n-WindowStyle Hidden"] --> B{"Windows checks\nPE subsystem"} B -->|"CUI (current behavior)"| C["OS creates visible\nconsole window"] C --> D["Main() starts"] D --> E["PowerShell parses\n-WindowStyle Hidden"] E --> F["ShowWindow(SW_HIDE)"] F --> G["Window already\nflashed on screen"] B -->|"CUI + detached manifest\n(this PR)"| H["OS skips\nconsole allocation"] H --> I["Main() starts"] I --> J["EarlyConsoleInit()\nscans args immediately"] J --> K["AllocConsoleWithOptions\n(NoWindow)"] K --> L["Console I/O works\nNo window ever appeared"]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 /\nolder Windows)"| C["return\n(leave to SetConsoleMode)"] B -->|"No (detached policy /\nDETACHED_PROCESS /\nCREATE_NO_WINDOW)"| F{"-WindowStyle\nHidden?"} F -->|Yes| G["TryAllocConsoleNoWindow()"] F -->|No| H["TryAllocConsoleDefault()"] G -->|"API missing\n(older Windows)"| I["no-op\n(preserve existing behavior)"] H -->|"API missing\n(older Windows)"| J["no-op\n(preserve existing behavior)"]Early arg scan design
EarlyCheckForHiddenWindowStyle(args)runs beforeCommandLineParameterParser. UsesReadOnlySpan<char>for zero heap allocations.Matches:
-w,-wi,-win, ...,-windowstyle--w,--windowstyle(strips second dash, matchingGetSwitchKey)/w,/windowstyleStringComparison.OrdinalIgnoreCaseDesign tradeoffs:
SetConsoleModepath which handles-WindowStyleviaShowWindow-windowstyle:hidden) intentionally unhandled — the full parser'sMatchSwitchalso does not split on colons for this parameterWhy no
AllocConsole()fallback in EarlyConsoleInitPer DHowett's review: plain
AllocConsole()overridesDETACHED_PROCESSfrom the parent'sCreateProcesscall and force-creates a console.AllocConsoleWithOptionswithDefaultmode respects the parent's intent.The manifest (
consoleAllocationPolicy=detached) and the API (AllocConsoleWithOptions) were designed and shipped together in Windows 11 build 26100. Therefore:TryAlloc*handles everythingGetConsoleWindow() != 0→EarlyConsoleInitreturns early (no-op)TryAlloc*returnsfalseand no console exists → existing behavior is no console I/O; a plainAllocConsole()fallback would defeatDETACHED_PROCESSNativeCommandProcessor.AllocateHiddenConsole()retains itsAllocConsole()fallback because native commands unconditionally need a console for I/O — that's a different contract than startup allocation.Both
TryAllocConsoleNoWindow()andTryAllocConsoleDefault()funnel through a sharedTryAllocConsoleWithMode()helper that checksresult != AllocConsoleResult.NoConsole.DETACHED_PROCESS + -WindowStyle Hidden
daxian-dbw raised an inconsistency: with
-WindowStyle Hidden,NoWindowmode creates a console even underDETACHED_PROCESS, while without-WindowStyle Hidden,Defaultmode respectsDETACHED_PROCESSand creates none.This is intentional. Per the API docs,
ALLOC_CONSOLE_MODE_NO_WINDOW"allocates a console session without a window, even if this process was created with DETACHED_PROCESS." When the user explicitly passes-WindowStyle Hidden, they are requesting invisible PowerShell with working I/O (Write-Host, native commands, pipeline). The alternative — honoringDETACHED_PROCESSand providing no console, causing stdin/stdout crashes — is strictly worse.-WindowStyle Hidden-WindowStyle HiddenDefault→ new visible consoleNoWindow→ new invisible consoleDETACHED_PROCESSDefault→NoConsole(respected)NoWindow→ new invisible console (user asked for I/O)CREATE_NO_WINDOWDefault→ExistingConsole(no-op)NoWindow→ExistingConsole(no-op)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
13 Pester tests in
WindowStyleHidden.Tests.ps1(Pester 4 compatible —-SkiponItblocks, notContext):-WindowStyle Hidden -Command, pipeline,Write-Host, exit codes-w,-win,--windowstyle,/windowstyle,-WINDOWSTYLEAllocConsoleWithOptionsavailability detection-WindowStyle NormalKnown limitations
#if !UNIXguarded — no effect, no risk.-NonInteractiveinteraction: The early arg scan only looks for-WindowStyle Hidden. It does not check-NonInteractive. This matches existing behavior (the full parser handles-NonInteractiveseparately).pwsh-preview.exe: No separatepwsh-preview.manifestexists in the repo. If the preview binary shares the same manifest, the fix applies automatically.-windowstyle:hidden): Intentionally unhandled — the full parser'sMatchSwitchalso does not split on colons for this parameter. Open to guidance if reviewers feel the early scan should handle it.PR Context
Issue #3028 has been open since January 2017 with 160+ upvotes. Two previous PRs attempted to fix it:
pwshw.exe(WinExe subsystem)Write-Host,Read-Host, native commands brokeBoth failed because
pwsh.exeis a CUI binary and Windows creates a visible console window beforeMain()runs. The flash cannot be eliminated by optimizing managed code startup — the window is created by the Windows loader. The fix required an OS-level mechanism:consoleAllocationPolicyandAllocConsoleWithOptions, shipped in Windows 11 build 26100. The Windows Console team (DHowett — designer/implementer ofconsoleAllocationPolicy) recommended this approach in issue #3028.PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright header