Skip to content

cx worktree runs: worker dies on 'writing outside of the project', no verdict.json written, finished work looks like a failure #167

Description

@rbinar

Summary

With the cx (Codex) backend in worktree mode, the worker's turn dies with
patch rejected: writing outside of the project; rejected by user approval settings.
The runner then reports worktree runner failed (code 1) and writes no
verdict.json
— but the work itself is usually complete and sitting in the
worktree
. An orchestrator that trusts the exit code concludes the run failed
and discards finished work; one that trusts a stale verdict.json from an
earlier run reads the wrong verdict entirely.

Hit 2 out of 3 runs in one session (2026-08-22).

Reproduction

cli-dispatch-run --backend cx --cwd <repo> \
  --prompt "Read the file /private/tmp/.../BRIEF.md and execute it exactly as written." \
  --verify 'sh backend/scripts/verify_backend_in_worktree.sh'

The brief lives outside the repo (a scratchpad path). Reading it is fine; the
failure comes later, when the worker tries to WRITE somewhere outside the
project — a temp file, or a report next to the brief.

Observed output (trimmed):

cx-stream → Codex (OpenAI Codex CLI) worker
  cwd:     /tmp/cx-wt-sarltQ
  sandbox: workspace-write (network: on)
2026-08-22T01:17:37Z ERROR codex_core::tools::router: error=patch rejected: writing outside of the project; rejected by user approval settings
cx-stream: codex turn failed: ...
cli-dispatch-run: worktree runner failed (code 1)

Meanwhile:

$ git -C /tmp/cx-wt-sarltQ status --short
 M CLAUDE.md
 M backend/app/api/v1/rewards.py
 M backend/app/services/reward_service.py
 ?? backend/app/services/reference_anchor.py
 ?? backend/tests/test_reward_engine_routing.py
 ?? worker-report.json

The task was done. Running the --verify command by hand in that worktree
passed (1881/1882 tests; the one failure was a genuine finding, unrelated to
this bug).

Why it matters

--verify never runs, so the run produces no verdict at all — not a failing
one, none. The two failure modes look identical from outside:

  • worker died having done nothing → correctly discarded
  • worker died having finished → work silently thrown away

Both print code 1. The only way I found to tell them apart is inspecting the
worktree manually after every run, which is exactly the babysitting the
deterministic runner is meant to remove.

There is a sharper version: on one run the wrapper reported success while the
worker had changed nothing, because a --verify on an unmodified tree
passes trivially. Combined with this issue, exit code alone is not a signal in
either direction.

Suggestions

  1. Always write verdict.json, including when the worker turn dies — with
    the diffstat and a state that distinguishes "worker died, tree dirty" from
    "worker died, tree clean". That alone makes the outcome recoverable.
  2. Run --verify anyway when the tree is dirty after a worker death, and
    report the result. If it passes, the run arguably succeeded.
  3. Consider a distinct exit code for "worker died but left changes", so a
    caller can branch without parsing logs.
  4. Possibly: pass the brief's directory to the sandbox as writable, or document
    that briefs should be copied INTO the repo first. Telling the worker in the
    brief not to write outside the repo did not prevent it (tried; it still
    attempted it on the very next run).

Environment

  • backend: cx (Codex CLI), model gpt-5.6-sol, sandbox workspace-write (network on)
  • mode: worktree (--cwd was a normal checkout, runner created /tmp/cx-wt-*)
  • also set: CLI_DISPATCH_ALLOW_CONCURRENT_EDITS=1 on two of the three runs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions