Claude Code stop-review gate returns empty output when finalMessage is missing

Resolved 💬 1 comment Opened Apr 23, 2026 by Dfskid Closed Apr 23, 2026

Summary

The Codex Claude Code stop-review gate can return a nested payload with status: 1 and rawOutput: "", which causes Claude Code to block with a generic "no final output" message and no actionable diagnosis.

Observed stop-hook failure surface:

{ "status": 1, "rawOutput": "" }

This appears to happen when the underlying Codex app-server turn completes without a string finalMessage, and the plugin turns that into an empty string instead of a diagnostic.

Symptom

In a Claude Code stop-hook flow, the first Codex review run returned a real finding. A later run returned no actionable content and produced a block decision equivalent to:

The stop-time Codex review task returned no final output. Run /codex:review --wait manually or bypass the gate.

That gives the operator no clue whether the turn failed, produced reasoning-only output, hit a rate limit, or returned malformed structured data.

Diagnosis

The problem appears to be in the Claude Code plugin cache path:

~/.claude/plugins/cache/openai-codex/codex/1.0.2/scripts/
  stop-review-gate-hook.mjs
  codex-companion.mjs

From reading the scripts:

  1. stop-review-gate-hook.mjs runs codex-companion.mjs task --json <prompt>.
  2. The hook reads the subprocess JSON payload and parses payload.rawOutput.
  3. In codex-companion.mjs, executeTaskRun() sets:
const rawOutput = typeof result.finalMessage === "string" ? result.finalMessage : "";
  1. If runAppServerTurn() returns a result with finalMessage: undefined or any non-string value, rawOutput becomes "".
  2. The subprocess still exits cleanly, so the outer stop-hook sees a successful process run but an empty nested result.
  3. The stop-hook then converts empty rawOutput into a generic blocking decision.

Why this is bad

This collapses several distinct failure modes into the same empty-output case:

  • turn completed with reasoning items only
  • nested Codex turn failed and exposed a non-zero status internally
  • app-server returned an error but no final text message
  • stderr or structured diagnostics existed but were not surfaced

The operator gets a hard block with no remediation details.

Likely root cause

The most likely combination is:

  1. runAppServerTurn() returned without a string finalMessage
  2. the nested status or error fields were not promoted into rawOutput

So the plugin loses the real reason before the stop-hook decides what to do.

Suggested fix

In codex-companion.mjs, harden the fallback path so an empty finalMessage becomes an explicit diagnostic instead of "".

Something along these lines:

const rawOutput = typeof result.finalMessage === "string" && result.finalMessage.trim() !== ""
  ? result.finalMessage
  : (result.error?.message
      ? `CODEX_TURN_ERROR: ${result.error.message}`
      : result.reasoningSummary
        ? `CODEX_NO_FINAL_MESSAGE (reasoning only): ${result.reasoningSummary}`
        : result.stderr
          ? `CODEX_TURN_STDERR: ${result.stderr}`
          : "CODEX_TURN_NO_OUTPUT");

Then in stop-review-gate-hook.mjs, treat those CODEX_* markers as diagnostics and degrade to a warning or explicit inconclusive state rather than a generic block with no context.

Extra note

This does not look like a NanoClaw bug or a repo-local issue. The failure lives entirely inside the Codex Claude Code plugin path, so it seems best fixed upstream instead of patched in plugin cache.

Environment

  • Claude Code stop-hook flow
  • Codex Claude Code plugin cache path observed locally at ~/.claude/plugins/cache/openai-codex/codex/1.0.2/
  • Nested task mode invoking codex-companion.mjs task --json ...

If useful, I can also provide a more exact repro transcript from the stop-hook flow.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