<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jason McKay</title>
    <description>The latest articles on DEV Community by Jason McKay (@jason_jmanmckay_5106d7).</description>
    <link>https://dev.to/jason_jmanmckay_5106d7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4188104%2F971ab8cb-63d8-47f9-b9f8-69529c21657f.jpg</url>
      <title>DEV Community: Jason McKay</title>
      <link>https://dev.to/jason_jmanmckay_5106d7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jason_jmanmckay_5106d7"/>
    <language>en</language>
    <item>
      <title>The permission you can't prompt away</title>
      <dc:creator>Jason McKay</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:48:31 +0000</pubDate>
      <link>https://dev.to/jason_jmanmckay_5106d7/the-permission-you-cant-prompt-away-2hgo</link>
      <guid>https://dev.to/jason_jmanmckay_5106d7/the-permission-you-cant-prompt-away-2hgo</guid>
      <description>&lt;p&gt;&lt;strong&gt;Enforcing agent safety in the tool layer, not the system prompt.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drawn from a personal agentic system ("Vantage") I built and run daily.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Most agent "guardrails" are sentences. Somewhere in a system prompt there is a line like &lt;em&gt;"Never delete files without asking"&lt;/em&gt; or &lt;em&gt;"Do not send email on the user's behalf."&lt;/em&gt; The line is polite, it is well-intentioned, and it is not a control. It is a request addressed to a probabilistic text generator, evaluated anew on every turn, competing with everything else in the context window. The day it loses that competition is the day you find out your safety model was a suggestion.&lt;/p&gt;

&lt;p&gt;I want to argue a narrower and more useful position than "prompts are unreliable," which everyone already knows. The position is: &lt;strong&gt;the boundary between what an agent may suggest and what it may do should be enforced by the shape of the tools you give it — ideally by the tools you withhold — and that enforcement belongs in a service you can test without a model in the loop.&lt;/strong&gt; A prompt is a request. A missing tool is a guarantee.&lt;/p&gt;

&lt;p&gt;I'll make the case with a system I actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Vantage is a personal agentic system. The agent (Claude Code, today — the substrate is meant to be replaceable) connects over MCP to a Spring Boot service that owns everything durable: an append-only event log in MySQL, a set of read collectors, and a small number of write tools. The service is plain Java, tested with JUnit, with no LLM anywhere in its tests. That split is the whole design — the model is the controller, the service is the service layer — and the safety model falls directly out of it.&lt;/p&gt;

&lt;p&gt;The founding rule is three sentences long:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read, summarise and draft are autonomous. Send, spend, delete, or anything publicly visible requires explicit confirmation. This is enforced in the service, not in a prompt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The interesting word is the last one, and the interesting mistakes are in how you honour it.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Write" is the wrong category
&lt;/h2&gt;

&lt;p&gt;The first thing the tiering gets right is that it does not tier on &lt;em&gt;read versus write&lt;/em&gt;. It tiers on &lt;em&gt;effect in the world&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That distinction matters because the obvious implementation — "reads are free, writes need approval" — immediately strangles the system. The agent's most useful autonomous act is to &lt;em&gt;record&lt;/em&gt; things: file an idea, note an observation, draft a proposal. Those are writes. They are also completely safe, because they touch only the system's own metadata — nothing is sent, nothing is spent, nothing is deleted, nothing becomes visible to anyone but the user. We call that &lt;strong&gt;tier zero&lt;/strong&gt;: a write whose blast radius is zero stays autonomous.&lt;/p&gt;

&lt;p&gt;So the question a new tool has to answer is not &lt;em&gt;"does this write?"&lt;/em&gt; but &lt;em&gt;"can the effect of this be seen or felt outside the system, and can it be undone?"&lt;/em&gt; Draft an email: tier zero. Send it: gated. The category that needs a human is &lt;strong&gt;irreversible or outward-facing effect&lt;/strong&gt;, and conflating it with "write" is how you end up with an agent that needs permission to take a note and no permission to wire money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate that is a hole
&lt;/h2&gt;

