A Read-Only Floor That Owns Its Decisions
August 13, 2026 · InfraScout Team
agent_exec_powershell_readonly validates every script against a command policy before it reaches a host. Underneath your administrators' allow and deny rules sits a curated floor that blocks host-mutating cmdlets outright. This release started as a one-line bug in that floor and ended up closing three older holes it had been hiding.
The bug: a pause was treated as a mutation
Start-Sleep was denied, while its bare alias sleep passed. Pausing does not change anything on a host, so a polling script that waited between reads was being refused for no reason.
The cause was not the rule that classifies verbs. A hard backstop catches dangerous bare aliases — start, move, copy, tee and their kin — and it terminated each alias token at a word boundary. A hyphen is a word boundary, so the token start matched the beginning of every Start- cmdlet, copy matched every Copy- cmdlet, and so on. That backstop is deliberately unappealable, so it swallowed cmdlets that belonged to more nuanced rules and never let them get there.
Alias tokens now terminate so that they catch bare aliases only. A full cmdlet name falls through to the rule actually designed for it. Start-Sleep is explicitly exempt — it pauses the calling process and touches nothing on the host — and the Tee-* family, the one alias token with a real cmdlet family behind it that is not otherwise an approved verb, now lands on the file-write rule rather than falling through to nothing.
What the fix exposed
The security review of that change turned up three gaps that all predate it. None were introduced by the fix, and the fix makes none of them worse — but the first is significant.
Set-Item was treated as a benign session cmdlet. It sat alongside genuinely harmless things like Set-Variable and Set-Location. But Set-Item is the generic provider setter: what it writes depends entirely on the drive you point it at. Every one of these passed the read-only floor:
| Script | What it does |
|---|---|
Set-Item C:\Windows\Temp\x.txt -Value … | Arbitrary file write |
Set-Item -Path Function:\prompt -Value {…} | Code execution on the next prompt |
Set-Item HKLM:\…\CurrentVersion\Run\x … | Persistence |
Set-Item -Path Env:\Path -Value … | PATH hijack |
Set-Item WSMan:\localhost\Client\TrustedHosts -Value * | Remoting trust |
The floor already covered the assignment form of the same primitive. It now covers the cmdlet form as well, along with Clear-Item. Both are admin-allowable rather than hard-denied — a curated allow of Set-Item for a specific need is a plausible decision, the same way Set-ItemProperty is treated.
Process launch had no rule of its own. Start-Process and Start-Job were denied only by the accident described above — the start alias token matching their prefix. Narrowing that backstop was correct, but on its own it would have left an administrator's allow entry able to turn read-only PowerShell into arbitrary process launch. Launch now has an explicit rule, and it is hard: no allow entry reopens it. If your operators genuinely need to start processes, that is what the role-gated agent_exec_powershell is for. The rule is scoped to launch primitives only — Start-Service still routes to service control, Start-BitsTransfer to dynamic code execution, and Start-Sleep stays exempt.
Twelve write and execution aliases were missing from the backstop, covering job control, WMI writes, module loading, remote sessions and breakpoints.
curl and wget are deliberately not among them. A bare curl https://example.com mutates nothing, and the full cmdlet it aliases is allowed to do exactly that — an alias rule stricter than the cmdlet it stands for would be incoherent. The floor blocks host mutation, so it keys on the output flag instead: curl https://example.com runs, and curl https://example.com -o C:\a.exe does not.
Two changes to check before you deploy
Both concern administrator-defined allow entries in your command policies.
An allow entry naming Copy-Item, Move-Item, Start-Service or Tee-Object was previously a silent no-op — the hard alias backstop re-denied the command after your rule had excused it. Those entries now work as they were always meant to. If you wrote one long ago and worked around it, the workaround is no longer needed; if you wrote one and forgot about it, it is now live.
An allow entry naming Start-Process, Start-Job, Start-ThreadJob or Invoke-Item stops working, deliberately. Launch is now hard-denied on the read-only path regardless of policy.
Default posture is unchanged for every real cmdlet. If you have not written allow entries, nothing about what your read-only scripts can do has changed except that pauses now work.
See Agent Tools for agent_exec_powershell_readonly, Tool Annotations & Role Scoping for how the read-only boundary is enforced, and Read-Only PowerShell With an Admin Command Policy for the original design.
Questions or feedback? Reach us at info@infrascout.cloud.