Skip to content

Fix -WindowStyle Hidden console window flash#27111

Open
SufficientDaikon wants to merge 5 commits intoPowerShell:masterfrom
SufficientDaikon:fix/windowstyle-hidden-console-flash
Open

Fix -WindowStyle Hidden console window flash#27111
SufficientDaikon wants to merge 5 commits intoPowerShell:masterfrom
SufficientDaikon:fix/windowstyle-hidden-console-flash

Conversation

@SufficientDaikon
Copy link
Copy Markdown

@SufficientDaikon SufficientDaikon commented Mar 28, 2026

PR Summary

type issue breaking api tests

Eliminate the console window flash when launching pwsh -WindowStyle Hidden by adopting the consoleAllocationPolicy=detached manifest element and AllocConsoleWithOptions kernel32 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.exe stays a CUI subsystem binary — no new executables, no breaking changes, no new public API.

Fixes #3028


Who This Fixes

Consumer Problem today With this PR
Task Scheduler users Scheduled pwsh -WindowStyle Hidden scripts flash a console window on every run No flash — console allocated invisibly
Desktop shortcut users Shortcuts with -WindowStyle Hidden show a brief window on launch No flash on Windows 26100+
System administrators Background maintenance scripts briefly steal focus No visible window, no focus steal
WPF/WinForms hosts AllocateHiddenConsole() flashes when launching native commands AllocConsoleWithOptions(NoWindow) — no flash
PowerShell remoting GUI-less contexts that inadvertently flash windows Progressive enhancement, no regression

Changes

File Change
assets/pwsh.manifest Added consoleAllocationPolicy=detached in a new <application> block
engine/Interop/Windows/AllocConsoleWithOptions.cs New — P/Invoke for AllocConsoleWithOptions, shared helpers via TryAllocConsoleWithMode()
host/msh/ManagedEntrance.cs New EarlyConsoleInit(args) + EarlyCheckForHiddenWindowStyle(args)
host/msh/ConsoleControl.cs SetConsoleMode() guards against null handle Dbg.Assert → graceful return
engine/NativeCommandProcessor.cs AllocateHiddenConsole() prefers AllocConsoleWithOptions(NoWindow)
test/powershell/Host/WindowStyleHidden.Tests.ps1 New — 12 Pester tests

PR Context

Issue #3028 has been open since January 2017 with 163 👍. Two previous PRs attempted to fix it and both failed:

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

Caution

Both failed because the root cause is architectural, not a code timing issue. pwsh.exe is a CUI binary, and Windows creates a visible console window for CUI processes before Main() runs. No amount of faster C# startup can prevent the flash.

The fix required an OS-level mechanism. The Windows Console team designed consoleAllocationPolicy and AllocConsoleWithOptions for 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:#fff
Loading

Tip

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 EarlyConsoleInit decides what to do

flowchart 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:#000
Loading

The 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 DEBUG

Warning

Critical 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 — 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

Launch context Old Windows
manifest ignored
New Windows
build 26100+
From cmd / pwsh / terminal Console inherited — unchanged Console inherited — unchanged
Explorer double-click OS creates console — unchanged AllocConsoleWithOptions(Default)
CreateProcess(DETACHED_PROCESS) No console — unchanged Default mode respects detached2
Task Scheduler + -WindowStyle Hidden #ff6b6b Flash → hide #51cf66 No flash
Shortcut + -WindowStyle Hidden #ff6b6b Flash → hide #51cf66 No flash

Early arg scan design

EarlyCheckForHiddenWindowStyle(args) runs before CommandLineParameterParser. Uses ReadOnlySpan<char> for zero heap allocations.

Matches:

  • Single dash: -w, -wi, -win, ..., -windowstyle
  • Double dash: --w, --windowstyle (strips second dash, matching GetSwitchKey)
  • Forward slash: /w, /windowstyle
  • Case-insensitive via StringComparison.OrdinalIgnoreCase

Design tradeoffs:

  • False positives acceptable — worst case is allocating a hidden console that the full parser later makes visible
  • False negatives fall through to the existing ShowWindow(SW_HIDE) path
  • Colon syntax (-windowstyle:hidden) intentionally unhandled — the full parser's MatchSwitch also does not split on colons for this parameter
