codex review regression: review content is output to stderr not stdout

Open 💬 2 comments Opened Feb 22, 2026 by bpowers

What version of Codex CLI is running?

codex-cli 0.104.0

What subscription do you have?

ChatGPT Pro

Which model were you using?

got-5.3-codex

What platform is your computer?

Darwin 25.3.0 arm64 arm

What terminal emulator and version are you using (if applicable)?

Ghostty

What issue are you seeing?

on 2/5/2026, using the latest or a recent version of codex at that time, I could run codex --model 'gpt-5.3-codex-xhigh' review --base origin/main 2>/tmp/codex.out in my repo and get the final review contents in stdout. In a release since then, review contents have _stopped_ being sent to stdout, causing my automation to think there is nothing wrong with the current branch's work.

It looks like this is caused by

  • ReviewTask::run was always returning None after delegating review, so parent TurnComplete.last_agent_message was empty (codex-rs/core/src/tasks/review.rs:59).
  • codex-exec prints final stdout from TurnComplete.last_agent_message (codex-rs/exec/src/event_processor_with_human_output.rs:270, codex-rs/exec/src/event_processor_with_human_output.rs:819), so stdout ended up empty.

What steps can reproduce the bug?

run codex --model 'gpt-5.3-codex-xhigh' review --base origin/main 2>/tmp/codex.out see stdout return nothing, then tail -n 100 /tmp/codex.out and notice that there is a final "codex" output block with the review.

What is the expected behavior?

review ends up on stdout, so that codex review ... is usable as a command from other tools.

Additional information

this is the fix:

diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs
index dcb3b4cdc..27f6eea2f 100644
--- a/codex-rs/core/src/tasks/review.rs
+++ b/codex-rs/core/src/tasks/review.rs
@@ -69,13 +69,15 @@ impl SessionTask for ReviewTask {
             None => None,
         };
         if !cancellation_token.is_cancelled() {
-            exit_review_mode(session.clone_session(), output.clone(), ctx.clone()).await;
+            let assistant_message =
+                exit_review_mode(session.clone_session(), output.clone(), ctx.clone()).await;
+            return Some(assistant_message);
         }
         None
     }

     async fn abort(&self, session: Arc<SessionTaskContext>, ctx: Arc<TurnContext>) {
-        exit_review_mode(session.clone_session(), None, ctx).await;
+        let _ = exit_review_mode(session.clone_session(), None, ctx).await;
     }
 }

@@ -199,7 +201,7 @@ pub(crate) async fn exit_review_mode(
     session: Arc<Session>,
     review_output: Option<ReviewOutputEvent>,
     ctx: Arc<TurnContext>,
-) {
+) -> String {
     const REVIEW_USER_MESSAGE_ID: &str = "review_rollout_user";
     const REVIEW_ASSISTANT_MESSAGE_ID: &str = "review_rollout_assistant";
     let (user_message, assistant_message) = if let Some(out) = review_output.clone() {
@@ -250,7 +252,7 @@ pub(crate) async fn exit_review_mode(
                 id: Some(REVIEW_ASSISTANT_MESSAGE_ID.to_string()),
                 role: "assistant".to_string(),
                 content: vec![ContentItem::OutputText {
-                    text: assistant_message,
+                    text: assistant_message.clone(),
                 }],
                 end_turn: None,
                 phase: None,
@@ -262,4 +264,6 @@ pub(crate) async fn exit_review_mode(
     // materialize rollout persistence. Do this after emitting review output so
     // file creation + git metadata collection cannot delay client-facing items.
     session.ensure_rollout_materialized().await;
+
+    assistant_message
 }

happy to send a fix w/ a unit test if thats helpful

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