Skip to content

Codex refusals are only observable in its stderr log, not in any structured output #1207

Description

@edwin-zvs

Follow-up from #1199.

When Codex's sandbox or approval policy refuses an action in headless mode, the only place that refusal appears is a formatted tracing line on stderr:

2026-08-04T13:29:07Z ERROR codex_core::tools::router: error=patch rejected: writing is blocked by read-only sandbox; rejected by user approval settings

#1199 detects it there, because there is nowhere else to look. I checked codex exec --json on 0.146.0 and its stdout carries only agent messages and the turn footer — the refusal is absent:

{"type":"thread.started",...}
{"type":"turn.started"}
{"type":"item.completed","item":{"type":"agent_message","text":"I'll create `index.html`..."}}
{"type":"item.completed","item":{"type":"agent_message","text":"I couldn't create `index.html` because the workspace is read-only..."}}
{"type":"turn.completed","usage":{...}}

Two consequences worth tracking:

  1. The parse is not a contract. It is a pretty-printed log line from a third-party tool: unversioned, undocumented, and free to change in any release with no compile-time signal. When it changes, detection stops and the tests stay green — the product regresses to not reporting sandbox misconfiguration at all, silently.
  2. The refusal cannot be correlated with the turn. A router refusal is action-level. Codex can be refused, work around it, and complete successfully; we latch the first refusal and report it on turn completion, so that turn is reported as having hit a permissions problem even though it finished. Detect Codex headless refusals from codex's own stderr log #1199 words the message to match what we actually know ("was refused an action… the turn may have completed anyway") rather than pretending otherwise, but the ambiguity is real and only a turn-correlated signal removes it.

Both go away if Codex emits the refusal as a structured event. Worth revisiting on each codex upgrade — check whether --json has grown a tool-error/refusal item type, and if so, switch to it and delete the log matcher. The rollout file's custom_tool_call_output (which does carry Script error: patch rejected…) may also be a better-correlated source than the tracing line; the adapter already reads rollouts for other purposes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions