The WindowStyle Hidden Fix

How PowerShell conquered the console window flash

PR #27111 Issue #3028
6 Files Changed
+200 Lines Added
13 Tests Added
0 Regressions
❌ The Problem

pwsh.exe flashes a console window before -WindowStyle Hidden can hide it

✅ The Solution

Tell Windows to skip auto-allocation, then allocate the right way in code

Before
pwsh.exe
PS C:\> _
⚡ Window flashes visibly
After (This PR)
No window. Zero flash.
Console I/O works invisibly

01Module 1: The Flash

Run pwsh -WindowStyle Hidden -Command 'exit' — a window appears for a split second then vanishes.

What's Happening?

1
Windows OS The operating system that manages process creation and console allocation creates a console window
2
Window is VISIBLE to the user
3
Main() The entry point function where PowerShell code execution begins function starts executing
4
Main() reads -WindowStyle Hidden and hides the window
5
⚠️ Too late — the user already saw the flash
💡 Why Can't PowerShell Prevent This?

Because pwsh.exe is a CUI binary Console User Interface - an executable type that tells Windows to automatically create a console window before any code runs , Windows creates the console window BEFORE any PowerShell code runs. By the time Main() executes and can read the -WindowStyle argument, the window is already visible.

02Module 2: Under the Hood

How Windows creates console windows — and why the OS acts before your code runs

CUI vs GUI Executables

CUI (Console User Interface) apps get a console window automatically. This enables stdin/stdout/stderr, console APIs like Console.ReadLine(), and pipe operators.

GUI (Graphical User Interface) apps don't get a console. They start without any console window.

pwsh.exe is CUI because it needs Console.ReadLine(), Write-Host, and pipe operators to work.

The Conversation Between Windows and PowerShell

🖥️
New process starting... let me check — pwsh.exe is a CUI app
🖥️
CUI means it needs a console. Creating one now... done! ✅
Wait, I just started running Main()... checking args... -WindowStyle Hidden?
I need to hide that console! ShowWindow(SW_HIDE)!
🖥️
Sure, hiding it now. But the user already saw it flash... 😬
🔑 Key Insight

The OS acts BEFORE your code runs. This is a fundamental Windows design — the console exists before Main() is even called.

03Module 3: The Solution

Manifest + Early Initialization = No Flash

Part 1: The Manifest Change

CODE (pwsh.manifest lines 26-30)
<application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
        <consoleAllocationPolicy
            xmlns="http://schemas.microsoft.com/SMI/2024/WindowsSettings">
            detached
        </consoleAllocationPolicy>
    </windowsSettings>
</application>
ENGLISH
"This tells Windows 11 24H2+: Don't create a console window automatically. We'll handle it ourselves."

The detached policy means Windows skips the automatic console creation step, giving PowerShell full control.

Part 2: The New Execution Flow

↓ Watch the execution flow unfold ↓
1
OS reads manifest An XML file embedded in the executable that tells Windows how to handle the application → sees detachedskips console creation
2
Main() starts → calls EarlyConsoleInit(args) before any other code
3
EarlyConsoleInit scans args for -WindowStyle Hidden
4
If hidden → AllocConsoleWithOptions(NoWindow) — invisible console with full I/O
5
If normal → AllocConsoleWithOptions(Default) — normal visible console
6
Older Windows → catch EntryPointNotFoundException → fallback AllocConsole()

Part 3: EarlyConsoleInit Logic

CODE (ManagedEntrance.cs lines 139-184)
private static void EarlyConsoleInit(string[] args)
{
    nint existingConsole = Interop.Windows.GetConsoleWindow();
    if (existingConsole != nint.Zero)
    {
        if (EarlyCheckForHiddenWindowStyle(args))
        {
            Interop.Windows.ShowWindow(existingConsole, 
                Interop.Windows.SW_HIDE);
        }
        return;
    }

    if (EarlyCheckForHiddenWindowStyle(args))
    {
        if (!Interop.Windows.TryAllocConsoleNoWindow())
        {
            Interop.Windows.AllocConsole();
            nint hwnd = Interop.Windows.GetConsoleWindow();
            if (hwnd != nint.Zero)
            {
                Interop.Windows.ShowWindow(hwnd, 
                    Interop.Windows.SW_HIDE);
            }
        }
    }
    else
    {
        if (!Interop.Windows.TryAllocConsoleDefault())
        {
            Interop.Windows.AllocConsole();
        }
    }
}
ENGLISH
First: Check if a console already exists (legacy Windows or inherited console). If yes, hide it if needed, then we're done.

Second: Check if user wants hidden mode. If yes, try the new API to create an invisible console. If the API doesn't exist, fall back to create-then-hide.

Third: For normal mode, try the new API with default settings. If the API doesn't exist, use the old AllocConsole().

