shell-command: PowerShell -Command extraction ignores trailing argv elements, corrupting UTF-8 prefix step
What issue are you seeing?
extract_powershell_command (codex-rs/shell-command/src/powershell.rs:43-71) accepts a PowerShell invocation even when extra elements trail the -Command/-c script argument, silently ignoring them. Its one real consumer, prefix_powershell_script_with_utf8, then reconstructs the argv incorrectly for that shape, producing a corrupted, duplicated command array rather than either the intended prefixed command or a rejection.
This is a source-level finding from reading codex-rs/shell-command/src/powershell.rs, not something reproduced by running the CLI end-to-end — I don't have a working Rust toolchain on this machine, so I'm reporting it via a hand-traced control-flow walkthrough of the (pure, deterministic) functions involved.
// codex-rs/shell-command/src/powershell.rs
pub fn extract_powershell_command(command: &[String]) -> Option<(&str, &str)> {
...
let mut i = 1usize;
while i + 1 < command.len() {
let flag = &command[i];
if !POWERSHELL_FLAGS.contains(&flag.to_ascii_lowercase().as_str()) {
return None;
}
if flag.eq_ignore_ascii_case("-Command") || flag.eq_ignore_ascii_case("-c") {
let script = &command[i + 1];
return Some((shell, script)); // returns even if more args follow
}
i += 1;
}
None
}
pub fn prefix_powershell_script_with_utf8(command: &[String]) -> Vec<String> {
let Some((_, script)) = extract_powershell_command(command) else {
return command.to_vec();
};
...
let mut command: Vec<String> = command[..(command.len() - 1)] // drops only the LAST element
.iter().map(std::string::ToString::to_string).collect();
command.push(script);
command
}
For comparison, the sibling parser used for dangerous-command detection, command_safety/windows_dangerous_commands.rs::parse_powershell_invocation, explicitly rejects this exact shape at line 381: if idx + 2 != args.len() { return None; }. extract_powershell_command has no equivalent check, so the two parsers disagree on the same input.
What steps can reproduce the bug?
Hand trace (both functions are pure and deterministic, so this is exact):
input: ["powershell", "-Command", "Write-Host hi", "EXTRA"]
extract_powershell_command(input)
i=1: flag="-Command" (allowed, matches) -> script = input[2] = "Write-Host hi"
returns Some(("powershell", "Write-Host hi")) // input[3] "EXTRA" never inspected
prefix_powershell_script_with_utf8(input)
script = "Write-Host hi" -> prefixed = "<UTF8_OUTPUT_PREFIX>Write-Host hi"
command[..(4-1)] = command[..3] = ["powershell","-Command","Write-Host hi"]
.push(prefixed)
=> ["powershell","-Command","Write-Host hi","<UTF8_OUTPUT_PREFIX>Write-Host hi"]
The result is a 4-element array where the original unprefixed script is still present at index 2 and a UTF-8-prefixed duplicate is appended at index 3 — not a well-formed PowerShell invocation, and not what the function's own contract (replace the script with a prefixed version) intends.
What is the expected behavior?
extract_powershell_command should only recognize the invocation when the script is the last element (mirroring parse_powershell_invocation's idx + 2 != args.len() check), returning None for anything with trailing elements so prefix_powershell_script_with_utf8 falls back to returning the input unchanged — rather than constructing a malformed, duplicated command array.
Additional information
Why this matters beyond a cosmetic mismatch: the resulting array is not the command that any earlier safety analysis inspected (which looks only at command[i+1]), and it's not a well-formed PowerShell invocation either — the command actually about to run differs from what was analyzed. I have not proven whether the tool/model layer can produce a trailing-argument argv for a PowerShell call in practice; I'm flagging the control-flow defect itself (proven by direct trace) and the fact that the sibling safety parser already treats this exact shape as a case worth explicitly rejecting.
Suggested fix:
if flag.eq_ignore_ascii_case("-Command") || flag.eq_ignore_ascii_case("-c") {
if i + 2 != command.len() {
return None;
}
let script = &command[i + 1];
return Some((shell, script));
}
Suggested test: add a case to codex-rs/shell-command/src/powershell.rs's test module with a trailing extra argument, e.g. ["powershell", "-Command", "Write-Host hi", "extra"], asserting extract_powershell_command returns None (matching parse_powershell_invocation's behavior for the identical shape), and that prefix_powershell_script_with_utf8 returns the input unchanged.
Found via static code review (commit 7c3747941), not via a live repro — happy to provide more detail if useful.