untrusted mode auto-approves path-qualified safe-basename commands (e.g. `./sed`) without a prompt
What issue are you seeing?
In untrusted approval mode (AskForApproval::UnlessTrusted), Codex auto-approves (runs without an approval prompt) any command it classifies as "known-safe". The known-safe classifier keys on the basename only, so a path-qualified argv[0] such as ./sed, /usr/bin/sed, or bin/grep is treated as known-safe and auto-approved — even though a path-qualified command bypasses PATH and can be an arbitrary, attacker-controlled local binary that merely shares a basename with a trusted utility (e.g. an attacker-dropped ./sed in the workspace).
Net effect: the auto-approve surface that untrusted mode intends only for standard PATH-resolved read-only utilities is silently widened to arbitrary local binaries. The command still runs inside the sandbox (Landlock/seccomp), so this is an approval-prompt bypass, not a sandbox escape — but it defeats the "ask before running untrusted code" intent of untrusted mode.
What steps can reproduce the bug?
With approval_policy = untrusted (UnlessTrusted):
- A workspace contains an attacker-controlled executable whose basename matches a known-safe utility, e.g.
./sed(any binary, not GNU sed). - The model proposes
./sed -n 1p file.txt(or/usr/bin/sed ...,bin/grep ...). - Codex auto-approves and runs it with no approval prompt, because the known-safe check matched the basename
sed.
Compare: a bare sed -n 1p file.txt is correctly auto-approved (the intended behavior). The defect is that the path-qualified form is treated identically.
What is the expected behavior?
A path-qualified argv[0] (one containing a path separator) should not be classified known-safe; it should fall through to the normal approval path (prompt in untrusted mode). Bare, PATH-resolved safe utilities should keep auto-approving.
Root cause (against main @ 45f6033)
codex-rs/shell-command/src/command_safety/is_safe_command.rs—is_safe_to_call_with_exectakescmd0 = command.first()and matchesexecutable_name_lookup_key(cmd0)against the known-safe utility list.executable_name_lookup_key(is_dangerous_command.rs) reducesargv[0]toPath::new(raw).file_name()— basename only — so./sed,/usr/bin/sed,bin/grepall collapse tosed/grepand match the safe arm.codex-rs/core/src/exec_policy.rs—render_decision_for_unmatched_command: whenis_known_safe && !used_complex_parsing && approval_policy == UnlessTrusted, it returnsDecision::Allow(no prompt). So the basename match becomes a no-prompt auto-approval.
High-level outline of a potential fix
In is_safe_to_call_with_exec, before the basename match, reject any path-qualified command:
#[cfg(not(windows))]
{
if cmd0.contains('/') {
return false; // path-qualified argv[0] is not known-safe -> normal approval path
}
}
- Bare PATH commands (
sed,git,cat, ...) are unaffected. - Windows full-path executables are already handled by the path-aware
windows_safe_commandssafelist, so the guard is#[cfg(not(windows))]. - Also covers
bash -lc "./sed ..."smuggling, sinceis_known_safe_commandruns each parsed sub-command throughis_safe_to_call_with_exec.
I've prototyped this change with regression tests (decision-layer + is_safe_command-layer, including the bash -lc case and a "bare commands still auto-approve" guard). The new tests fail on main and pass with the change, and the full codex-shell-command suite stays green (140/0). Happy to share the patch or open a PR if the team would like to pursue this — filing here first per the contributing guidelines.
Severity / scope
untrusted (UnlessTrusted) approval mode only. Approval-prompt bypass; the command still runs inside the sandbox. AskForApproval::Never/OnFailure are unaffected (they already auto-run and rely on the sandbox). Out of scope (pre-existing, basename-trust limitations): PATH poisoning of a bare command, and the dangerous-command (rm/sudo) side which also matches on basename.