Part 4: Argument Scanning

CODE (lines 194-223)
private static bool EarlyCheckForHiddenWindowStyle(string[] args)
{
    for (int i = 0; i < args.Length - 1; i++)
    {
        string arg = args[i];
        if (arg.Length >= 2 && (arg[0] == '-' || arg[0] == '/'))
        {
            int start = 1;
            if (arg.Length >= 3 && arg[0] == '-' && arg[1] == '-')
            {
                start = 2;
            }

            ReadOnlySpan<char> key = arg.AsSpan(start);
            if (key.Length >= 1
                && key.Length <= "windowstyle".Length
                && "windowstyle".AsSpan().StartsWith(key, 
                    StringComparison.OrdinalIgnoreCase))
            {
                if (args[i + 1].Equals("hidden", 
                    StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }
        }
    }
    return false;
}
ENGLISH
Scan through all arguments. For each one:

• Strip leading dashes (-, /, or --)
• Check if it's a prefix match for "windowstyle" (so -w, -win, -windowstyle all work)
• If yes, check if the next argument is "hidden"

This happens before the full parameter parser loads, so it needs to be fast and simple.

04Module 4: Six Files, One Fix

📄
pwsh.manifest
Tell OS: don't auto-create console
+5 lines
📄
AllocConsoleWithOptions.cs
NEW: P/Invoke for the new Win32 API
+98 lines
📄
ManagedEntrance.cs
Early console init at top of Main()
+96 lines
📄
ConsoleControl.cs
Null-handle guard for detached mode
+5 lines
📄
NativeCommandProcessor.cs
Hidden console prefers NoWindow
+12 lines
📄
WindowStyleHidden.Tests.ps1
NEW: 13 Pester tests
+139 lines

AllocConsoleWithOptions API

CODE (lines 78-96)
private static bool TryAllocConsoleWithMode(AllocConsoleMode mode)
{
    try
    {
        var options = new AllocConsoleOptions
        {
            Mode = mode,
            UseShowWindow = 0,
            ShowWindow = 0,
        };

        int hr = AllocConsoleWithOptions(ref options, out _);
        return hr >= 0; // S_OK
    }
    catch (EntryPointNotFoundException)
    {
        return false;
    }
}
ENGLISH
Create options struct with the requested mode (NoWindow or Default), call the Windows API, and return success/failure.

If the API doesn't exist (older Windows), catch the EntryPointNotFoundException and return false so we can fall back to the old method.

NativeCommandProcessor Integration

CODE (NativeCommandProcessor)
bool allocated = Interop.Windows.TryAllocConsoleNoWindow();

if (!allocated)
{
    Interop.Windows.AllocConsole();
    hwnd = Interop.Windows.GetConsoleWindow();
    if (hwnd == nint.Zero)
    {
        return false;
    }
    Interop.Windows.ShowWindow(hwnd, 
        Interop.Windows.SW_HIDE);
}
ENGLISH
Try the new API first (no window flash). If it's not available (API doesn't exist), fall back to the old approach: create a console with AllocConsole(), then immediately hide it with ShowWindow(SW_HIDE).

This ensures the fix works on both new and old Windows versions.

ConsoleControl Null Guard

CODE (ConsoleControl.cs lines 420-429)
internal static void SetConsoleMode(ProcessWindowStyle style)
{
    IntPtr hwnd = GetConsoleWindow();
    if (hwnd == IntPtr.Zero)
    {
        // No console window to modify. This can happen when
        // running with consoleAllocationPolicy=detached before
        // the console is allocated.
        return;
    }
    // ... switch on style ...
}
ENGLISH
Before trying to modify the console window, check if it exists. With the new detached manifest policy, there might be a brief moment where PowerShell code runs but the console hasn't been allocated yet.

Instead of crashing, just return early. The console will be allocated soon.

05Module 5: Every Scenario

Behavior Matrix

Launch Context Win11 26100+ (New) Older Windows (Legacy)
Direct terminal
Console inherited, no flash
EarlyConsoleInit detects existing console, keeps it visible
Console inherited, no flash
OS provides console automatically, works as before
Explorer/Scheduler
Visible console, no flash
AllocConsoleWithOptions(Default) creates normal console
Visible console, no flash
OS creates console automatically, works as before
-WindowStyle Hidden
No window ever appears ✨
AllocConsoleWithOptions(NoWindow) creates invisible console with full I/O
Brief flash, then hidden
Falls back to AllocConsole() + ShowWindow(SW_HIDE), but now happens in EarlyConsoleInit (faster than before)
DETACHED_PROCESS parent
Respects parent's intent
AllocConsoleWithOptions(Default) respects the DETACHED flag from parent
Overrides to visible
Old AllocConsole() would override DETACHED flag and create a visible console
Inherited console
Uses inherited console
EarlyConsoleInit detects existing console via GetConsoleWindow() and uses it
Uses inherited console
OS provides inherited console automatically

Scenario Quiz

Q1: A user runs pwsh -WindowStyle Hidden from Task Scheduler on Windows 11 24H2. What happens?
A) Console flashes briefly then hides
B) No console window ever appears
C) PowerShell crashes
D) A visible console stays open
✓ Correct! On Windows 11 24H2+, the manifest tells Windows not to create a console. EarlyConsoleInit then calls AllocConsoleWithOptions(NoWindow), which creates an invisible console with full I/O. No flash occurs.
Q2: Same command on Windows 10. What happens?
A) No console window ever appears
B) Console flashes briefly then hides
C) PowerShell uses a different API
✓ Correct! Windows 10 doesn't support the new API, so TryAllocConsoleNoWindow() returns false. The code falls back to AllocConsole() + ShowWindow(SW_HIDE). However, this now happens in EarlyConsoleInit at the very start of Main(), so the flash is much shorter than before.
Q3: A parent process creates pwsh with DETACHED_PROCESS flag on Win11 24H2. What does EarlyConsoleInit do?
A) Creates a hidden console
B) Creates a visible console
C) Calls AllocConsoleWithOptions(Default), which respects the parent's DETACHED flag
D) Skips console allocation entirely
✓ Correct! When the parent uses DETACHED_PROCESS, EarlyConsoleInit calls TryAllocConsoleDefault(). The new API respects the parent's DETACHED flag, so no console is created. This is better than the old AllocConsole(), which would override DETACHED and force a visible console.

