Files
codex/prs/bolinfest/PR-1929.md
2025-09-02 15:17:45 -07:00

1381 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PR #1929: [config] Onboarding flow with persistence
- URL: https://github.com/openai/codex/pull/1929
- Author: dylan-hurd-oai
- Created: 2025-08-07 07:20:33 UTC
- Updated: 2025-08-07 16:27:47 UTC
- Changes: +434/-189, Files changed: 13, Commits: 20
## Description
## Summary
In collaboration with @gpeal: upgrade the onboarding flow, and persist user settings.
## Testing
Tested a few scenarios locally
## Full Diff
```diff
diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index eabd9f35db..4eddf7bd7b 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -708,6 +708,7 @@ dependencies = [
"tokio-test",
"tokio-util",
"toml 0.9.4",
+ "toml_edit 0.23.3",
"tracing",
"tree-sitter",
"tree-sitter-bash",
@@ -3273,7 +3274,7 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
- "toml_edit",
+ "toml_edit 0.22.27",
]
[[package]]
@@ -4800,7 +4801,7 @@ dependencies = [
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
- "toml_edit",
+ "toml_edit 0.22.27",
]
[[package]]
@@ -4849,11 +4850,24 @@ dependencies = [
"winnow",
]
+[[package]]
+name = "toml_edit"
+version = "0.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17d3b47e6b7a040216ae5302712c94d1cf88c95b47efa80e2c59ce96c878267e"
+dependencies = [
+ "indexmap 2.10.0",
+ "toml_datetime 0.7.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
[[package]]
name = "toml_parser"
-version = "1.0.1"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
+checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
dependencies = [
"winnow",
]
diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml
index e9d6970ded..006a218abf 100644
--- a/codex-rs/core/Cargo.toml
+++ b/codex-rs/core/Cargo.toml
@@ -36,6 +36,7 @@ sha1 = "0.10.6"
shlex = "1.3.0"
similar = "2.7.0"
strum_macros = "0.27.2"
+tempfile = "3"
thiserror = "2.0.12"
time = { version = "0.3", features = ["formatting", "local-offset", "macros"] }
tokio = { version = "1", features = [
@@ -47,6 +48,7 @@ tokio = { version = "1", features = [
] }
tokio-util = "0.7.14"
toml = "0.9.4"
+toml_edit = "0.23.3"
tracing = { version = "0.1.41", features = ["log"] }
tree-sitter = "0.25.8"
tree-sitter-bash = "0.25.0"
diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs
index 081306dab1..723ee5f817 100644
--- a/codex-rs/core/src/config.rs
+++ b/codex-rs/core/src/config.rs
@@ -22,13 +22,17 @@ use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
+use tempfile::NamedTempFile;
use toml::Value as TomlValue;
+use toml_edit::DocumentMut;
/// Maximum number of bytes of the documentation that will be embedded. Larger
/// files are *silently truncated* to this size so we do not take up too much of
/// the context window.
pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB
+const CONFIG_TOML_FILE: &str = "config.toml";
+
/// Application configuration loaded from disk and merged with overrides.
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
@@ -191,10 +195,28 @@ impl Config {
}
}
+pub fn load_config_as_toml_with_cli_overrides(
+ codex_home: &Path,
+ cli_overrides: Vec<(String, TomlValue)>,
+) -> std::io::Result<ConfigToml> {
+ let mut root_value = load_config_as_toml(codex_home)?;
+
+ for (path, value) in cli_overrides.into_iter() {
+ apply_toml_override(&mut root_value, &path, value);
+ }
+
+ let cfg: ConfigToml = root_value.try_into().map_err(|e| {
+ tracing::error!("Failed to deserialize overridden config: {e}");
+ std::io::Error::new(std::io::ErrorKind::InvalidData, e)
+ })?;
+
+ Ok(cfg)
+}
+
/// Read `CODEX_HOME/config.toml` and return it as a generic TOML value. Returns
/// an empty TOML table when the file does not exist.
-fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
- let config_path = codex_home.join("config.toml");
+pub fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
+ let config_path = codex_home.join(CONFIG_TOML_FILE);
match std::fs::read_to_string(&config_path) {
Ok(contents) => match toml::from_str::<TomlValue>(&contents) {
Ok(val) => Ok(val),
@@ -214,6 +236,35 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
}
}
+/// Patch `CODEX_HOME/config.toml` project state.
+/// Use with caution.
+pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Result<()> {
+ let config_path = codex_home.join(CONFIG_TOML_FILE);
+ // Parse existing config if present; otherwise start a new document.
+ let mut doc = match std::fs::read_to_string(config_path.clone()) {
+ Ok(s) => s.parse::<DocumentMut>()?,
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
+ Err(e) => return Err(e.into()),
+ };
+
+ // Mark the project as trusted. toml_edit is very good at handling
+ // missing properties
+ let project_key = project_path.to_string_lossy().to_string();
+ doc["projects"][project_key.as_str()]["trust_level"] = toml_edit::value("trusted");
+
+ // ensure codex_home exists
+ std::fs::create_dir_all(codex_home)?;
+
+ // create a tmp_file
+ let tmp_file = NamedTempFile::new_in(codex_home)?;
+ std::fs::write(tmp_file.path(), doc.to_string())?;
+
+ // atomically move the tmp file into config.toml
+ tmp_file.persist(config_path)?;
+
+ Ok(())
+}
+
/// Apply a single dotted-path override onto a TOML value.
fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) {
use toml::value::Table;
@@ -350,6 +401,13 @@ pub struct ConfigToml {
/// The value for the `originator` header included with Responses API requests.
pub internal_originator: Option<String>,
+
+ pub projects: Option<HashMap<String, ProjectConfig>>,
+}
+
+#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
+pub struct ProjectConfig {
+ pub trust_level: Option<String>,
}
impl ConfigToml {
@@ -377,6 +435,36 @@ impl ConfigToml {
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
}
}
+
+ pub fn is_cwd_trusted(&self, resolved_cwd: &Path) -> bool {
+ let projects = self.projects.clone().unwrap_or_default();
+
+ projects
+ .get(&resolved_cwd.to_string_lossy().to_string())
+ .map(|p| p.trust_level.clone().unwrap_or("".to_string()) == "trusted")
+ .unwrap_or(false)
+ }
+
+ pub fn get_config_profile(
+ &self,
+ override_profile: Option<String>,
+ ) -> Result<ConfigProfile, std::io::Error> {
+ let profile = override_profile.or_else(|| self.profile.clone());
+
+ match profile {
+ Some(key) => {
+ if let Some(profile) = self.profiles.get(key.as_str()) {
+ return Ok(profile.clone());
+ }
+
+ Err(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ format!("config profile `{key}` not found"),
+ ))
+ }
+ None => Ok(ConfigProfile::default()),
+ }
+ }
}
/// Optional overrides for user configuration (e.g., from CLI flags).
diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs
index c789798bcd..9008ad307d 100644
--- a/codex-rs/core/src/protocol.rs
+++ b/codex-rs/core/src/protocol.rs
@@ -139,7 +139,6 @@ pub enum AskForApproval {
/// Under this policy, only "known safe" commands—as determined by
/// `is_safe_command()`—that **only read files** are autoapproved.
/// Everything else will ask the user to approve.
- #[default]
#[serde(rename = "untrusted")]
#[strum(serialize = "untrusted")]
UnlessTrusted,
@@ -151,6 +150,7 @@ pub enum AskForApproval {
OnFailure,
/// The model decides when to ask the user for approval.
+ #[default]
OnRequest,
/// Never ask the user to approve commands. Failures are immediately returned
diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs
index 5d7f1281ee..6ed57898b2 100644
--- a/codex-rs/exec/src/lib.rs
+++ b/codex-rs/exec/src/lib.rs
@@ -181,7 +181,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
event_processor.print_config_summary(&config, &prompt);
if !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()) {
- eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified.");
+ eprintln!("Not inside a trusted directory and --skip-git-repo-check was not specified.");
std::process::exit(1);
}
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
index 1ba8883b0b..d71a331e3b 100644
--- a/codex-rs/tui/src/app.rs
+++ b/codex-rs/tui/src/app.rs
@@ -13,7 +13,6 @@ use codex_core::config::Config;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
-use codex_core::util::is_inside_git_repo;
use color_eyre::eyre::Result;
use crossterm::SynchronizedUpdate;
use crossterm::event::KeyCode;
@@ -71,7 +70,7 @@ pub(crate) struct App<'a> {
/// deferred until after the Git warning screen is dismissed.
#[derive(Clone, Debug)]
pub(crate) struct ChatWidgetArgs {
- config: Config,
+ pub(crate) config: Config,
initial_prompt: Option<String>,
initial_images: Vec<PathBuf>,
enhanced_keys_supported: bool,
@@ -81,8 +80,8 @@ impl App<'_> {
pub(crate) fn new(
config: Config,
initial_prompt: Option<String>,
- skip_git_repo_check: bool,
initial_images: Vec<std::path::PathBuf>,
+ show_trust_screen: bool,
) -> Self {
let (app_event_tx, app_event_rx) = channel();
let app_event_tx = AppEventSender::new(app_event_tx);
@@ -134,9 +133,7 @@ impl App<'_> {
}
let show_login_screen = should_show_login_screen(&config);
- let show_git_warning =
- !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf());
- let app_state = if show_login_screen || show_git_warning {
+ let app_state = if show_login_screen || show_trust_screen {
let chat_widget_args = ChatWidgetArgs {
config: config.clone(),
initial_prompt,
@@ -149,7 +146,7 @@ impl App<'_> {
codex_home: config.codex_home.clone(),
cwd: config.cwd.clone(),
show_login_screen,
- show_git_warning,
+ show_trust_screen,
chat_widget_args,
}),
}
diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs
index 078936dc33..91ee9cfdc7 100644
--- a/codex-rs/tui/src/cli.rs
+++ b/codex-rs/tui/src/cli.rs
@@ -54,10 +54,6 @@ pub struct Cli {
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
- /// Allow running Codex outside a Git repository.
- #[arg(long = "skip-git-repo-check", default_value_t = false)]
- pub skip_git_repo_check: bool,
-
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
}
diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs
index 0e809afdbe..057d25168b 100644
--- a/codex-rs/tui/src/lib.rs
+++ b/codex-rs/tui/src/lib.rs
@@ -6,8 +6,12 @@ use app::App;
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
+use codex_core::config::ConfigToml;
+use codex_core::config::find_codex_home;
+use codex_core::config::load_config_as_toml_with_cli_overrides;
use codex_core::config_types::SandboxMode;
use codex_core::protocol::AskForApproval;
+use codex_core::protocol::SandboxPolicy;
use codex_login::load_auth;
use codex_ollama::DEFAULT_OSS_MODEL;
use log_layer::TuiLogLayer;
@@ -89,33 +93,38 @@ pub async fn run_main(
None
};
- let config = {
+ // canonicalize the cwd
+ let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p));
+
+ let overrides = ConfigOverrides {
+ model,
+ approval_policy,
+ sandbox_mode,
+ cwd,
+ model_provider: model_provider_override,
+ config_profile: cli.config_profile.clone(),
+ codex_linux_sandbox_exe,
+ base_instructions: None,
+ include_plan_tool: Some(true),
+ disable_response_storage: cli.oss.then_some(true),
+ show_raw_agent_reasoning: cli.oss.then_some(true),
+ };
+
+ // Parse `-c` overrides from the CLI.
+ let cli_kv_overrides = match cli.config_overrides.parse_overrides() {
+ Ok(v) => v,
+ #[allow(clippy::print_stderr)]
+ Err(e) => {
+ eprintln!("Error parsing -c overrides: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let mut config = {
// Load configuration and support CLI overrides.
- let overrides = ConfigOverrides {
- model,
- approval_policy,
- sandbox_mode,
- cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)),
- model_provider: model_provider_override,
- config_profile: cli.config_profile.clone(),
- codex_linux_sandbox_exe,
- base_instructions: None,
- include_plan_tool: Some(true),
- disable_response_storage: cli.oss.then_some(true),
- show_raw_agent_reasoning: cli.oss.then_some(true),
- };
- // Parse `-c` overrides from the CLI.
- let cli_kv_overrides = match cli.config_overrides.parse_overrides() {
- Ok(v) => v,
- #[allow(clippy::print_stderr)]
- Err(e) => {
- eprintln!("Error parsing -c overrides: {e}");
- std::process::exit(1);
- }
- };
#[allow(clippy::print_stderr)]
- match Config::load_with_cli_overrides(cli_kv_overrides, overrides) {
+ match Config::load_with_cli_overrides(cli_kv_overrides.clone(), overrides) {
Ok(config) => config,
Err(err) => {
eprintln!("Error loading configuration: {err}");
@@ -124,6 +133,34 @@ pub async fn run_main(
}
};
+ // we load config.toml here to determine project state.
+ #[allow(clippy::print_stderr)]
+ let config_toml = {
+ let codex_home = match find_codex_home() {
+ Ok(codex_home) => codex_home,
+ Err(err) => {
+ eprintln!("Error finding codex home: {err}");
+ std::process::exit(1);
+ }
+ };
+
+ match load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides) {
+ Ok(config_toml) => config_toml,
+ Err(err) => {
+ eprintln!("Error loading config.toml: {err}");
+ std::process::exit(1);
+ }
+ }
+ };
+
+ let should_show_trust_screen = determine_repo_trust_state(
+ &mut config,
+ &config_toml,
+ approval_policy,
+ sandbox_mode,
+ cli.config_profile.clone(),
+ )?;
+
let log_dir = codex_core::config::log_dir(&config)?;
std::fs::create_dir_all(&log_dir)?;
// Open (or create) your log file, appending to it.
@@ -204,12 +241,14 @@ pub async fn run_main(
eprintln!("");
}
- run_ratatui_app(cli, config, log_rx).map_err(|err| std::io::Error::other(err.to_string()))
+ run_ratatui_app(cli, config, should_show_trust_screen, log_rx)
+ .map_err(|err| std::io::Error::other(err.to_string()))
}
fn run_ratatui_app(
cli: Cli,
config: Config,
+ should_show_trust_screen: bool,
mut log_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
) -> color_eyre::Result<codex_core::protocol::TokenUsage> {
color_eyre::install()?;
@@ -227,7 +266,7 @@ fn run_ratatui_app(
terminal.clear()?;
let Cli { prompt, images, .. } = cli;
- let mut app = App::new(config.clone(), prompt, cli.skip_git_repo_check, images);
+ let mut app = App::new(config.clone(), prompt, images, should_show_trust_screen);
// Bridge log receiver into the AppEvent channel so latest log lines update the UI.
{
@@ -277,3 +316,39 @@ fn should_show_login_screen(config: &Config) -> bool {
false
}
}
+
+/// Determine if user has configured a sandbox / approval policy,
+/// or if the current cwd project is trusted, and updates the config
+/// accordingly.
+fn determine_repo_trust_state(
+ config: &mut Config,
+ config_toml: &ConfigToml,
+ approval_policy_overide: Option<AskForApproval>,
+ sandbox_mode_override: Option<SandboxMode>,
+ config_profile_override: Option<String>,
+) -> std::io::Result<bool> {
+ let config_profile = config_toml.get_config_profile(config_profile_override)?;
+
+ if approval_policy_overide.is_some() || sandbox_mode_override.is_some() {
+ // if the user has overridden either approval policy or sandbox mode,
+ // skip the trust flow
+ Ok(false)
+ } else if config_profile.approval_policy.is_some() {
+ // if the user has specified settings in a config profile, skip the trust flow
+ // todo: profile sandbox mode?
+ Ok(false)
+ } else if config_toml.approval_policy.is_some() || config_toml.sandbox_mode.is_some() {
+ // if the user has specified either approval policy or sandbox mode in config.toml
+ // skip the trust flow
+ Ok(false)
+ } else if config_toml.is_cwd_trusted(&config.cwd) {
+ // if the current cwd project is trusted and no config has been set
+ // skip the trust flow and set the approval policy and sandbox mode
+ config.approval_policy = AskForApproval::OnRequest;
+ config.sandbox_policy = SandboxPolicy::new_workspace_write_policy();
+ Ok(false)
+ } else {
+ // if none of the above conditions are met, show the trust screen
+ Ok(true)
+ }
+}
diff --git a/codex-rs/tui/src/onboarding/continue_to_chat.rs b/codex-rs/tui/src/onboarding/continue_to_chat.rs
index 071d0851da..01e31d900a 100644
--- a/codex-rs/tui/src/onboarding/continue_to_chat.rs
+++ b/codex-rs/tui/src/onboarding/continue_to_chat.rs
@@ -8,12 +8,14 @@ use crate::app_event_sender::AppEventSender;
use crate::onboarding::onboarding_screen::StepStateProvider;
use super::onboarding_screen::StepState;
+use std::sync::Arc;
+use std::sync::Mutex;
/// This doesn't render anything explicitly but serves as a signal that we made it to the end and
/// we should continue to the chat.
pub(crate) struct ContinueToChatWidget {
pub event_tx: AppEventSender,
- pub chat_widget_args: ChatWidgetArgs,
+ pub chat_widget_args: Arc<Mutex<ChatWidgetArgs>>,
}
impl StepStateProvider for ContinueToChatWidget {
@@ -24,7 +26,9 @@ impl StepStateProvider for ContinueToChatWidget {
impl WidgetRef for &ContinueToChatWidget {
fn render_ref(&self, _area: Rect, _buf: &mut Buffer) {
- self.event_tx
- .send(AppEvent::OnboardingComplete(self.chat_widget_args.clone()));
+ if let Ok(args) = self.chat_widget_args.lock() {
+ self.event_tx
+ .send(AppEvent::OnboardingComplete(args.clone()));
+ }
}
}
diff --git a/codex-rs/tui/src/onboarding/git_warning.rs b/codex-rs/tui/src/onboarding/git_warning.rs
deleted file mode 100644
index e4e5747404..0000000000
--- a/codex-rs/tui/src/onboarding/git_warning.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-use std::path::PathBuf;
-
-use codex_core::util::is_inside_git_repo;
-use crossterm::event::KeyCode;
-use crossterm::event::KeyEvent;
-use ratatui::buffer::Buffer;
-use ratatui::layout::Rect;
-use ratatui::prelude::Widget;
-use ratatui::style::Modifier;
-use ratatui::style::Style;
-use ratatui::style::Stylize;
-use ratatui::text::Line;
-use ratatui::text::Span;
-use ratatui::widgets::Paragraph;
-use ratatui::widgets::WidgetRef;
-use ratatui::widgets::Wrap;
-
-use crate::app_event::AppEvent;
-use crate::app_event_sender::AppEventSender;
-use crate::colors::LIGHT_BLUE;
-
-use crate::onboarding::onboarding_screen::KeyboardHandler;
-use crate::onboarding::onboarding_screen::StepStateProvider;
-
-use super::onboarding_screen::StepState;
-
-pub(crate) struct GitWarningWidget {
- pub event_tx: AppEventSender,
- pub cwd: PathBuf,
- pub selection: Option<GitWarningSelection>,
- pub highlighted: GitWarningSelection,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub(crate) enum GitWarningSelection {
- Continue,
- Exit,
-}
-
-impl WidgetRef for &GitWarningWidget {
- fn render_ref(&self, area: Rect, buf: &mut Buffer) {
- let mut lines: Vec<Line> = vec![
- Line::from(vec![
- Span::raw("> "),
- Span::raw("You are running Codex in "),
- Span::styled(
- self.cwd.to_string_lossy().to_string(),
- Style::default().add_modifier(Modifier::BOLD),
- ),
- Span::raw(". This folder is not version controlled."),
- ]),
- Line::from(""),
- Line::from(" Do you want to continue?"),
- Line::from(""),
- ];
-
- let create_option =
- |idx: usize, option: GitWarningSelection, text: &str| -> Line<'static> {
- let is_selected = self.highlighted == option;
- if is_selected {
- Line::from(vec![
- Span::styled(
- format!("> {}. ", idx + 1),
- Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM),
- ),
- Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)),
- ])
- } else {
- Line::from(format!(" {}. {}", idx + 1, text))
- }
- };
-
- lines.push(create_option(0, GitWarningSelection::Continue, "Yes"));
- lines.push(create_option(1, GitWarningSelection::Exit, "No"));
- lines.push(Line::from(""));
- lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM));
-
- Paragraph::new(lines)
- .wrap(Wrap { trim: false })
- .render(area, buf);
- }
-}
-
-impl KeyboardHandler for GitWarningWidget {
- fn handle_key_event(&mut self, key_event: KeyEvent) {
- match key_event.code {
- KeyCode::Up | KeyCode::Char('k') => {
- self.highlighted = GitWarningSelection::Continue;
- }
- KeyCode::Down | KeyCode::Char('j') => {
- self.highlighted = GitWarningSelection::Exit;
- }
- KeyCode::Char('1') => self.handle_continue(),
- KeyCode::Char('2') => self.handle_quit(),
- KeyCode::Enter => match self.highlighted {
- GitWarningSelection::Continue => self.handle_continue(),
- GitWarningSelection::Exit => self.handle_quit(),
- },
- _ => {}
- }
- }
-}
-
-impl StepStateProvider for GitWarningWidget {
- fn get_step_state(&self) -> StepState {
- let is_git_repo = is_inside_git_repo(&self.cwd);
- match is_git_repo {
- true => StepState::Hidden,
- false => match self.selection {
- Some(_) => StepState::Complete,
- None => StepState::InProgress,
- },
- }
- }
-}
-
-impl GitWarningWidget {
- fn handle_continue(&mut self) {
- self.selection = Some(GitWarningSelection::Continue);
- }
-
- fn handle_quit(&mut self) {
- self.highlighted = GitWarningSelection::Exit;
- self.event_tx.send(AppEvent::ExitRequest);
- }
-}
diff --git a/codex-rs/tui/src/onboarding/mod.rs b/codex-rs/tui/src/onboarding/mod.rs
index 645cda22d9..c116936851 100644
--- a/codex-rs/tui/src/onboarding/mod.rs
+++ b/codex-rs/tui/src/onboarding/mod.rs
@@ -1,5 +1,5 @@
mod auth;
mod continue_to_chat;
-mod git_warning;
pub mod onboarding_screen;
+mod trust_directory;
mod welcome;
diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs
index 7ce7d16c47..a104f777c2 100644
--- a/codex-rs/tui/src/onboarding/onboarding_screen.rs
+++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs
@@ -1,3 +1,4 @@
+use codex_core::util::is_inside_git_repo;
use crossterm::event::KeyEvent;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -11,16 +12,18 @@ use crate::app_event_sender::AppEventSender;
use crate::onboarding::auth::AuthModeWidget;
use crate::onboarding::auth::SignInState;
use crate::onboarding::continue_to_chat::ContinueToChatWidget;
-use crate::onboarding::git_warning::GitWarningSelection;
-use crate::onboarding::git_warning::GitWarningWidget;
+use crate::onboarding::trust_directory::TrustDirectorySelection;
+use crate::onboarding::trust_directory::TrustDirectoryWidget;
use crate::onboarding::welcome::WelcomeWidget;
use std::path::PathBuf;
+use std::sync::Arc;
+use std::sync::Mutex;
#[allow(clippy::large_enum_variant)]
enum Step {
Welcome(WelcomeWidget),
Auth(AuthModeWidget),
- GitWarning(GitWarningWidget),
+ TrustDirectory(TrustDirectoryWidget),
ContinueToChat(ContinueToChatWidget),
}
@@ -49,7 +52,7 @@ pub(crate) struct OnboardingScreenArgs {
pub codex_home: PathBuf,
pub cwd: PathBuf,
pub show_login_screen: bool,
- pub show_git_warning: bool,
+ pub show_trust_screen: bool,
}
impl OnboardingScreen {
@@ -60,7 +63,7 @@ impl OnboardingScreen {
codex_home,
cwd,
show_login_screen,
- show_git_warning,
+ show_trust_screen,
} = args;
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
is_logged_in: !show_login_screen,
@@ -71,20 +74,33 @@ impl OnboardingScreen {
highlighted_mode: AuthMode::ChatGPT,
error: None,
sign_in_state: SignInState::PickMode,
- codex_home,
+ codex_home: codex_home.clone(),
}))
}
- if show_git_warning {
- steps.push(Step::GitWarning(GitWarningWidget {
- event_tx: event_tx.clone(),
+ let is_git_repo = is_inside_git_repo(&cwd);
+ let highlighted = if is_git_repo {
+ TrustDirectorySelection::Trust
+ } else {
+ // Default to not trusting the directory if it's not a git repo.
+ TrustDirectorySelection::DontTrust
+ };
+ // Share ChatWidgetArgs between steps so changes in the TrustDirectory step
+ // are reflected when continuing to chat.
+ let shared_chat_args = Arc::new(Mutex::new(chat_widget_args));
+ if show_trust_screen {
+ steps.push(Step::TrustDirectory(TrustDirectoryWidget {
cwd,
+ codex_home,
+ is_git_repo,
selection: None,
- highlighted: GitWarningSelection::Continue,
+ highlighted,
+ error: None,
+ chat_widget_args: shared_chat_args.clone(),
}))
}
steps.push(Step::ContinueToChat(ContinueToChatWidget {
event_tx: event_tx.clone(),
- chat_widget_args,
+ chat_widget_args: shared_chat_args,
}));
// TODO: add git warning.
Self { event_tx, steps }
@@ -215,7 +231,7 @@ impl KeyboardHandler for Step {
match self {
Step::Welcome(_) | Step::ContinueToChat(_) => (),
Step::Auth(widget) => widget.handle_key_event(key_event),
- Step::GitWarning(widget) => widget.handle_key_event(key_event),
+ Step::TrustDirectory(widget) => widget.handle_key_event(key_event),
}
}
}
@@ -225,7 +241,7 @@ impl StepStateProvider for Step {
match self {
Step::Welcome(w) => w.get_step_state(),
Step::Auth(w) => w.get_step_state(),
- Step::GitWarning(w) => w.get_step_state(),
+ Step::TrustDirectory(w) => w.get_step_state(),
Step::ContinueToChat(w) => w.get_step_state(),
}
}
@@ -240,7 +256,7 @@ impl WidgetRef for Step {
Step::Auth(widget) => {
widget.render_ref(area, buf);
}
- Step::GitWarning(widget) => {
+ Step::TrustDirectory(widget) => {
widget.render_ref(area, buf);
}
Step::ContinueToChat(widget) => {
diff --git a/codex-rs/tui/src/onboarding/trust_directory.rs b/codex-rs/tui/src/onboarding/trust_directory.rs
new file mode 100644
index 0000000000..3be9bac1ac
--- /dev/null
+++ b/codex-rs/tui/src/onboarding/trust_directory.rs
@@ -0,0 +1,179 @@
+use std::path::PathBuf;
+
+use codex_core::config::set_project_trusted;
+use codex_core::protocol::AskForApproval;
+use codex_core::protocol::SandboxPolicy;
+use crossterm::event::KeyCode;
+use crossterm::event::KeyEvent;
+use ratatui::buffer::Buffer;
+use ratatui::layout::Rect;
+use ratatui::prelude::Widget;
+use ratatui::style::Color;
+use ratatui::style::Modifier;
+use ratatui::style::Style;
+use ratatui::style::Stylize;
+use ratatui::text::Line;
+use ratatui::text::Span;
+use ratatui::widgets::Paragraph;
+use ratatui::widgets::WidgetRef;
+use ratatui::widgets::Wrap;
+
+use crate::colors::LIGHT_BLUE;
+
+use crate::onboarding::onboarding_screen::KeyboardHandler;
+use crate::onboarding::onboarding_screen::StepStateProvider;
+
+use super::onboarding_screen::StepState;
+use crate::app::ChatWidgetArgs;
+use std::sync::Arc;
+use std::sync::Mutex;
+
+pub(crate) struct TrustDirectoryWidget {
+ pub codex_home: PathBuf,
+ pub cwd: PathBuf,
+ pub is_git_repo: bool,
+ pub selection: Option<TrustDirectorySelection>,
+ pub highlighted: TrustDirectorySelection,
+ pub error: Option<String>,
+ pub chat_widget_args: Arc<Mutex<ChatWidgetArgs>>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) enum TrustDirectorySelection {
+ Trust,
+ DontTrust,
+}
+
+impl WidgetRef for &TrustDirectoryWidget {
+ fn render_ref(&self, area: Rect, buf: &mut Buffer) {
+ let mut lines: Vec<Line> = vec![
+ Line::from(vec![
+ Span::raw("> "),
+ Span::styled(
+ "You are running Codex in ",
+ Style::default().add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(self.cwd.to_string_lossy().to_string()),
+ ]),
+ Line::from(""),
+ ];
+
+ if self.is_git_repo {
+ lines.push(Line::from(
+ " Since this folder is version controlled, you may wish to allow Codex",
+ ));
+ lines.push(Line::from(
+ " to work in this folder without asking for approval.",
+ ));
+ } else {
+ lines.push(Line::from(
+ " Since this folder is not version controlled, we recommend requiring",
+ ));
+ lines.push(Line::from(" approval of all edits and commands."));
+ }
+ lines.push(Line::from(""));
+
+ let create_option =
+ |idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> {
+ let is_selected = self.highlighted == option;
+ if is_selected {
+ Line::from(vec![
+ Span::styled(
+ format!("> {}. ", idx + 1),
+ Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM),
+ ),
+ Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)),
+ ])
+ } else {
+ Line::from(format!(" {}. {}", idx + 1, text))
+ }
+ };
+
+ if self.is_git_repo {
+ lines.push(create_option(
+ 0,
+ TrustDirectorySelection::Trust,
+ "Yes, allow Codex to work in this folder without asking for approval",
+ ));
+ lines.push(create_option(
+ 1,
+ TrustDirectorySelection::DontTrust,
+ "No, ask me to approve edits and commands",
+ ));
+ } else {
+ lines.push(create_option(
+ 0,
+ TrustDirectorySelection::Trust,
+ "Allow Codex to work in this folder without asking for approval",
+ ));
+ lines.push(create_option(
+ 1,
+ TrustDirectorySelection::DontTrust,
+ "Require approval of edits and commands",
+ ));
+ }
+ lines.push(Line::from(""));
+ if let Some(error) = &self.error {
+ lines.push(Line::from(format!(" {error}")).fg(Color::Red));
+ lines.push(Line::from(""));
+ }
+ lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM));
+
+ Paragraph::new(lines)
+ .wrap(Wrap { trim: false })
+ .render(area, buf);
+ }
+}
+
+impl KeyboardHandler for TrustDirectoryWidget {
+ fn handle_key_event(&mut self, key_event: KeyEvent) {
+ match key_event.code {
+ KeyCode::Up | KeyCode::Char('k') => {
+ self.highlighted = TrustDirectorySelection::Trust;
+ }
+ KeyCode::Down | KeyCode::Char('j') => {
+ self.highlighted = TrustDirectorySelection::DontTrust;
+ }
+ KeyCode::Char('1') => self.handle_trust(),
+ KeyCode::Char('2') => self.handle_dont_trust(),
+ KeyCode::Enter => match self.highlighted {
+ TrustDirectorySelection::Trust => self.handle_trust(),
+ TrustDirectorySelection::DontTrust => self.handle_dont_trust(),
+ },
+ _ => {}
+ }
+ }
+}
+
+impl StepStateProvider for TrustDirectoryWidget {
+ fn get_step_state(&self) -> StepState {
+ match self.selection {
+ Some(_) => StepState::Complete,
+ None => StepState::InProgress,
+ }
+ }
+}
+
+impl TrustDirectoryWidget {
+ fn handle_trust(&mut self) {
+ if let Err(e) = set_project_trusted(&self.codex_home, &self.cwd) {
+ tracing::error!("Failed to set project trusted: {e:?}");
+ self.error = Some(e.to_string());
+ // self.error = Some("Failed to set project trusted".to_string());
+ }
+
+ // Update the in-memory chat config for this session to a more permissive
+ // policy suitable for a trusted workspace.
+ if let Ok(mut args) = self.chat_widget_args.lock() {
+ args.config.approval_policy = AskForApproval::OnRequest;
+ args.config.sandbox_policy = SandboxPolicy::new_workspace_write_policy();
+ }
+
+ self.selection = Some(TrustDirectorySelection::Trust);
+ }
+
+ fn handle_dont_trust(&mut self) {
+ self.highlighted = TrustDirectorySelection::DontTrust;
+ self.selection = Some(TrustDirectorySelection::DontTrust);
+ }
+}
```
## Review Comments
### codex-rs/core/Cargo.toml
- Created: 2025-08-07 15:46:51 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2260738622
```diff
@@ -53,6 +53,8 @@ tree-sitter-bash = "0.25.0"
uuid = { version = "1", features = ["serde", "v4"] }
whoami = "1.6.0"
wildmatch = "2.4.0"
+toml_edit = "0.23.3"
```
> alpha :P
### codex-rs/core/src/config.rs
- Created: 2025-08-07 09:11:39 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259655004
```diff
@@ -214,6 +225,33 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
}
}
+/// Patch `CODEX_HOME/config.toml` project state.
+/// Use with caution.
+pub fn set_project_trusted(
+ codex_home: &Path,
+ project_path: &Path,
+ trusted: bool,
+) -> anyhow::Result<()> {
+ let config_path = codex_home.join("config.toml");
+
+ // Parse existing config if present; otherwise start a new document.
+ let mut doc = match std::fs::read_to_string(&config_path) {
+ Ok(s) => s.parse::<DocumentMut>()?,
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
+ Err(e) => return Err(e.into()),
+ };
+
+ let project_key = project_path.to_string_lossy().to_string();
+ doc["projects"][project_key.as_str()]["trusted"] = toml_edit::value(trusted);
+
+ if let Some(parent) = config_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ std::fs::write(config_path, doc.to_string())?;
```
> We should maybe write to a temp file in the folder and then `mv` it so writes are atomic.
- Created: 2025-08-07 09:12:03 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259656059
```diff
@@ -214,6 +225,33 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
}
}
+/// Patch `CODEX_HOME/config.toml` project state.
+/// Use with caution.
+pub fn set_project_trusted(
+ codex_home: &Path,
+ project_path: &Path,
+ trusted: bool,
+) -> anyhow::Result<()> {
+ let config_path = codex_home.join("config.toml");
+
+ // Parse existing config if present; otherwise start a new document.
+ let mut doc = match std::fs::read_to_string(&config_path) {
+ Ok(s) => s.parse::<DocumentMut>()?,
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
+ Err(e) => return Err(e.into()),
+ };
+
+ let project_key = project_path.to_string_lossy().to_string();
+ doc["projects"][project_key.as_str()]["trusted"] = toml_edit::value(trusted);
+
+ if let Some(parent) = config_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
```
> By construction, `parent` is `codex_home`, so just use that?
- Created: 2025-08-07 09:12:51 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259658005
```diff
@@ -214,6 +225,33 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
}
}
+/// Patch `CODEX_HOME/config.toml` project state.
+/// Use with caution.
+pub fn set_project_trusted(
+ codex_home: &Path,
+ project_path: &Path,
+ trusted: bool,
+) -> anyhow::Result<()> {
+ let config_path = codex_home.join("config.toml");
+
+ // Parse existing config if present; otherwise start a new document.
+ let mut doc = match std::fs::read_to_string(&config_path) {
+ Ok(s) => s.parse::<DocumentMut>()?,
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
+ Err(e) => return Err(e.into()),
+ };
+
+ let project_key = project_path.to_string_lossy().to_string();
+ doc["projects"][project_key.as_str()]["trusted"] = toml_edit::value(trusted);
```
> What happens if `doc` does not have a `"projects"` key: does it create an entry on demand? How does this not panic?
- Created: 2025-08-07 09:18:05 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259670414
```diff
@@ -518,6 +565,41 @@ impl Config {
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
let base_instructions = base_instructions.or(file_base_instructions);
+ // Let's begin.
+ // First: load approval_policy and sandbox_mode from overrides, config
+ // profile, or config.toml.
+ let mut approval_policy = approval_policy
+ .or(config_profile.approval_policy)
+ .or(cfg.approval_policy.clone());
+ // TODO: Add sandbox_mode to the config profile? Doesn't
+ // appear to exist right now
+ let mut sandbox_mode = sandbox_mode.or(cfg.sandbox_mode.clone());
+ let mut projects = cfg.projects.clone().unwrap_or_default();
+
+ // Second: Default to "trusted" if the user has configured approval policy
```
> What does "trusted" mean, particularly if sandbox mode is "read only"?
- Created: 2025-08-07 09:19:15 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259673214
```diff
@@ -518,6 +565,41 @@ impl Config {
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
let base_instructions = base_instructions.or(file_base_instructions);
+ // Let's begin.
+ // First: load approval_policy and sandbox_mode from overrides, config
+ // profile, or config.toml.
+ let mut approval_policy = approval_policy
+ .or(config_profile.approval_policy)
+ .or(cfg.approval_policy.clone());
+ // TODO: Add sandbox_mode to the config profile? Doesn't
+ // appear to exist right now
+ let mut sandbox_mode = sandbox_mode.or(cfg.sandbox_mode.clone());
+ let mut projects = cfg.projects.clone().unwrap_or_default();
+
+ // Second: Default to "trusted" if the user has configured approval policy
+ // or sandbox mode, regardless of projects config. If you've modified the
+ // config.toml, we'll respect your decision.
+ //
+ // This is a bit of a hack, but it allows us to skip tui onboarding
+ // for now.
+ if approval_policy.is_some() || sandbox_mode.is_some() {
+ projects.insert(
```
> What if `projects` has an existing entry for `cwd` where `trusted=false`? Should this override it? Should it write it back?
- Created: 2025-08-07 09:30:40 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259702973
```diff
@@ -214,6 +225,33 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
}
}
+/// Patch `CODEX_HOME/config.toml` project state.
+/// Use with caution.
+pub fn set_project_trusted(
+ codex_home: &Path,
+ project_path: &Path,
+ trusted: bool,
+) -> anyhow::Result<()> {
+ let config_path = codex_home.join("config.toml");
+
+ // Parse existing config if present; otherwise start a new document.
+ let mut doc = match std::fs::read_to_string(&config_path) {
+ Ok(s) => s.parse::<DocumentMut>()?,
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
+ Err(e) => return Err(e.into()),
+ };
+
+ let project_key = project_path.to_string_lossy().to_string();
+ doc["projects"][project_key.as_str()]["trusted"] = toml_edit::value(trusted);
```
> I see, `DocumentMut` implements `IndexMut<&str>`.
- Created: 2025-08-07 09:34:45 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259712484
```diff
@@ -518,6 +565,41 @@ impl Config {
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
let base_instructions = base_instructions.or(file_base_instructions);
+ // Let's begin.
+ // First: load approval_policy and sandbox_mode from overrides, config
+ // profile, or config.toml.
+ let mut approval_policy = approval_policy
+ .or(config_profile.approval_policy)
+ .or(cfg.approval_policy);
+ // TODO: Add sandbox_mode to the config profile? Doesn't
+ // appear to exist right now
+ let mut sandbox_mode = sandbox_mode.or(cfg.sandbox_mode);
+ let mut projects = cfg.projects.clone().unwrap_or_default();
+
+ // Second: Default to "trusted" if the user has configured approval policy
+ // or sandbox mode, regardless of projects config. If you've modified the
+ // config.toml, we'll respect your decision.
+ //
+ // This is a bit of a hack, but it allows us to skip tui onboarding
+ // for now.
+ if approval_policy.is_some() || sandbox_mode.is_some() {
+ projects.insert(
+ resolved_cwd.to_string_lossy().to_string(),
+ ProjectConfig {
+ trusted: Some(true),
+ },
+ );
+ } else if let Some(project) = projects.get(&resolved_cwd.to_string_lossy().to_string()) {
+ // Third: If we have a project config, and it's trusted, set the approval policy and sandbox mode
+ if project.trusted.unwrap_or(false) {
+ approval_policy = Some(AskForApproval::OnRequest);
+ sandbox_mode = Some(SandboxMode::WorkspaceWrite);
```
> So if I run the Codex CLI once with `--sandbox read-only`, we would set it as trusted, and the next time, if I didn't run any flags, we would run it with workplace-write?
- Created: 2025-08-07 09:40:22 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259725983
```diff
@@ -518,6 +565,41 @@ impl Config {
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
let base_instructions = base_instructions.or(file_base_instructions);
+ // Let's begin.
+ // First: load approval_policy and sandbox_mode from overrides, config
+ // profile, or config.toml.
+ let mut approval_policy = approval_policy
+ .or(config_profile.approval_policy)
+ .or(cfg.approval_policy.clone());
+ // TODO: Add sandbox_mode to the config profile? Doesn't
+ // appear to exist right now
+ let mut sandbox_mode = sandbox_mode.or(cfg.sandbox_mode.clone());
+ let mut projects = cfg.projects.clone().unwrap_or_default();
+
+ // Second: Default to "trusted" if the user has configured approval policy
```
> What if I ran `codex exec --sandbox read-only`?
- Created: 2025-08-07 09:40:56 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259727288
```diff
@@ -518,6 +565,41 @@ impl Config {
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
let base_instructions = base_instructions.or(file_base_instructions);
+ // Let's begin.
+ // First: load approval_policy and sandbox_mode from overrides, config
+ // profile, or config.toml.
+ let mut approval_policy = approval_policy
+ .or(config_profile.approval_policy)
+ .or(cfg.approval_policy.clone());
+ // TODO: Add sandbox_mode to the config profile? Doesn't
+ // appear to exist right now
+ let mut sandbox_mode = sandbox_mode.or(cfg.sandbox_mode.clone());
+ let mut projects = cfg.projects.clone().unwrap_or_default();
+
+ // Second: Default to "trusted" if the user has configured approval policy
```
> This logic is in `core`, but it seems you are making strong assumptions about the UI here?
- Created: 2025-08-07 15:49:16 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2260744829
```diff
@@ -666,6 +715,28 @@ pub fn find_codex_home() -> std::io::Result<PathBuf> {
Ok(p)
}
+pub fn resolve_cwd(cwd: Option<PathBuf>) -> std::io::Result<PathBuf> {
```
> I think this function can go away?
### codex-rs/core/src/protocol.rs
- Created: 2025-08-07 09:20:26 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259675989
```diff
@@ -151,6 +150,7 @@ pub enum AskForApproval {
OnFailure,
/// The model decides when to ask the user for approval.
+ #[default]
```
> Update docs?
### codex-rs/exec/src/lib.rs
- Created: 2025-08-07 09:21:30 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259679636
```diff
@@ -180,11 +178,6 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
// is using.
event_processor.print_config_summary(&config, &prompt);
- if !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()) {
```
> I thought there was still going to be a case where we exit for `codex exec` even if not in a Git repo?
### codex-rs/tui/src/lib.rs
- Created: 2025-08-07 15:48:51 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2260743769
```diff
@@ -277,3 +322,33 @@ fn should_show_login_screen(config: &Config) -> bool {
false
}
}
+
+fn should_show_trust_screen(
+ config: &mut Config,
+ config_toml: &ConfigToml,
+ approval_policy_overide: Option<AskForApproval>,
+ sandbox_mode_override: Option<SandboxMode>,
+ cwd: Option<PathBuf>,
```
> Why does this take `cwd` as an arg instead of using `config.cwd`?
>
> We should be sure we are honoring `--cwd` if the user passed it in...
- Created: 2025-08-07 15:52:25 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2260751522
```diff
@@ -277,3 +322,33 @@ fn should_show_login_screen(config: &Config) -> bool {
false
}
}
+
+fn should_show_trust_screen(
+ config: &mut Config,
+ config_toml: &ConfigToml,
+ approval_policy_overide: Option<AskForApproval>,
+ sandbox_mode_override: Option<SandboxMode>,
+ cwd: Option<PathBuf>,
+) -> std::io::Result<bool> {
+ let cwd = cwd.map(|p| p.canonicalize().unwrap_or(p));
+ let resolved_cwd = resolve_cwd(cwd)?;
+
+ if approval_policy_overide.is_some() || sandbox_mode_override.is_some() {
```
> Note the user _could have_ passed `-c sandbox_mode=read-only`...
### codex-rs/tui/src/onboarding/continue_to_chat.rs
- Created: 2025-08-07 09:24:44 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259687584
```diff
@@ -8,12 +8,14 @@ use crate::app_event_sender::AppEventSender;
use crate::onboarding::onboarding_screen::StepStateProvider;
use super::onboarding_screen::StepState;
+use std::sync::Arc;
+use std::sync::Mutex;
/// This doesn't render anything explicitly but serves as a signal that we made it to the end and
/// we should continue to the chat.
pub(crate) struct ContinueToChatWidget {
pub event_tx: AppEventSender,
- pub chat_widget_args: ChatWidgetArgs,
+ pub chat_widget_args: Arc<Mutex<ChatWidgetArgs>>,
```
> Maybe I'm missing where this happens, but I don't see where this is mutated (though it is cloned), so does it need `Arc<Mutex>`?