A Windows scheduled-task saga, and the fix that wasn't where the flag was.
A debugging story from my own machine, with a lesson that outlived the bug.
The symptom was absurd enough to feel like a prank. Every so often — no pattern I could see at first — a black terminal window would snap open on my desktop, hold focus for a second or two, and vanish. Mid-sentence, mid-click, it didn't matter: the window would steal the foreground, eat a keystroke or two, and be gone before I could read what was in it. A ghost with a short attention span.
These were my own scheduled tasks. Housekeeping jobs — snapshots, log collectors, a daily brief — all PowerShell, all registered by me, and every one of them launched with the flag that is supposed to make exactly this not happen:
powershell.exe -WindowStyle Hidden -NoProfile -File .\collector.ps1
-WindowStyle Hidden. It is the first answer every forum gives, it is in Microsoft's own docs, and on this machine it did nothing. That gap — between the flag that everyone knows and the behaviour I was watching — is the whole story, and the lesson at the end is not really about PowerShell.
The theories that were wrong, in order
I want to show the wrong turns, because they're where the actual mechanism hides.
Theory one: the flag is being ignored because the script re-shows the window. Plausible — PowerShell scripts can call into user32 and flip their own window state. So I read every script. None of them touched a window. Dead end, but a useful one: it moved the suspicion off my code and onto whatever was drawing the window, which was not my code at all.
Theory two: -WindowStyle is the wrong lever; use the Task Scheduler "hidden" checkbox. Task Scheduler has a Hidden property right there in the UI. I set it. The window kept flashing. That checkbox, it turns out, hides the task's entry in the Task Scheduler Library list — it is a metadata flag about the task, not an instruction about the process's window. A perfect example of a setting whose name describes your intention and not its effect.
Theory three: run it minimised, or off-screen, or wrapped in a VBScript shim that launches it invisibly. This is the point in the forum threads where the hacks pile up, and I nearly joined in. The VBScript-Run-with-0 trick, the hidden-cmd-wrapper trick, the conhost --headless trick. I'm glad I stopped, because every one of them is a workaround for a mechanism I hadn't understood yet, and a workaround you don't understand is a bug you've rescheduled for later.
The thing I was missing: the window isn't PowerShell's
Here is the fact the flag assumes and my machine violated.
-WindowStyle Hidden is a request about a console window. But on a current Windows 11 desktop, when a console program starts, the process that creates the visible window is not the console program — it's the default terminal application, which is now Windows Terminal, not the old conhost. Windows hands the new console off to Windows Terminal to host. That handoff is the hinge. The window style that powershell.exe asked for belongs to a window powershell.exe was never going to draw itself; the host draws it, on its own terms, and the "please start hidden" request doesn't survive the trip.
So the flag isn't being overridden. It's being addressed to the wrong party. I was writing "keep the lights off" on a note and handing it to someone who doesn't work in that building. The terminal host shows its window because showing windows is its job, and a per-invocation style flag from the thing it's hosting doesn't change the host's mind.
(I'll be honest about the edge of my certainty here: I'm describing the behaviour I could reproduce and the mechanism that explains all of it — the default-terminal handoff — not a line I read in Microsoft's own source. The test below is what I actually trusted.)
Once you see it this way, every theory above collapses into the same category error. I kept trying to hide a window from the one process that had no authority over it.
The fix was in a field I'd never looked at
If the problem is that the task runs in a desktop session where a host is standing by to draw a window, the fix is not to hide the window. It's to run the task somewhere there is no desktop to draw on.
Windows already has this. It's the choice buried in the task's General tab between Run only when user is logged on and Run whether user is logged on or not. The second one isn't about reliability-when-logged-out, which is how it reads. It changes the task's logon type to what Windows calls S4U — a non-interactive logon with no associated interactive session. No session, no desktop, no terminal host waiting to host anything. The script runs in the background with nowhere to put a window, so it doesn't put one anywhere. The ghost had been a side effect of running my housekeeping in my own logged-in session, where a window has a screen to appear on.
In PowerShell, registering it that way rather than clicking through the dialog:
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -File "<path-to-script>"'
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERNAME" `
-LogonType S4U -RunLevel Limited
Register-ScheduledTask -TaskName 'Collector' -Action $action -Principal $principal
And the one-liner that told me it had actually taken — the test I trusted over any amount of forum folklore:
(Get-ScheduledTask -TaskName 'Collector').Principal.LogonType
# want: S4U (not: Interactive)
Interactive is the setting that lets the window flash. S4U is silence. I didn't touch -WindowStyle at all in the end — I'd been tuning a dial on the wrong box.
The one task that couldn't be fixed, and why that's the real lesson
One of my jobs refused to go quiet, and it's the most instructive one on the list.
It was a focus-watcher: a small script that periodically asked Windows which application is in the foreground right now — a GetForegroundWindow call — and logged it. Move it to S4U and it goes silent in a different, worse sense: it returns nothing useful, every time. Of course it does. A non-interactive session has no foreground window because it has no desktop. The exact property that made the task quiet is the property that made the task pointless.
That's not a bug to work around. It's the system telling me the truth: this task genuinely needs the interactive desktop, so it genuinely cannot be hidden by removing the desktop, because the desktop is the thing it's there to observe. There is no clever flag that resolves that. The honest move — the one I made — was to retire the task, not to fight it with another layer of shim.
The general shape is worth keeping: when the fix for a nuisance is "remove the context the task runs in," check whether the task's actual job depends on that context. Sometimes the annoyance and the purpose are the same mechanism viewed from two sides, and the task has to go rather than hide.
What this cost, and what I'd tell someone mid-flash
The embarrassing part is how long I spent on the window and how short the real fix was. Days of suspicion aimed at -WindowStyle, and the answer was a principal setting I'd scrolled past a hundred times because its label talked about logon, not about windows.
The transferable lesson isn't a PowerShell incantation — those rot, and the default-terminal landscape on Windows keeps shifting under them. It's a debugging reflex: when a well-documented flag does nothing, stop tuning the flag and find out who actually performs the action it names. The flag described the outcome I wanted. It did not describe the component that produces the window, and on a modern Windows desktop that component had changed out from under the flag. Every hour I lost was spent addressing the right request to the wrong party.
The one line to keep
If a setting that is supposed to suppress a behaviour doesn't, the setting is probably being read by something that doesn't own the behaviour. Find the owner. On this machine the owner of the window wasn't the script, wasn't the flag, and wasn't even a window problem — it was which session the task ran in, two tabs away from where I was looking.
This is one of a set of write-ups drawn from real systems and real machines. I do advisory and hands-on work on exactly this kind of thing — Windows automation, scheduled-task and agent runtimes, and the unglamorous ops debugging where the documented fix and the actual mechanism have quietly drifted apart.
Top comments (0)