Sandbox-denial provenance is dropped from model-facing exec results

Open 💬 0 comments Opened Aug 28, 2026 by YuriyKrasilnikov

What issue are you seeing?

Codex can correctly recognize that an exec attempt was denied by its sandbox, but this classification is lost before the result is sent back to the model.

The model receives an ordinary command result containing an exit code and raw stdout/stderr. It is not told that Codex already classified the result as a sandbox denial. In the unified-exec path, the reconstructed tool result is also serialized with success: true.

This collapses two different facts:

  1. the attempt failed inside the selected sandbox;
  2. the command, resource, service, or host is actually unavailable.

As a result, the model may incorrectly diagnose the target system. For example, a Docker socket denial can be interpreted as an unavailable Docker daemon even when the identical command succeeds through the configured local escalation path.

The problem is not Docker-specific. It applies to filesystem writes, Unix sockets, package caches, Git metadata, and other operations blocked by the execution sandbox.

A downstream consequence is that the agent may search for another execution environment instead of retrying the same operation through the configured approval path. That remote trust-boundary behavior is separately tracked in #32919; this report concerns the earlier loss of causal information.

Environment

  • Source revision inspected: f98649cde9402d8b055154d1121248cc72163947
  • Locally installed CLI: codex-cli 0.150.1
  • Platform used for source inspection: Linux 6.18.33.2-microsoft-standard-WSL2 x86_64
  • Model: not model-specific; the classification is lost while constructing the tool result before the next model request

What steps can reproduce the bug?

A deterministic reproduction already exists in the repository:

  1. Run the sandbox_denied_exec_command_returns_original_output integration test with a read-only permission profile.
  2. The command attempts a write that the sandbox denies.
  3. Codex internally produces UnifiedExecError::SandboxDenied.
  4. Inspect the function_call_output captured for the next model request.
  5. Observe that it contains the exit code and original denial text, but no model-visible sandbox-denial classification.

A user-level reproduction:

  1. Use on-request approval with a restricted sandbox.
  2. Choose a command that succeeds for the host user but is blocked inside the sandbox, such as access to a local Docker Unix socket.
  3. Run the command with default sandbox permissions.
  4. Observe permission denied.
  5. Retry the identical local command through the configured escalation path and observe that it succeeds.
  6. In the first result, observe that the model was given only the ordinary process output and could therefore attribute the failure to Docker or the target resource rather than to the Codex sandbox.

What is the expected behavior?

When Codex classifies a tool attempt as sandbox-denied, that fact should remain explicit in every model-visible representation.

The result should:

  • preserve the original stdout/stderr;
  • identify the sandbox as the boundary that denied the attempt;
  • make clear, structurally or unambiguously, that the output does not prove the command or target is unavailable outside that sandbox;
  • preserve equivalent semantics in direct tool mode and code mode;
  • distinguish a sandbox denial from an ordinary successful tool invocation.

The exact wire shape is an implementation decision. The required invariant is that sandbox-denial provenance must not be discarded before the next model step.

Root-cause analysis

Current main already carries the classification through most of the execution path:

  • The sandboxing crate recognizes strings including permission denied, operation not permitted, and read-only file system:

https://github.com/openai/codex/blob/f98649cde9402d8b055154d1121248cc72163947/codex-rs/sandboxing/src/denial.rs#L13-L71

  • Unified exec converts that evidence into UnifiedExecError::SandboxDenied:

https://github.com/openai/codex/blob/f98649cde9402d8b055154d1121248cc72163947/codex-rs/core/src/unified_exec/process.rs#L290-L321

  • The exec-command handler catches SandboxDenied, extracts only the aggregated output, and reconstructs an ordinary ExecCommandToolOutput without retaining the denial classification:

https://github.com/openai/codex/blob/f98649cde9402d8b055154d1121248cc72163947/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs#L382-L406

  • ExecCommandToolOutput serializes the reconstructed result with success: Some(true):

https://github.com/openai/codex/blob/f98649cde9402d8b055154d1121248cc72163947/codex-rs/core/src/tools/context.rs#L341-L382

  • The existing integration test verifies preservation of the raw output but does not require preservation of the sandbox-denial classification:

https://github.com/openai/codex/blob/f98649cde9402d8b055154d1121248cc72163947/codex-rs/core/tests/suite/tools.rs#L650-L732

A possible implementation direction is to retain a typed sandbox-denial outcome through ExecCommandToolOutput and its direct/code-mode serializers, while preserving the original process output. The existing integration test can then assert both properties.

Related issues

  • #6251: the model failed to request escalation after a sandbox-related failure.
  • #25076: Codex App denied access to a Docker Unix socket allowed by CLI.
  • #32919: a sandbox-denied operation crossed into an MCP-backed remote executor.
  • #21304: generic permission-denied output gave no indication that the sandbox was responsible.

This report is intentionally scoped to diagnostic/provenance loss. It does not claim that the serializer defect by itself bypasses sandbox enforcement.

View original on GitHub ↗