Claude Code stop-review gate returns empty output when finalMessage is missing
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:
stop-review-gate-hook.mjsrunscodex-companion.mjs task --json <prompt>.- The hook reads the subprocess JSON payload and parses
payload.rawOutput. - In
codex-companion.mjs,executeTaskRun()sets:
const rawOutput = typeof result.finalMessage === "string" ? result.finalMessage : "";
- If
runAppServerTurn()returns a result withfinalMessage: undefinedor any non-string value,rawOutputbecomes"". - The subprocess still exits cleanly, so the outer stop-hook sees a successful process run but an empty nested result.
- The stop-hook then converts empty
rawOutputinto 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:
runAppServerTurn()returned without a stringfinalMessage- 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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