untrusted mode auto-approves path-qualified safe-basename commands (e.g. `./sed`) without a prompt

Open 💬 0 comments Opened Jun 17, 2026 by warmjademe

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):

  1. A workspace contains an attacker-controlled executable whose basename matches a known-safe utility, e.g. ./sed (any binary, not GNU sed).
  2. The model proposes ./sed -n 1p file.txt (or /usr/bin/sed ..., bin/grep ...).
  3. 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.rsis_safe_to_call_with_exec takes cmd0 = command.first() and matches executable_name_lookup_key(cmd0) against the known-safe utility list.
  • executable_name_lookup_key (is_dangerous_command.rs) reduces argv[0] to Path::new(raw).file_name()basename only — so ./sed, /usr/bin/sed, bin/grep all collapse to sed/grep and match the safe arm.
  • codex-rs/core/src/exec_policy.rsrender_decision_for_unmatched_command: when is_known_safe && !used_complex_parsing && approval_policy == UnlessTrusted, it returns Decision::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_commands safelist, so the guard is #[cfg(not(windows))].
  • Also covers bash -lc "./sed ..." smuggling, since is_known_safe_command runs each parsed sub-command through is_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.

View original on GitHub ↗