On native Windows, external CLI commands are always wrapped by PowerShell, making permission rules hard to match reliably
Problem
When using Codex CLI on native Windows, external CLI commands appear to be wrapped and executed through PowerShell rather than being treated as direct command/argv invocations.
For example, I would expect a command like this to be recognized in a direct form:
["agent-browser", "open", "https://example.com"]
But in practice, execution appears closer to:
pwsh.exe -Command "agent-browser open https://example.com ..."
This makes permission rules, prefix matching, and command recognition much less reliable on Windows, especially for tools like agent-browser that are already standalone CLIs.
---
Current issue
- External CLI commands on native Windows are generally wrapped inside PowerShell.
- The rules system often sees
pwsh.exe -Command "..."instead of the original external command and its arguments. - This makes it difficult to configure tools like
agent-browseras reliably default-allowed via prefix rules. - Matching behavior may vary depending on arguments, URLs, or chained command forms, which reduces predictability.
---
Expected behavior
I would like Codex to provide more stable and predictable execution and rule-matching behavior for external CLI commands on native Windows.
From a user perspective, the expectation is:
- External CLI commands should be recognized as close to their original command form as possible;
- Permission rules should be able to reliably match the actual tool prefix users intend to allow;
- Standalone CLIs such as
agent-browsershould be easier to configure as default-allowed without being affected by PowerShell wrapping behavior.
---
Codex information
- Codex CLI version: 0.116.0
- Platform: native Windows
- Shell: PowerShell / pwsh
- Approval mode: on-request
- Sandbox mode: workspace-write
---
Additional context
This mainly affects external CLI tool execution and permission rule matching on native Windows. The problem becomes especially visible when trying to default-allow specific command-line tools.
2 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
There look to be two independent mechanisms here, which may be why the behaviour seems to vary by argument. All references are main @ bb5054fe.
The wrap itself is unconditional.
shell_commandtakes a script string andderive_exec_argsalways produces[pwsh, -NoProfile, -Command, <script>](core/src/shell.rs:32-41, via handlers/shell/shell_command.rs:84-86), and that argv is what the approval layer receives verbatim (handlers/shell.rs:175-190).Rule matching then tries to unwrap and fails open.
commands_for_exec_policy(core/src/exec_policy.rs:845-883) attempts the PowerShell lowering at :856-867, and if it returns None it falls through to :878-882 and evaluates the raw wrapper argv["pwsh.exe","-NoProfile","-Command","<whole script>"], which no userprefix_rulecan match. Withcommand_origin == Genericthe safelist check at :743-749 also cannot fire, so you get prompted.The lowering rejects more than it looks like it would.
try_parse_powershell_ast_commandsmaps both Unsupported and Failed to None (command_safety/powershell_parser.rs:37-45), and the AST walker in powershell_parser.ps1 returns unsupported for: any parse error (:41-43); param/begin/process/clean blocks,using, traps (:47-58);--%(:63-67); any top-level statement that is not a pipeline, which excludes assignments, if, foreach, try/catch and function (:234-248); any redirection or invocation operator, so& "C:\path\tool.exe" argsis out (:160-169); and any non-literal argument, so$env:FOO,"$dir\file",$(...)and@argsare out (:115-152, whose own comment says dynamic forms make the whole request unsupported). It is all-or-nothing per invocation, so one dynamic argument anywhere makes every segment fall back to the raw wrapper.There is a second gate that also fails closed:
extract_powershell_commandrejects any flag before-Commandoutside a four-item allowlist (shell-command/src/powershell.rs:9, :60-63), so-NonInteractive,-ExecutionPolicy Bypass,-NoExit,-Fileand-EncodedCommandall prevent unwrapping.Separately, and I think this is the part that best explains "varies depending on arguments, URLs": the approval cache never lowers PowerShell at all.
canonicalize_command_for_approval(core/src/command_canonicalization.rs:14-38) has aparse_shell_lc_plain_commandsbranch for bash at :15-19 but no PowerShell equivalent, and the PowerShell path at :30-34 returns[<prefix>, <raw script string>]. So on Windows the approval key is the exact script text, and every distinct URL is a new cache key with no relation to the tool name. "Approve for this session" effectively means "approve this exact string".Verified from source: the unconditional wrap, the Generic fallback, the rejection list, the flag allowlist, the missing canonicalization branch, and the safelist consequence.
Hypothesis, since I do not have a Windows host to run these: that the specific
agent-browser open <url>failures are parse errors rather than dynamic-argument rejections. An unquoted&in a query string is a PowerShell syntax error, which hitsstatus = 'parse_errors'at powershell_parser.ps1:41-43, while the same URL quoted should lower fine. Also worth ruling out that the parser child process is alive at all:PowershellParserProcess::spawn(powershell_parser.rs:115-127) keeps a long-lived-EncodedCommandchild, and if that spawn is blocked by AV or constrained language mode thenparse_with_cached_processreturns Failed and every command falls back, which would make this fully reproducible rather than argument-dependent.If someone on Windows can run these with
prefix_rule(pattern=["agent-browser"], decision="allow")installed, it should separate the two:Predicted: 1 and 2 match the rule, 3 4 and 5 do not. If 1 also fails, check
Get-Process pwsh, powershellwhile Codex is running, because a dead parser child would explain all of it at once. For the approval-cache half: approveagent-browser open https://a.comfor the session, then run it with https://b.com. Being prompted again points at command_canonicalization.rs.On fixes, the cheap pair is adding the PowerShell branch to
canonicalize_command_for_approval, which fixes the per-URL churn but not the prefix-rule complaint, and widening POWERSHELL_FLAGS, being careful that the loop at powershell.rs:57-69 advances one index at a time so value-taking flags like-ExecutionPolicy Bypassneed handling. Widening the AST lowering is what actually fixes the headline complaint, but it needs care: the same lowered argv feedsis_safe_powershell_wordsanddangerous_powershell_words_match, and bash draws that line deliberately, its literal parser being documented as "must not be used to prove that a command is safe". That probably wants a two-tier split, a permissive parse for matching and the conservative one for safety, rather than one widened parser.