Why AllocConsoleWithOptions(Default) for normal launches

Per reviewer feedback2: 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.

  // Normal interactive launch
- Interop.Windows.AllocConsole();
+ if (!Interop.Windows.TryAllocConsoleDefault())
+ {
+     Interop.Windows.AllocConsole();
+ }

Both TryAllocConsoleNoWindow() and TryAllocConsoleDefault() funnel through a shared TryAllocConsoleWithMode() helper. Falls back via catch (EntryPointNotFoundException) on older Windows.

API reference
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

Test coverage details

12 Pester tests in test/powershell/Host/WindowStyleHidden.Tests.ps1:

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

28 adversarial tests across 5 categories:

Category Count Covers
Arg scan edge cases 8 All prefix/case/dash variants
Boundary conditions 4 Args after -Command, non-hidden styles
Functional correctness 8 Write-Host, Write-Error, exit codes, pipeline, env vars, native commands
Normal regression 3 No-WindowStyle, PSEdition, Version
Adversarial inputs 5 Empty args, missing value, wrong prefix, 500-char arg, unicode

PR Checklist

Footnotes

  1. Approach recommended by the Windows Console team in this comment. consoleAllocationPolicy and AllocConsoleWithOptions were designed specifically for PowerShell's use case. 2

  2. Per review feedback: AllocConsole() overrides DETACHED_PROCESS, while AllocConsoleWithOptions(Default) respects it. 2

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
@SufficientDaikon SufficientDaikon requested a review from a team as a code owner March 28, 2026 12:42
- 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
@DHowett
Copy link
Copy Markdown

DHowett commented Mar 28, 2026

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 AllocConsoleWithOptions, you should always use it (when coupled with the "detached" policy). Using AllocConsole will override a CreateProcess(... DETACHED_PROCESS), whereas AllocConsoleWithOptions with ALLOC_CONSOLE_MODE_DEFAULT will respect DETACHED_PROCESS.

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>
@SufficientDaikon
Copy link
Copy Markdown
Author

SufficientDaikon commented Mar 28, 2026

Thanks @DHowett! Great catch — you're absolutely right.

I've pushed a fix in 1a86fe6: normal (non-hidden) launches now use AllocConsoleWithOptions(Default) instead of plain AllocConsole(), so DETACHED_PROCESS from the parent's CreateProcess call is properly respected.

The logic is now:

  • -WindowStyle HiddenAllocConsoleWithOptions(NoWindow) — invisible console session
  • Normal launchAllocConsoleWithOptions(Default) — respects parent's detach intent
  • Older Windows (API missing) → falls back to AllocConsole() via catch (EntryPointNotFoundException)

Both helpers go through a shared TryAllocConsoleWithMode() to keep it DRY.

generated by opus.

@DHowett
Copy link
Copy Markdown

DHowett commented Mar 28, 2026

Great catch — you're absolutely right.

It is rude to respond to people with AI-generated text.

@SufficientDaikon
Copy link
Copy Markdown
Author

SufficientDaikon commented Mar 28, 2026

@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.

@DHowett
Copy link
Copy Markdown

DHowett commented Mar 28, 2026

@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!

@SufficientDaikon
Copy link
Copy Markdown
Author

SufficientDaikon commented Mar 28, 2026

@DHowett
omg i was like panicking thinking i made a huge error, i appreciate that you're so understanding, that's actually so nice, i'm not worried now of actaully making the PRs i want because this whole time i'm just trying to fix my problems but they're way too big to be reviewed in a timely manner 😅

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>
@kilasuit kilasuit added WG-Engine core PowerShell engine, interpreter, and runtime CL-Engine Indicates that a PR should be marked as an engine change in the Change Log WG-NeedsReview Needs a review by the labeled Working Group labels Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-Engine Indicates that a PR should be marked as an engine change in the Change Log WG-Engine core PowerShell engine, interpreter, and runtime WG-NeedsReview Needs a review by the labeled Working Group

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Powershell -WindowStyle Hidden still shows a window briefly

3 participants