A PowerShell window flashed up on our desktop every 30 minutes, on the dot. It was a black console that appeared and vanished while we were in the middle of something else. The cause turned out to be one property on a scheduled task, and the fix removed the window completely with no stored password. The wrong turn we almost took came from a review that was right about a limitation and wrong about whether it applied.

One word in the principal

The culprit was a Windows scheduled task we’d set up for a small orchestration job. It runs every 30 minutes, classifies what’s in our queue and writes out a proposal file. It does local file work and makes one outbound HTTPS call to an LLM API. Nothing about it should ever draw a window.

We registered it with its principal set to LogonType = Interactive. In Task Scheduler terms that is “Run only when user is logged on”. An Interactive task runs attached to your desktop session, so when it launches a PowerShell process, that process gets a real console on your actual desktop. Every time the trigger fired, we saw it pop up and disappear.

The script was not at fault. The logon type told Windows “this thing belongs to the user’s desktop,” and Windows drew it there. Interactive was the wrong choice for a task that never needed the desktop.

The fix: S4U

The fix is to stop attaching the task to the desktop. That is the “Run whether user is logged on or not” option, which under the hood uses an S4U principal (Service-For-User). An S4U task runs in a non-interactive session with no desktop to draw on, so there is no window to flash. S4U also does not require you to store the account’s password the way the older “run whether logged on or not, with password” path does.

Terminal window
New-ScheduledTaskPrincipal -LogonType S4U -RunLevel Limited

Re-register the task with that principal and the every-30-minute flash is gone. No window, no stored secret.

The review that said S4U was out

Before we made the change we ran the situation past codex to sanity-check it. Codex came back recommending a stored-password logon over S4U, on the grounds that S4U has more environment limitations and “no network/EFS access.”

That limitation is real and documented, and on the surface it sounds disqualifying for a task that makes an outbound API call. If S4U can’t touch the network and our task hits an API, then we need the stored-password path.

That reading is wrong. S4U’s “no network access” caveat is about authenticating to network resources as the user: SMB shares, mapped drives, anything where Windows has to present your credentials to another machine. S4U logons don’t carry the credentials for that outbound authentication, so they can’t auth to a file share.

It says nothing about outbound IP connectivity. An ordinary HTTPS call to an API does not need your Windows credentials presented to the remote end. The TCP connection and the TLS handshake go out fine, and the API’s own auth is an app-layer bearer key, not a Windows logon. Our task does local file work plus one outbound HTTPS call, and neither touches the share-authentication path that S4U restricts.

So S4U was sufficient. The stored-password route would have solved a problem we didn’t have, and it has a practical wall of its own here: you can’t type a Windows password into a non-interactive shell to register the task in the first place. The option codex called the safer default was both unnecessary and more friction.

The reviewer was right that the limitation exists. It was wrong about whether the limitation applied.

Verifying the fix ran

A task that no longer flashes a window is easy to “confirm” by just not seeing the window. That proves the symptom is gone. It does not prove the task still works, and an S4U task runs in a different session than your interactive one.

So we triggered a run manually under the new S4U principal and checked the result code. It returned LastResult 0x0, a clean exit, and it wrote a fresh proposal file. The task ran to completion in the non-interactive session and produced its output.

The two result codes you’ll bump into:

  • 0x41301 means the task is currently running. You’ll see it if you check while a run is in flight.
  • 0x0 means a clean exit.

Two things are worth checking before you assume a task that worked Interactive will work under S4U, because session 0 doesn’t inherit your interactive environment:

  • Is every drive the task touches a real local drive? A path on a network drive that mapped fine in your logged-on session may not exist in the non-interactive one. We had to confirm G: was a genuine local disk and not a session-scoped mapping.
  • Do the executables the task calls resolve without your interactive PATH? Tools like node and npm often live on a PATH your interactive profile sets up, so a command that runs fine from your shell can fail to be found under S4U.

Those were the two things codex flagged to check first, and that part of its advice was good.

Cleaning up the rest

Once the pattern was clear it was the same fix everywhere. We switched the daily and weekly cron-style tasks over to S4U too, since none of them need a desktop. The one task we deliberately left as Interactive was a browser broker that drives a real browser session and does need the desktop attached. Leave a task Interactive only when it has to paint on or read from the desktop.

Why a scheduled task and not a service

For the Cloudflare tunnel, reliability between the two options was a wash. As a Windows service, cloudflared runs as SYSTEM, whose profile keeps the .cloudflared directory under systemprofile. A tunnel config is not write-once: we add hostnames, tweak ingress rules and rotate credentials, and each edit there would re-engage UAC. A logon-triggered scheduled task keeps every future config edit free. If the tunnel config were static, the service would be fine.

What to check

If a scheduled task flashes a console window at regular intervals, check whether its principal is set to Interactive. Switching it to S4U removes the window with no stored password to manage. If someone, human or model, says S4U is out because “it has no network access,” look at what the task actually needs: that caveat covers authenticating to network shares and says nothing about an outbound HTTP call. In our case the fix was confirmed by a manual run that returned LastResult 0x0 and wrote a fresh proposal file, and the missing window was only the symptom.