Skip to content

codex:codex-rescue subagent returns stub instead of actual task output #324

Open
tgkim-openerd/codex-plugin-cc
#1

Description

@taibaran

Symptom

When a parent agent calls subagent_type: codex:codex-rescue expecting a substantive answer, the wrapper returns within seconds with text like:

"Codex is running the review in the background. You will be notified when the output is ready."

The actual task spawned by the wrapper continues in a separate background job; the parent agent never gets the real result because it was subscribed to the wrapper agent (which exited immediately), not to the orphaned background job that holds the real Codex output.

Discovered while running a multi-reviewer cycle against a third-party plugin: the Codex rescue stub returned within seconds while the real task-worker continued for ~18 minutes and died without producing a verdict.

Reproduction

From a parent agent (Claude Code session):

Agent({
  subagent_type: "codex:codex-rescue",
  prompt: "Review <some code>... return a structured verdict."
})

Expected: the wrapper waits for Codex, returns the verdict.
Observed: the wrapper returns within seconds with "Codex is running the review in the background."

Root cause (3 stacked issues)

Issue 1: task subcommand is async-by-design

In scripts/codex-companion.mjs:643:

const child = spawn(process.execPath, [scriptPath, "task-worker", "--cwd", cwd, "--job-id", jobId], {
    ...
    detached: true,

task spawns a detached task-worker and returns a job ID. Without --wait, the call returns immediately and the actual reasoning happens in the spawned worker.

Issue 2: rescue skill instructions STRIP --wait

skills/codex-cli-runtime/SKILL.md says:

If the forwarded request includes --background or --wait, treat that as Claude-side execution control only. Strip it before calling task, and do not treat it as part of the natural-language task text.

So even if the parent passes --wait, the rescue wrapper removes it. The wrapper never adds --wait either — the task call always runs in detached/async mode.

Issue 3: Claude Code's Bash tool further auto-backgrounds long commands

When the wrapper agent's Claude calls Bash(node codex-companion.mjs task "..."), the Bash tool itself auto-decides to background the command. The wrapper sees only the Bash auto-background acknowledgement:

"Command running in background with ID: bodpz7pmd. Output is being written to: /private/tmp/claude-501/.../tasks/bodpz7pmd.output"

…and forwards that to its parent verbatim, per the rescue skill's "return stdout exactly as-is" rule.

Real Codex output goes to bodpz7pmd.output, which the parent never knows about.

Evidence from a real session

Auto-background output landed at:

/private/tmp/claude-501/.../tasks/bodpz7pmd.output

23 lines of genuine Codex progress (excerpts):

[codex] Starting Codex task thread.
[codex] Thread ready (019e2b02-ded1-7e20-a0f3-b584b6b6b3e7).
[codex] Turn started.
[codex] Assistant message captured: I'll try the requested checkout first.
[codex] Searching:
[codex] Assistant message captured: The direct clone is blocked by the read-only sandbox: `/tmp/grok-v060-codex` cannot be created
[codex] Calling codex_apps/github_fetch_file.
[codex] Tool codex_apps/github_fetch_file completed.
...

Codex was actually working — hit a read-only sandbox blocking git clone, pivoted to GitHub API fetches, ran for ~18 minutes, then died without producing a final verdict. The parent agent had no visibility into any of this.

Suggested fixes (any one is sufficient)

Fix A (cleanest) — drop the strip---wait rule from the skill

In skills/codex-cli-runtime/SKILL.md, change the execution rule from:

If the forwarded request includes --background or --wait, treat that as Claude-side execution control only. Strip it before calling task.

to:

Always pass --wait to task from inside the rescue subagent — the rescue contract is "block until done and return the final result." If the user also passed --background, strip it (rescue is never async).

This makes the wrapper synchronous from the parent's point of view, which is what the rescue subagent contract implies.

Fix B — task defaults to synchronous when invoked from a rescue context

Detect inside codex-companion.mjs::handleTask that the caller is non-interactive (no TTY, or process.env.CODEX_RESCUE_MODE=1) and skip the detached spawn — just await the worker inline.

Fix C — wrapper polls until done

The skill instructs the wrapper to call task → capture job ID → loop on status --wait <id> until terminal status → return final result. More changes to the skill, but does not require changes to task.

Fix A is the smallest delta and matches what users actually expect from a rescue subagent.

Related

I found the same pattern in taibaran/gemini-plugin-cc (same async-task + strip---wait design). Opening a parallel issue there.

Cross-link: LovelaceLoom/gemini-plugin-cc#3


Reported while building taibaran/grok-plugin-cc, a third Claude-Code plugin modeled on this one — context for why the multi-reviewer flow surfaced the bug.

Activity

  1. gitctor commented on May 27, 2026

    @gitctor

    Confirming this still repros on plugin 1.0.4 — independent observation 2026-05-27, macOS, Claude Code CLI. Posting both as a "+1 with data" and as a 4th fix option that's narrower than A/B/C.

    Independent repro

    Parent Claude in a long-running session spawned codex:codex-rescue for an architecture review of a Minecraft modpack repo (~9 minutes of codex work, write-mode disabled). The subagent's Bash call to codex-companion.mjs task ... did not pass timeout, so the harness default (120000ms foreground) applied. At the 2-minute mark the harness auto-promoted the Bash call to background mode and returned a synthetic "Command running in background with ID: …" line.

    The subagent emitted text equivalent to "I'll be notified when it finishes" and exited. Codex itself completed cleanly ~7 minutes later — the synthesis is preserved in the task .output file and in ~/.codex/sessions/2026/05/27/rollout-*.jsonl, but the parent Claude never saw it. From the parent's perspective: rescue returned a stub, no error, and the actual verdict was orphaned.

    This matches Issue 3 in the original report exactly. The only difference vs your repro is that I didn't pass --background anywhere — the strip---wait rule means foreground is the contract, but the harness still backgrounds the Bash call once it crosses 2 minutes. So this bug fires on every codex-rescue invocation that takes longer than 2 minutes, regardless of what flags the parent passes.

    Fix D — agent-side timeout on the Bash call

    The three fixes you listed change the companion (B), the skill (A), or add polling logic (C). There's a smaller delta that doesn't touch the companion or the skill: require the agent to pass timeout: 600000 (10 min, Claude Code's Bash max) on the call itself.

    In plugins/codex/agents/codex-rescue.md, the forwarding rules become:

    • Use exactly one Bash call to invoke node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task ....
    • Always pass timeout: 600000 on the Bash tool call. This is Claude Code's Bash max and keeps the call foreground for the full codex run, so stdout returns synchronously.
    • Never pass run_in_background: true. The harness auto-promotes long Bash calls to background mode at 2 min unless an explicit timeout extends the foreground window; combined with the "return stdout exactly as-is" rule downstream, that produces the orphan-task stub described in this issue.
    • For tasks the user explicitly requests as --background (rare; codex runs > 10 min), pass --background to codex-companion itself — the companion returns a job ID synchronously, and the agent surfaces that job ID + a pointer to /codex:status polling. Do not rely on Claude Code's Bash background mode for this.

    (Replaces the current "prefer background execution if the task looks complicated, open-ended, multi-step, or likely to keep Codex running for a long time" heuristic at agents/codex-rescue.md:24, which interacts badly with the harness auto-promotion.)

    Why this might be the right call

    • No companion changes. task keeps its current async-by-design semantics. The --wait flag and the strip---wait skill rule stay as-is.
    • Self-contained. The change is one paragraph in the agent definition. Ships in a 1.0.5 patch.
    • Closes the 95% case. Almost every rescue I've seen completes in < 10 min. The 10-min foreground window covers the realistic distribution.
    • The escape hatch for > 10 min runs is explicit. If a user actually needs a 20-minute codex run, they pass --background and get a job ID — which is the codex-companion mode that was already designed for this case but never reachable through the rescue agent.

    Where Fix D doesn't help

    It doesn't help if a single rescue genuinely runs past 10 minutes without the user knowing in advance. For that case, A or B is still needed. But for the orphan-task class described in this issue, D is the smallest fix.

    Combination

    Fix D + the fix from #350 (forward stderr + exit code on Bash failure) closes both the silent-stub path (this issue) and the silent-empty-output path (#350) without touching the companion. Both are agent-prompt-only changes.

    What's deployed locally as a workaround

    I shipped a local override at ~/.claude/agents/codex-rescue.md that takes precedence over the plugin's copy and does the three changes above. Pattern has held for the rest of the day across 4 more rescues, all of which surfaced full codex output to the parent. Happy to PR the same as a plugin change if a maintainer signals which of A/B/C/D you'd prefer to land.

  2. mychaelconnolly commented on Jun 27, 2026

    @mychaelconnolly

    Reproced this one on macOS (darwin 25.5), plugin 1.0.4, codex-cli 0.142.2 — with an extra symptom worth noting.

    Parent Agent({subagent_type: "codex:codex-rescue"}) returned the "running in background, will notify" stub; the real task-worker ran ~25 min and died without a verdict (as described here). Additionally: because the worker roots at the session cwd (resolveCommandCwd → process.cwd(), per #380), and our target repo was not the session cwd, the orphaned worker reviewed the wrong tree — its transcript cited files that don't exist in the target repo (confabulated paths).

    So the stub-then-die failure (#324) compounds with the cwd default (#380): the run looks like a stall, and any partial output is ungrounded. A loud "task-worker exited without result" + honoring a per-task target cwd would both help.

  3. ElliotDrel commented on Jul 14, 2026

    @ElliotDrel

    Adding a data point from the other failure mode of the same wrapper (cross-ref #370).

    Same root cause you describe (the rescue wrapper's task path + Bash auto-backgrounding), but the observable symptom here was total output loss rather than a stub:

    • Called subagent_type: codex:codex-rescue from a parent Claude Code session with an adversarial multi-file review prompt.
    • The wrapper's single foreground Bash call ran until Claude Code's 10-min ceiling and was killed; the parent received "The Bash call timed out after 10 minutes" and nothing else — no stub, no job id, no .output path to recover from.

    So between this issue and #370, the codex-rescue wrapper can fail in two ways depending on whether Bash backgrounds the call: (a) return a stub pointing at an orphaned job the parent never subscribed to (#324), or (b) return nothing at all when the foreground call is killed at 10:00 (#370). Both leave the parent with no usable Codex output. A fix that makes the wrapper always surface the real job id + output path (even on timeout/stub) would cover both.

    Environment: Windows 11, Claude Code, codex plugin 1.0.6, Codex CLI 0.144.3.

  4. Captain-Bacon commented on Aug 31, 2026

    @Captain-Bacon

    Same symptom, and two compounding notes worth adding
    v 1.0.6
    Claude Code on macOS (darwin), Node 22

    1. Chasing the missing result made it worse. When the main agent gets the "running in the background" stub and messages the rescue subagent to resume/collect, each nudge spawns a fresh job whose log contains only Starting Codex Resume. in the /state//jobs/ folder, while the original job keeps running untouched. In my case the chase ended with the original, still-running review being cancelled by cleanup - the full round-trip cost on just that one instance was one lost ~15-minute review plus a re-run.

    2. It overlaps with #639. While the original job is still running, result <jobId> throws No job found for "<jobId>" (the finished-only filter, per that issue), so the natural recovery path also dead-ends. The main agent can see the job in status, but neither the wrapper nor result will hand over anything until it finishes. Nothing tells the wrapper to wait.

    Workaround
    Patched the local skills/codex-cli-runtime/SKILL.md to add:

    ALWAYS pass --wait to task: the rescue subagent's only deliverable is the task's stdout, and without --wait the helper backgrounds the job and returns nothing.

    With that in place (or --wait written into the dispatch prompt), every rescue since has returned the actual Codex output. Since the cache is versioned, the patch will be overwritten on next version update, so a fix in the shipped skill (or defaulting task to foreground when invoked from the rescue skill, per #395's proposal) would be very welcome.

  5. germanelias13 commented on Nov 4, 2026

    @germanelias13

    A data point on Issue 3 above, from plugin 1.0.6 and Claude Code 2.1.317 on Windows 11 (2026-11-04).

    In two runs the agent ran task --effort medium --prompt-file <path> in the foreground with timeout: 600000. At about 180 seconds the Bash tool still moved the call to the background, so a long timeout doesn't prevent this here (#634 reports 120 seconds on Linux). The agent then handed back a report with no result. The companion was not killed: it ran to the end, and a second report with Codex's answer followed. When to expect it depends on the caller. In a third run the caller was busy with other tool calls, and the second report came only when the caller's own turn ended, 21 minutes after the job had finished, though the answer had been in the Bash output file all along. In a fourth run the caller was idle, and it came 19 seconds after the job finished.

    So the first report is not a failure, but nothing says so. agents/codex-rescue.md says to return the stdout as-is (line 41) and to return nothing if the call fails (line 42), and neither fits a call that is still running. A caller told to carry on if Codex fails can read that report as a failure, then skip the step or start a second run.

    Suggested rule for the agent: when the call moves to the background, say that the job is still running, that this report is not the result, that a final report will follow, and where the output is in the meantime. Then do nothing else. This doesn't replace the fixes discussed above for getting the result back. It only keeps the caller from misreading the first report.

  6. smontoy1-vasu commented on Nov 4, 2026

    @smontoy1-vasu

    Second sighting, twice in one session. codex plugin 1.0.6, codex-cli 0.166.0, Claude Code 2.1.316, macOS 26.6.2 (arm64). Both times the codex:codex-rescue subagent, launched in the background with an xhigh read-only review prompt, returned after about 2-3 minutes with only the [codex] progress lines and no final answer, while the companion job kept running. Reading the result needed codex-companion.mjs status and result by hand. Workaround that worked: running the same prompt through codex exec -s read-only -o <file> directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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