GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Fix -WindowStyle Hidden console window flash by SufficientDaikon · Pull Request #27111 · PowerShell/PowerShell Conversation
|
| File | Change |
|---|---|
assets/pwsh.manifest |
Add consoleAllocationPolicy=detached in a new <application> block (asm.v3 xmlns required) |
engine/Interop/Windows/AllocConsoleWithOptions.cs |
New — P/Invoke for AllocConsoleWithOptions, shared TryAllocConsoleWithMode() helper, checks AllocConsoleResult != NoConsole |
host/msh/ManagedEntrance.cs |
New EarlyConsoleInit(args) + EarlyCheckForHiddenWindowStyle(args) |
host/msh/ConsoleControl.cs |
SetConsoleMode() guards against null handle — graceful return instead of Dbg.Assert |
engine/NativeCommandProcessor.cs |
AllocateHiddenConsole() prefers AllocConsoleWithOptions(NoWindow) with AllocConsole() fallback (native commands always need a console) |
test/powershell/Host/WindowStyleHidden.Tests.ps1 |
New — 13 Pester tests (Pester 4 compatible) |
| Context | Old Windows | Windows 26100+ |
|---|---|---|
| Terminal / cmd / pwsh | Console inherited — unchanged | Console inherited — unchanged |
| Explorer double-click | OS creates console — unchanged | AllocConsoleWithOptions(Default) |
CreateProcess(DETACHED_PROCESS) |
No console — unchanged | Default mode respects detached intent |
CreateProcess(CREATE_NO_WINDOW) |
Console session, no window — unchanged | AllocConsoleWithOptions returns ExistingConsole (no-op) |
DETACHED_PROCESS + -WindowStyle Hidden |
No console (crash) | NoWindow creates invisible console — user explicitly requested working I/O |
Task Scheduler + -WindowStyle Hidden |
Flash then hide | No flash |
Shortcut + -WindowStyle Hidden |
Flash then hide | No flash |
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
Ordering constraint: EarlyConsoleInit() must run before EarlyStartup.Init(). The Lazy<ConsoleHandle> in ConsoleControl.cs opens CONOUT$ via CreateFile — it's a once-only Lazy initializer, so if accessed before a console exists, the HostException is cached permanently. The console must exist before any background warmup (AMSI, Compiler init) touches it.
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.
EarlyConsoleInit decides 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)"]
EarlyCheckForHiddenWindowStyle(args) runs before CommandLineParameterParser. Uses ReadOnlySpan<char> for zero heap allocations.
Matches:
-w, -wi, -win, ..., -windowstyle--w, --windowstyle (strips second dash, matching GetSwitchKey)/w, /windowstyleStringComparison.OrdinalIgnoreCaseDesign tradeoffs:
SetConsoleMode path which handles -WindowStyle via ShowWindow-windowstyle:hidden) intentionally unhandled — the full parser's MatchSwitch also does not split on colons for this parameterAllocConsole() fallback in EarlyConsoleInitPer DHowett's review: plain AllocConsole() overrides DETACHED_PROCESS from the parent's CreateProcess call and force-creates a console. AllocConsoleWithOptions with Default mode 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 → EarlyConsoleInit returns early (no-op)TryAlloc* returns false and no console exists → existing behavior is no console I/O; a plain AllocConsole() fallback would defeat DETACHED_PROCESSNativeCommandProcessor.AllocateHiddenConsole() retains its AllocConsole() fallback because native commands unconditionally need a console for I/O — that's a different contract than startup allocation.
Both TryAllocConsoleNoWindow() and TryAllocConsoleDefault() funnel through a shared TryAllocConsoleWithMode() helper that checks result != AllocConsoleResult.NoConsole.
daxian-dbw raised an inconsistency: with -WindowStyle Hidden, NoWindow mode creates a console even under DETACHED_PROCESS, while without -WindowStyle Hidden, Default mode respects DETACHED_PROCESS and 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 — honoring DETACHED_PROCESS and providing no console, causing stdin/stdout crashes — is strictly worse.
| Launch flags | Without -WindowStyle Hidden |
With -WindowStyle Hidden |
|---|---|---|
| Normal | Default → new visible console |
NoWindow → new invisible console |
DETACHED_PROCESS |
Default → NoConsole (respected) |
NoWindow → new invisible console (user asked for I/O) |
CREATE_NO_WINDOW |
Default → ExistingConsole (no-op) |
NoWindow → ExistingConsole (no-op) |
| API | Docs | Notes |
|---|---|---|
AllocConsoleWithOptions |
learn.microsoft.com | Returns HRESULT, takes ALLOC_CONSOLE_OPTIONS* and ALLOC_CONSOLE_RESULT* |
ALLOC_CONSOLE_OPTIONS |
learn.microsoft.com | { ALLOC_CONSOLE_MODE mode; BOOL useShowWindow; WORD showWindow; } |
Minimum: Windows 11 24H2 (build 26100) / Windows Server 2025
13 Pester tests in WindowStyleHidden.Tests.ps1 (Pester 4 compatible — -Skip on It blocks, not Context):
| Context | Tests |
|---|---|
| Manifest verification | PE embedded manifest extraction |
| Output capture | -WindowStyle Hidden -Command, pipeline, Write-Host, exit codes |
| Arg prefix variants | -w, -win, --windowstyle, /windowstyle, -WINDOWSTYLE |
| API probe | AllocConsoleWithOptions availability detection |
| Regression | Normal startup, -WindowStyle Normal |
#if !UNIX guarded — no effect, no risk.-NonInteractive interaction: The early arg scan only looks for -WindowStyle Hidden. It does not check -NonInteractive. This matches existing behavior (the full parser handles -NonInteractive separately).pwsh-preview.exe: No separate pwsh-preview.manifest exists in the repo. If the preview binary shares the same manifest, the fix applies automatically.-windowstyle:hidden): Intentionally unhandled — the full parser's MatchSwitch also does not split on colons for this parameter. Open to guidance if reviewers feel the early scan should handle it.Issue #3028 has been open since January 2017 with 160+ upvotes. Two previous PRs attempted to fix it:
| PR | Approach | Result |
|---|---|---|
| #10962 | Separate pwshw.exe (WinExe subsystem) |
Closed — 127 test failures; Write-Host, Read-Host, native commands broke |
| #10965 | Parser-level changes | Closed — 66 test failures |
Both failed because pwsh.exe is a CUI binary and Windows creates a visible console window before Main() 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: consoleAllocationPolicy and AllocConsoleWithOptions, shipped in Windows 11 build 26100. The Windows Console team (DHowett — designer/implementer of consoleAllocationPolicy) recommended this approach in issue #3028.
📚 Interactive walkthrough of this fix — visual before/after comparisons, annotated source changes, and quizzes
.h, .cpp, .cs, .ps1 and .psm1 files have the correct copyright header
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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]>
There was an error while loading. Please reload this page.
|
Thanks Dustin L. Howett (@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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
It is rude to respond to people with AI-generated text. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Dustin L. Howett (@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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Ahmed Taha (@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! |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Dustin L. Howett (@DHowett) but yea, i'll keep working on this and tag you when i need input. thank you again, i appreciate your understanding ❤️ |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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.
The reason will be displayed to describe this comment to others. Learn more.
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:
consoleAllocationPolicy=detached to pwsh’s embedded manifest to prevent OS auto-allocating a visible console on Win11 24H2+.AllocConsoleWithOptions interop helpers and use them during early console initialization and hidden-console allocation.-WindowStyle Hidden behavior.Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
| 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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
| // 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.
The reason will be displayed to describe this comment to others. Learn more.
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.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
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]>
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
(FWIW, as a note for the author -- I'm not just the person who recommended |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| nint existingConsole = Interop.Windows.GetConsoleWindow(); | ||
| if (existingConsole != nint.Zero) |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Dustin L. Howett (@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?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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 Dongbo Wang (@daxian-dbw) thank you so much for feedback, very very helpful, you're mentioning things i haven't even thought of. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Nice. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Oh, yea, i forgot, I'm gonna see if I can backport this |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
So bad news, can't back port that far Good news, I'll just add a migration guide in the docs, you may as well update at this point. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Dustin L. Howett (@DHowett) is there something holding this up? Need to put on my CV "I'm an open source contributer" and it be true xD |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Ahmed Taha (@SufficientDaikon) Great work here! Seems like this has been thoroughly reviewed and comments addressed. LGTM |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Cache AllocConsoleWithOptions availability in a static field so later calls skip the P/Invoke and its exception once the API is known to be missing, matching the s_WNetApiNotAvailable pattern in WNetGetConnection.cs. Restore the foreground window on the AllocateHiddenConsole early-return path. AllocConsole() can shift focus even when GetConsoleWindow() returns zero, so the restore now runs on every path that captured it. Remove the AllocConsoleWithOptions probe test. It asserted only that a bool-returning method returns a bool, so it passed regardless of the fix.
|
(FYI: I am not on the powershell team, and so I cannot actually meaningfully approve this PR such that it would merge.) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
i see. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Dustin L. Howett (@DHowett), as an aside re:
As of this writing, PowerShell actually quietly aborts on startup when launched with |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
That seems significantly less than ideal. 😱 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
Powershell -WindowStyle Hidden still shows a window briefly
There was an error while loading. Please reload this page.