06Module 6: The Tests

Test Coverage

Manifest Validation
1

Verifies pwsh.manifest contains the detached policy

Output Correctness
4

Tests that -WindowStyle Hidden still captures output, errors, exit codes, and Write-Host

Arg Prefix Variants
5

Tests -w, -win, -window, -windowstyle, and --windowstyle

API Probe
1

Checks if AllocConsoleWithOptions is available

Normal Startup
2

Ensures normal (non-hidden) launch still works

Key Test: Output Correctness

CODE (WindowStyleHidden.Tests.ps1)
It "captures output from -WindowStyle Hidden -Command" {
    $output = & $powershell -NoProfile `
        -WindowStyle Hidden `
        -Command "'hello'"
    
    $output | Should -Be "hello"
}
ENGLISH
Run PowerShell with -WindowStyle Hidden, tell it to output the string 'hello', and verify we actually get 'hello' back.

This proves console I/O works even without a visible window. The invisible console created by AllocConsoleWithOptions(NoWindow) has full stdin/stdout/stderr support.

Final Understanding Quiz

Q1: Why does the fix use a manifest rather than just calling the API earlier?
A) Manifests are faster
B) The manifest prevents the OS from creating a console before any code runs
C) The API doesn't work without a manifest
✓ Correct! The fundamental problem is that the OS creates the console before Main() runs. No amount of "calling earlier" solves this — we need the manifest to tell the OS "don't create a console at all." Then our code can create the right kind of console.
Q2: Why does EarlyConsoleInit scan args itself instead of using the full parameter parser?
A) The full parser hasn't loaded yet — we need to check before any other code runs
B) The full parser is too slow
C) The full parser doesn't support -WindowStyle
✓ Correct! EarlyConsoleInit is called at the very top of Main(), before any PowerShell infrastructure loads. The full parameter parser depends on dozens of assemblies and subsystems. We need a simple, fast scan that works with just the raw string[] args.
Q3: What happens if someone runs -w Hidden (shortest prefix)?
A) It doesn't work — too short
B) EarlyConsoleInit recognizes it because it matches any unambiguous prefix of 'windowstyle'
C) Only the full parser handles it
✓ Correct! The code checks: "windowstyle".AsSpan().StartsWith(key, StringComparison.OrdinalIgnoreCase). As long as the prefix is at least 1 character and doesn't exceed "windowstyle".Length, it matches. So -w, -win, -windowstyle all work.
Q4: Why does normal (non-hidden) launch use AllocConsoleWithOptions(Default) instead of AllocConsole()?
A) Default is faster
B) Default respects DETACHED_PROCESS from the parent, while AllocConsole() overrides it
C) AllocConsole() is deprecated
✓ Correct! When a parent process creates PowerShell with the DETACHED_PROCESS flag, it's saying "don't create a console." The old AllocConsole() would ignore this and create one anyway. AllocConsoleWithOptions(Default) respects the parent's intent.
🎉 You've Completed the Course!

You now understand how PowerShell conquered the console window flash by combining a manifest directive with early initialization and graceful fallbacks. This fix works on both new and old Windows versions, respects parent process intentions, and maintains full console I/O functionality.