From ae743d56b09f29b3a28a57a6c8aff7565e7b075a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 30 May 2025 14:41:55 -0700 Subject: [PATCH] feat: for `codex exec`, if PROMPT is not specified, read from stdin if not a TTY (#1178) This attempts to make `codex exec` more flexible in how the prompt can be passed: * as before, it can be passed as a single string argument * if `-` is passed as the value, the prompt is read from stdin * if no argument is passed _and stdin is a tty_, prints a warning to stderr that no prompt was specified an exits non-zero. * if no argument is passed _and stdin is NOT a tty_, prints `Reading prompt from stdin...` to stderr to let the user know that Codex will wait until it reads EOF from stdin to proceed. (You can repro this case by doing `yes | just exec` since stdin is not a TTY in that case but it also never reaches EOF). --- codex-rs/exec/src/cli.rs | 6 ++++-- codex-rs/exec/src/lib.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 1c2a9eb8..413fd23c 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -45,8 +45,10 @@ pub struct Cli { #[arg(long = "output-last-message")] pub last_message_file: Option, - /// Initial instructions for the agent. - pub prompt: String, + /// Initial instructions for the agent. If not provided as an argument (or + /// if `-` is used), instructions are read from stdin. + #[arg(value_name = "PROMPT")] + pub prompt: Option, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 8c94fe5d..6602213b 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -2,6 +2,7 @@ mod cli; mod event_processor; use std::io::IsTerminal; +use std::io::Read; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -40,6 +41,41 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any config_overrides, } = cli; + // Determine the prompt based on CLI arg and/or stdin. + let prompt = match prompt { + Some(p) if p != "-" => p, + // Either `-` was passed or no positional arg. + maybe_dash => { + // When no arg (None) **and** stdin is a TTY, bail out early – unless the + // user explicitly forced reading via `-`. + let force_stdin = matches!(maybe_dash.as_deref(), Some("-")); + + if std::io::stdin().is_terminal() && !force_stdin { + eprintln!( + "No prompt provided. Either specify one as an argument or pipe the prompt into stdin." + ); + std::process::exit(1); + } + + // Ensure the user knows we are waiting on stdin, as they may + // have gotten into this state by mistake. If so, and they are not + // writing to stdin, Codex will hang indefinitely, so this should + // help them debug in that case. + if !force_stdin { + eprintln!("Reading prompt from stdin..."); + } + let mut buffer = String::new(); + if let Err(e) = std::io::stdin().read_to_string(&mut buffer) { + eprintln!("Failed to read prompt from stdin: {e}"); + std::process::exit(1); + } else if buffer.trim().is_empty() { + eprintln!("No prompt provided via stdin."); + std::process::exit(1); + } + buffer + } + }; + let (stdout_with_ansi, stderr_with_ansi) = match color { cli::Color::Always => (true, true), cli::Color::Never => (false, false),