From 4f46360aa493b04eae12045ac8c34a4e42441734 Mon Sep 17 00:00:00 2001 From: Thibault Sottiaux Date: Sat, 18 Oct 2025 22:13:53 -0700 Subject: [PATCH] feat: add --add-dir flag for extra writable roots (#5335) Add a `--add-dir` CLI flag so sessions can use extra writable roots in addition to the ones specified in the config file. These are ephemerally added during the session only. Fixes #3303 Fixes #2797 --- codex-rs/README.md | 10 +++ .../app-server/src/codex_message_processor.rs | 1 + codex-rs/cli/src/main.rs | 3 + codex-rs/core/src/config.rs | 67 ++++++++++++++++++- codex-rs/exec/src/lib.rs | 1 + codex-rs/mcp-server/src/codex_tool_config.rs | 1 + codex-rs/tui/src/cli.rs | 5 ++ codex-rs/tui/src/lib.rs | 2 + 8 files changed, 89 insertions(+), 1 deletion(-) diff --git a/codex-rs/README.md b/codex-rs/README.md index a05a8d48..7f944d7a 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -61,6 +61,16 @@ In the transcript preview, the footer shows an `Esc edit prev` hint while editin Sometimes it is not convenient to `cd` to the directory you want Codex to use as the "working root" before running Codex. Fortunately, `codex` supports a `--cd` option so you can specify whatever folder you want. You can confirm that Codex is honoring `--cd` by double-checking the **workdir** it reports in the TUI at the start of a new session. +### `--add-dir` flag + +Need to work across multiple projects? Pass `--add-dir` one or more times to expose extra directories as writable roots for the current session while keeping the main working directory unchanged. For example: + +```shell +codex --cd apps/frontend --add-dir ../backend --add-dir ../shared +``` + +Codex can now inspect and edit files in each listed directory without leaving the primary workspace. + ### Shell completions Generate shell completion scripts via: diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index f455ad32..23f1a378 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1353,6 +1353,7 @@ async fn derive_config_from_params( include_view_image_tool: None, show_raw_agent_reasoning: None, tools_web_search_request: None, + additional_writable_roots: Vec::new(), }; let cli_overrides = cli_overrides diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 71fee4fd..e01bd259 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -563,6 +563,9 @@ fn merge_resume_cli_flags(interactive: &mut TuiCli, resume_cli: TuiCli) { if !resume_cli.images.is_empty() { interactive.images = resume_cli.images; } + if !resume_cli.add_dir.is_empty() { + interactive.add_dir.extend(resume_cli.add_dir); + } if let Some(prompt) = resume_cli.prompt { interactive.prompt = Some(prompt); } diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 6f5defce..efc5ca3d 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -42,6 +42,7 @@ use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::Verbosity; use codex_rmcp_client::OAuthCredentialsStoreMode; use dirs::home_dir; +use dunce::canonicalize; use serde::Deserialize; use similar::DiffableStr; use std::collections::BTreeMap; @@ -1094,6 +1095,8 @@ pub struct ConfigOverrides { pub include_view_image_tool: Option, pub show_raw_agent_reasoning: Option, pub tools_web_search_request: Option, + /// Additional directories that should be treated as writable roots for this session. + pub additional_writable_roots: Vec, } impl Config { @@ -1122,6 +1125,7 @@ impl Config { include_view_image_tool: include_view_image_tool_override, show_raw_agent_reasoning, tools_web_search_request: override_tools_web_search_request, + additional_writable_roots, } = overrides; let active_profile_name = config_profile_key @@ -1169,11 +1173,32 @@ impl Config { } } }; + let additional_writable_roots: Vec = additional_writable_roots + .into_iter() + .map(|path| { + let absolute = if path.is_absolute() { + path + } else { + resolved_cwd.join(path) + }; + match canonicalize(&absolute) { + Ok(canonical) => canonical, + Err(_) => absolute, + } + }) + .collect(); let active_project = cfg .get_active_project(&resolved_cwd) .unwrap_or(ProjectConfig { trust_level: None }); - let sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode, &resolved_cwd); + let mut sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode, &resolved_cwd); + if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy { + for path in additional_writable_roots { + if !writable_roots.iter().any(|existing| existing == &path) { + writable_roots.push(path); + } + } + } let mut approval_policy = approval_policy_override .or(config_profile.approval_policy) .or(cfg.approval_policy) @@ -1613,6 +1638,46 @@ trust_level = "trusted" ); } + #[test] + fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<()> { + let temp_dir = TempDir::new()?; + let frontend = temp_dir.path().join("frontend"); + let backend = temp_dir.path().join("backend"); + std::fs::create_dir_all(&frontend)?; + std::fs::create_dir_all(&backend)?; + + let overrides = ConfigOverrides { + cwd: Some(frontend), + sandbox_mode: Some(SandboxMode::WorkspaceWrite), + additional_writable_roots: vec![PathBuf::from("../backend"), backend.clone()], + ..Default::default() + }; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + overrides, + temp_dir.path().to_path_buf(), + )?; + + let expected_backend = canonicalize(&backend).expect("canonicalize backend directory"); + match config.sandbox_policy { + SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { + assert_eq!( + writable_roots + .iter() + .filter(|root| **root == expected_backend) + .count(), + 1, + "expected single writable root entry for {}", + expected_backend.display() + ); + } + other => panic!("expected workspace-write policy, got {other:?}"), + } + + Ok(()) + } + #[test] fn approve_all_feature_forces_on_request_policy() -> std::io::Result<()> { let cfg = r#" diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 144e7cab..efd2ff54 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -181,6 +181,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any include_view_image_tool: None, show_raw_agent_reasoning: oss.then_some(true), tools_web_search_request: None, + additional_writable_roots: Vec::new(), }; // Parse `-c` overrides. let cli_kv_overrides = match config_overrides.parse_overrides() { diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 3cd2cccd..5b5242f2 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -164,6 +164,7 @@ impl CodexToolCallParam { include_view_image_tool: None, show_raw_agent_reasoning: None, tools_web_search_request: None, + additional_writable_roots: Vec::new(), }; let cli_overrides = cli_overrides diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index ee2a0355..d86040b5 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -1,4 +1,5 @@ use clap::Parser; +use clap::ValueHint; use codex_common::ApprovalModeCliArg; use codex_common::CliConfigOverrides; use std::path::PathBuf; @@ -72,6 +73,10 @@ pub struct Cli { #[arg(long = "search", default_value_t = false)] pub web_search: bool, + /// Additional directories that should be writable alongside the primary workspace. + #[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)] + pub add_dir: Vec, + #[clap(skip)] pub config_overrides: CliConfigOverrides, } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 3cae3939..1753f311 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -161,6 +161,7 @@ pub async fn run_main( // canonicalize the cwd let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)); + let additional_dirs = cli.add_dir.clone(); let overrides = ConfigOverrides { model, @@ -177,6 +178,7 @@ pub async fn run_main( include_view_image_tool: None, show_raw_agent_reasoning: cli.oss.then_some(true), tools_web_search_request: cli.web_search.then_some(true), + additional_writable_roots: additional_dirs, }; let raw_overrides = cli.config_overrides.raw_overrides.clone(); let overrides_cli = codex_common::CliConfigOverrides { raw_overrides };