&lt;p&gt;Here is the part I find most worth stealing.&lt;/p&gt;

&lt;p&gt;The system's one tool that &lt;em&gt;speaks first&lt;/em&gt; — that produces output nobody asked for — lets the agent file a &lt;strong&gt;proposal&lt;/strong&gt;: a suggested next action on a project, for the user to accept or reject. The agent can file one. It cannot accept one. Not because a prompt tells it not to, but because &lt;strong&gt;there is no tool that accepts a proposal.&lt;/strong&gt; The accept path exists only in the user-facing console, behind a human click.&lt;/p&gt;

&lt;p&gt;The comment in the source is the entire security argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * There is no accept_proposal and there must not be. Vantage suggests and Jason decides.
 * The service enforces the budget, but nothing except the absence of this tool stops an
 * agent accepting its own suggestion into Jason's declared next actions -- which would make
 * the accept/reject history a record of Vantage agreeing with itself, and that history is
 * the only feedback signal this feature has.
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that last clause carefully, because it is the general principle hiding inside a specific feature. An agent that can both propose &lt;em&gt;and&lt;/em&gt; approve is not an assistant with oversight; it is an agent with a logbook it writes itself. The approval record becomes a record of the system agreeing with itself, which is worse than no record, because it looks like oversight. The only way to keep the human judgment real is to make it structurally impossible for the agent to forge — and the cheapest structural impossibility is a function that was never written.&lt;/p&gt;

&lt;p&gt;This generalises cleanly. Every time you are tempted to write &lt;em&gt;"the agent should not do X on its own"&lt;/em&gt; into a prompt, ask whether X corresponds to a tool the agent is holding. If it does, you have not built a guardrail; you have built a label on a loaded tool. The fix is usually to split the tool: one that proposes X, one that commits X, and the second one lives somewhere the agent cannot reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bounding the human, not the agent
&lt;/h2&gt;

&lt;p&gt;The same proposal tool shows a second lesson, which is that even a perfectly safe action has a cost, and the cost is the human's attention.&lt;/p&gt;

&lt;p&gt;The first design was a per-run budget: an agent reviewing a project may file at most three proposals. It is the obvious cap and it does not work. Two runs produce six proposals; five runs produce fifteen; each run is individually within budget, and the review pane floods anyway, just more slowly. The flood you have to fear is the &lt;em&gt;second&lt;/em&gt; review — the one nobody is watching for.&lt;/p&gt;

&lt;p&gt;The cap that works counts what is &lt;em&gt;open&lt;/em&gt;, not what was filed per run: &lt;strong&gt;at most three undecided proposals may stand against any one project at a time.&lt;/strong&gt; This looks like a cosmetic change and is a categorical one. A per-run cap bounds the generator's enthusiasm. An open-items cap bounds the human's backlog — the system cannot offer a fourth decision until you have made one of the first three. The limit is denominated in the scarce resource, which is not the agent's output but the user's willingness to keep reading it. A guardrail that caps the wrong quantity gives you the comfortable feeling of a limit and none of the protection.&lt;/p&gt;

&lt;p&gt;And because the limit lives in the service, the tool's own description can tell the model the &lt;em&gt;reason&lt;/em&gt; — "every proposal costs a decision the user did not ask to make, and one that wasn't worth it trains him to dismiss the next one" — so the model files one strong proposal instead of three mediocre ones to fill the quota. The prompt-level instruction is still there. It is just no longer load-bearing. If the model ignores it, the service refuses the fourth proposal regardless. Belt from the service; suspenders from the prompt — never the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the judgment measurable
&lt;/h2&gt;

