Windows execpolicy + PowerShell wrapping: safe delete rules are either bypassable or too noisy
Windows execpolicy + PowerShell wrapping: safe delete rules are either bypassable or too noisy
I am reporting this because I tried to build a simple "prompt before delete" safety policy and ran into a reliability gap on Windows.
The short version:
- direct delete rules work in simple cases
- the same deletes can bypass rules when executed through
pwsh -Command "..." - the only obvious workaround (
prompton allpwsh -Command) becomes noisy enough to block normal usage
I want to explain the reasoning and experiments clearly, step by step.
Goal and why it matters
Goal: always prompt before file deletion commands.
This matters even more if users consider danger-full-access. If delete protection can be bypassed by command shape, the policy is not trustworthy.
---
Step 1: baseline rule works (so far)
I started with direct delete rules:
prefix_rule(pattern=["Remove-Item"], decision="prompt")
prefix_rule(pattern=["del"], decision="prompt")
prefix_rule(pattern=["rm"], decision="prompt")
Reasoning: if the command token starts with Remove-Item (or del/rm), prompt should always trigger.
Check:
codex execpolicy check --pretty --rules .\default.rules Remove-Item .\tmp.txt
Observed result: prompt (expected).
At this point, the rule looked correct.
---
Step 2: hypothesis about wrapper execution
Then I asked: what if the same deletion is executed through PowerShell wrapper form?
pwsh.exe -Command "Remove-Item .\tmp.txt"
Why this hypothesis: agent tooling may execute commands through shell wrappers, not only as direct argv tokens.
Check:
codex execpolicy check --pretty --rules .\default.rules "C:\Program Files\PowerShell\7-preview\pwsh.exe" -Command "Remove-Item .\tmp.txt"
Observed result: no matched rules.
So the same deletion intent now escapes the delete rule, only because command shape changed.
This creates the core fear: a policy that appears safe in direct tests can still be bypassed in wrapped execution paths.
---
Step 3: attempted targeted fix for wrapper form
I tried to explicitly match wrapped deletes:
prefix_rule(
pattern=["C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe", "-Command", "Remove-Item"],
decision="prompt"
)
Reasoning: this should catch pwsh -Command Remove-Item ....
But the match still fails when arguments are present, because the third token is the entire script string, for example:
"Remove-Item .\tmp.txt"
So "Remove-Item" does not match "Remove-Item .\tmp.txt" as a token prefix element.
In practice, exact full-string matching is too brittle (any argument or formatting change can miss).
---
Step 4: broad safety workaround, and why it breaks usability
Next idea:
prefix_rule(
pattern=["C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe", "-Command"],
decision="prompt"
)
Reasoning: if we cannot reliably inspect script content, at least force approval on every wrapped command.
Expected: occasional prompt only when long wrapper commands are used.
Observed in my setup: this triggers for almost everything, so I get approval popups constantly.
So this workaround trades bypass risk for heavy daily friction.
---
Why this feels like an engine limitation
From these experiments, it looks like:
- matching is exact token-prefix based
- with
pwsh -Command, the script body is treated as one opaque token
That combination makes it difficult to express a natural safety policy:
- reliable prompt for destructive operations
- low-noise operation for normal commands
---
Requested improvements (with concrete examples)
Any one of these would help:
- PowerShell-aware rule mode that parses
-Commandscript safely.
Example intent:
powershell_command_rule(contains_cmdlet="Remove-Item", decision="prompt")
- Documented wildcard/regex for script token matching.
Example intent:
prefix_rule(
pattern=["C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe", "-Command", r"^Remove-Item(\s|$)"],
decision="prompt"
)
---
Environment
- OS: Windows
- Shell: PowerShell (
pwsh.exe) - Codex CLI: observed on 2026-03-05
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