Skip to content

execpolicy: unwrap PowerShell -Command wrappers on Windows#20336

Merged
iceweasel-oai merged 9 commits into
mainfrom
codex/windows-powershell-execpolicy-v2
May 1, 2026
Merged

execpolicy: unwrap PowerShell -Command wrappers on Windows#20336
iceweasel-oai merged 9 commits into
mainfrom
codex/windows-powershell-execpolicy-v2

Conversation

@iceweasel-oai
Copy link
Copy Markdown
Collaborator

@iceweasel-oai iceweasel-oai commented Apr 30, 2026

Why

On Windows, Codex runs shell commands through a top-level powershell.exe -NoProfile -Command ... wrapper. execpolicy was matching that wrapper instead of the inner command, so prefix rules like ["git", "push"] did not fire for PowerShell-wrapped commands even though the same normalization already happens for bash -lc on Unix.

This change makes the Windows shell wrapper transparent to rule matching while preserving the existing Windows unmatched-command safelist and dangerous-command heuristics.

What changed

  • add parse_powershell_command_plain_commands() in shell-command/src/powershell.rs to unwrap the top-level PowerShell -Command body with extract_powershell_command() and parse it with the existing PowerShell AST parser
  • update core/src/exec_policy.rs so commands_for_exec_policy() treats top-level PowerShell wrappers like bash -lc and evaluates rules against the parsed inner commands
  • carry a small ExecPolicyCommandOrigin through unmatched-command evaluation and expose is_safe_powershell_words() / is_dangerous_powershell_words() so Windows safelist and dangerous-command checks still work after unwrap
  • add Windows-focused tests for wrapped PowerShell prompt/allow matches, wrapper parsing, and unmatched safe/dangerous inner commands, and re-enable the end-to-end execpolicy_blocks_shell_invocation test on Windows

Testing

  • cargo test -p codex-shell-command
@iceweasel-oai iceweasel-oai changed the title Unwrap PowerShell shell wrappers in execpolicy Use inner powershell script/command in execpolicy Apr 30, 2026
@iceweasel-oai
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6042b542e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/core/src/exec_policy.rs Outdated
@iceweasel-oai iceweasel-oai changed the title Use inner powershell script/command in execpolicy Use powershell AST parser in exec_command for prefix-rule matching Apr 30, 2026
@iceweasel-oai
Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown
Contributor

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@iceweasel-oai iceweasel-oai marked this pull request as ready for review April 30, 2026 18:13
@iceweasel-oai iceweasel-oai requested a review from a team as a code owner April 30, 2026 18:13
@iceweasel-oai iceweasel-oai requested a review from bolinfest April 30, 2026 18:13
cmd,
sandbox_permissions,
used_complex_parsing,
crate::exec_policy::UnmatchedCommandContext {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this struct was introduced to fix a clippy warning about too many parameters due to me adding one new one (command_origin)

@iceweasel-oai iceweasel-oai changed the title Use powershell AST parser in exec_command for prefix-rule matching execpolicy: unwrap PowerShell -Command wrappers on Windows Apr 30, 2026
Comment on lines 1002 to 1004
#[cfg(test)]
#[path = "exec_policy_tests.rs"]
mod tests;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you create a separate file exec_policy_windows_tests.rs and include it here in the same way, but with #[cfg(all(windows, test))] so you don't have to #[cfg(windows)] in exec_policy_windows_tests itself?

Comment thread codex-rs/core/src/exec_policy.rs Outdated
fn commands_for_exec_policy(command: &[String]) -> (Vec<Vec<String>>, bool) {
fn commands_for_exec_policy(
command: &[String],
) -> (Vec<Vec<String>>, bool, ExecPolicyCommandOrigin) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take responsibility for the original tuple return value being bad, so since you're touching this code, can you declare a small struct before this function to use as a return value and return that?

Comment thread codex-rs/core/src/exec_policy.rs Outdated
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_shell_command::is_dangerous_command::command_might_be_dangerous;
#[cfg(windows)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you only use the function in one block in this file and that block already has #[cfg(windows)], I would favor doing the import in that block so the top-level imports are less noisy.

We should add that to AGENTS.md...

];

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ExecPolicyCommandOrigin {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a docstring because I'm a little fuzzy on what this means.

ExecPolicyCommandOrigin::PowerShell to me implies the command was targeted to be run under PowerShell, but looking at the code, it feels like it actually means "the type of shell parser that was used to parse the command." It's possible the user is on Windows but could be using a shell that isn't PowerShell, isn't it? Or no?

To that end, I'm not clear why the other variant is named Direct.

Comment thread codex-rs/core/src/exec_policy_tests.rs Outdated
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
},
ExecApprovalRequirement::Forbidden {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, why isn't this ExecApprovalRequirement::NeedsApproval?


#[tokio::test]
async fn execpolicy_blocks_shell_invocation() -> Result<()> {
// TODO execpolicy doesn't parse powershell commands yet
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that explains that...

/// This is intentionally narrower than the Windows safe-command parser: it only unwraps the
/// `-Command`/`-c` body from a PowerShell invocation we already recognize, then delegates the
/// script itself to the PowerShell AST parser.
pub fn parse_powershell_command_plain_commands(command: &[String]) -> Option<Vec<Vec<String>>> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe parse_powershell_command_into_plain_commands()?

Comment on lines +78 to +79
let (shell, script) = extract_powershell_command(command)?;
try_parse_powershell_ast_commands(shell, script)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think executable is the more appropriate name here.

Suggested change
let (shell, script) = extract_powershell_command(command)?;
try_parse_powershell_ast_commands(shell, script)
let (executable, script) = extract_powershell_command(command)?;
try_parse_powershell_ast_commands(executable, script)
#[cfg(windows)]
#[test]
fn parses_plain_powershell_commands() {
let commands = parse_powershell_command_plain_commands(&[
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add a test for a case where commands has more than one entry?

@iceweasel-oai iceweasel-oai enabled auto-merge (squash) May 1, 2026 00:39
@iceweasel-oai iceweasel-oai merged commit 4f96001 into main May 1, 2026
25 checks passed
@iceweasel-oai iceweasel-oai deleted the codex/windows-powershell-execpolicy-v2 branch May 1, 2026 00:56
@github-actions github-actions Bot locked and limited conversation to collaborators May 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

2 participants