&lt;p&gt;Two smaller decisions round out the pattern, both about keeping the human's judgment legible after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accepting is editing.&lt;/strong&gt; When the user accepts a proposal, they can accept it &lt;em&gt;as reworded&lt;/em&gt;. The system stores both the original text and what was actually accepted, so the gap between what the agent suggested and what the human was willing to endorse stays measurable. A system that overwrites the suggestion the instant it's accepted throws away its only signal about how good its suggestions were.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decisions are append-only.&lt;/strong&gt; A decided proposal cannot be re-decided. If the answer changes, you file a new one; two dated rows disagreeing &lt;em&gt;is&lt;/em&gt; the history. The approval log is a judgment record, and a judgment record you can quietly edit is not a record. This is the same instinct as the event log underneath the whole system: you append, you never rewrite, and you can therefore replay and audit the system's entire life at 3am when you need to know exactly what it did and when.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs, honestly
&lt;/h2&gt;

&lt;p&gt;This approach is not free, and it is not always right.&lt;/p&gt;

&lt;p&gt;It pushes work out of the prompt and into typed service code, which is slower to change — you ship a Java method and a migration, not a sentence. For a throwaway agent, that overhead is not worth it; a prompt-level "please don't" is a fine control for a toy. The structural approach earns its keep exactly when the cost of the agent being wrong is real and the system is going to live for a while — which is to say, in production, in a company, on anything that can send, spend, or be seen.&lt;/p&gt;

&lt;p&gt;It also demands that you actually enumerate your dangerous actions and give each one a home in the tiering. That is work, and it is the work — "which of our tools have outward-facing effect, and which of those is the agent currently holding?" is a question most teams shipping an agent have not written down the answer to. The value of the tool-layer approach is that it &lt;em&gt;forces&lt;/em&gt; you to write it down, because a tool either exists or it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one line to keep
&lt;/h2&gt;

&lt;p&gt;If you take a single thing from this: &lt;strong&gt;your agent's real permissions are the set of tools it can call, not the set of instructions you've written for it.&lt;/strong&gt; Audit the first list. The second one is documentation of your intentions, and your intentions are not a security boundary.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is one of a set of write-ups drawn from real systems. If your team is putting agents into an environment where a wrong action has consequences — enterprise, regulated, on-prem — the same structural approach applies, and I do advisory work on exactly that: MCP tool design, tiered permission gating, and auditable agent runtimes in Java/Spring environments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>java</category>
      <category>security</category>
    </item>
    <item>
      <title>You can't hide a window you don't own</title>
      <dc:creator>Jason McKay</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:48:10 +0000</pubDate>
      <link>https://dev.to/jason_jmanmckay_5106d7/you-cant-hide-a-window-you-dont-own-1ij9</link>
      <guid>https://dev.to/jason_jmanmckay_5106d7/you-cant-hide-a-window-you-dont-own-1ij9</guid>
      <description>&lt;p&gt;&lt;strong&gt;A Windows scheduled-task saga, and the fix that wasn't where the flag was.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A debugging story from my own machine, with a lesson that outlived the bug.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;powershell.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-WindowStyle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Hidden&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoProfile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\collector.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-WindowStyle Hidden&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The theories that were wrong, in order
&lt;/h2&gt;

&lt;p&gt;I want to show the wrong turns, because they're where the actual mechanism hides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theory one: the flag is being ignored because the script re-shows the window.&lt;/strong&gt; Plausible — PowerShell scripts can call into &lt;code&gt;user32&lt;/code&gt; 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 &lt;em&gt;drawing&lt;/em&gt; the window, which was not my code at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theory two: &lt;code&gt;-WindowStyle&lt;/code&gt; is the wrong lever; use the Task Scheduler "hidden" checkbox.&lt;/strong&gt; Task Scheduler has a &lt;em&gt;Hidden&lt;/em&gt; property right there in the UI. I set it. The window kept flashing. That checkbox, it turns out, hides the task's &lt;em&gt;entry in the Task Scheduler Library list&lt;/em&gt; — 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theory three: run it minimised, or off-screen, or wrapped in a VBScript shim that launches it invisibly.&lt;/strong&gt; This is the point in the forum threads where the hacks pile up, and I nearly joined in. The VBScript-&lt;code&gt;Run&lt;/code&gt;-with-&lt;code&gt;0&lt;/code&gt; trick, the hidden-&lt;code&gt;cmd&lt;/code&gt;-wrapper trick, the &lt;code&gt;conhost --headless&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I was missing: the window isn't PowerShell's
&lt;/h2&gt;

