codex review regression: review content is output to stderr not stdout
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
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