&lt;p&gt;Here is the fact the flag assumes and my machine violated.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-WindowStyle Hidden&lt;/code&gt; is a request about a console window. But on a current Windows 11 desktop, when a console program starts, &lt;strong&gt;the process that creates the visible window is not the console program — it's the default terminal application&lt;/strong&gt;, which is now Windows Terminal, not the old &lt;code&gt;conhost&lt;/code&gt;. Windows hands the new console off to Windows Terminal to host. That handoff is the hinge. The window style that &lt;code&gt;powershell.exe&lt;/code&gt; asked for belongs to a window &lt;code&gt;powershell.exe&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;So the flag isn't being &lt;em&gt;overridden&lt;/em&gt;. 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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(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.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix was in a field I'd never looked at
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;there is no desktop to draw on.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Windows already has this. It's the choice buried in the task's General tab between &lt;em&gt;Run only when user is logged on&lt;/em&gt; and &lt;em&gt;Run whether user is logged on or not&lt;/em&gt;. The second one isn't about reliability-when-logged-out, which is how it reads. It changes the task's &lt;strong&gt;logon type&lt;/strong&gt; 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 &lt;em&gt;in my own logged-in session&lt;/em&gt;, where a window has a screen to appear on.&lt;/p&gt;

&lt;p&gt;In PowerShell, registering it that way rather than clicking through the dialog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-ScheduledTaskAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Execute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'powershell.exe'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="nt"&gt;-Argument&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'-NoProfile -File "&amp;lt;path-to-script&amp;gt;"'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$principal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-ScheduledTaskPrincipal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UserId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="nt"&gt;-LogonType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;S4U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RunLevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Limited&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Register-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Collector'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Principal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$principal&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the one-liner that told me it had actually taken — the test I trusted over any amount of forum folklore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Collector'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Principal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogonType&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# want: S4U   (not: Interactive)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Interactive&lt;/code&gt; is the setting that lets the window flash. &lt;code&gt;S4U&lt;/code&gt; is silence. I didn't touch &lt;code&gt;-WindowStyle&lt;/code&gt; at all in the end — I'd been tuning a dial on the wrong box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one task that couldn't be fixed, and why that's the real lesson
&lt;/h2&gt;

&lt;p&gt;One of my jobs refused to go quiet, and it's the most instructive one on the list.&lt;/p&gt;

&lt;p&gt;It was a focus-watcher: a small script that periodically asked Windows &lt;em&gt;which application is in the foreground right now&lt;/em&gt; — a &lt;code&gt;GetForegroundWindow&lt;/code&gt; 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 &lt;strong&gt;has no foreground window&lt;/strong&gt; because it has no desktop. The exact property that made the task quiet is the property that made the task pointless.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The general shape is worth keeping: &lt;strong&gt;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.&lt;/strong&gt; Sometimes the annoyance and the purpose are the same mechanism viewed from two sides, and the task has to go rather than hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this cost, and what I'd tell someone mid-flash
&lt;/h2&gt;

&lt;p&gt;The embarrassing part is how long I spent on the window and how short the real fix was. Days of suspicion aimed at &lt;code&gt;-WindowStyle&lt;/code&gt;, and the answer was a principal setting I'd scrolled past a hundred times because its label talked about &lt;em&gt;logon&lt;/em&gt;, not about &lt;em&gt;windows&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;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: &lt;strong&gt;when a well-documented flag does nothing, stop tuning the flag and find out who actually performs the action it names.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one line to keep
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;which session the task ran in&lt;/em&gt;, two tabs away from where I was looking.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>windows</category>
      <category>powershell</category>
      <category>devops</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
