Skip to content

Commit 4f46360

Browse files
authored
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
1 parent 2287d2a commit 4f46360

File tree

8 files changed

+89
-1
lines changed

8 files changed

+89
-1
lines changed

codex-rs/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ In the transcript preview, the footer shows an `Esc edit prev` hint while editin
6161

6262
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.
6363

64+
### `--add-dir` flag
65+
66+
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:
67+
68+
```shell
69+
codex --cd apps/frontend --add-dir ../backend --add-dir ../shared
70+
```
71+
72+
Codex can now inspect and edit files in each listed directory without leaving the primary workspace.
73+
6474
### Shell completions
6575

6676
Generate shell completion scripts via:

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,7 @@ async fn derive_config_from_params(
13531353
include_view_image_tool: None,
13541354
show_raw_agent_reasoning: None,
13551355
tools_web_search_request: None,
1356+
additional_writable_roots: Vec::new(),
13561357
};
13571358

13581359
let cli_overrides = cli_overrides

codex-rs/cli/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,9 @@ fn merge_resume_cli_flags(interactive: &mut TuiCli, resume_cli: TuiCli) {
563563
if !resume_cli.images.is_empty() {
564564
interactive.images = resume_cli.images;
565565
}
566+
if !resume_cli.add_dir.is_empty() {
567+
interactive.add_dir.extend(resume_cli.add_dir);
568+
}
566569
if let Some(prompt) = resume_cli.prompt {
567570
interactive.prompt = Some(prompt);
568571
}

codex-rs/core/src/config.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use codex_protocol::config_types::SandboxMode;
4242
use codex_protocol::config_types::Verbosity;
4343
use codex_rmcp_client::OAuthCredentialsStoreMode;
4444
use dirs::home_dir;
45+
use dunce::canonicalize;
4546
use serde::Deserialize;
4647
use similar::DiffableStr;
4748
use std::collections::BTreeMap;
@@ -1094,6 +1095,8 @@ pub struct ConfigOverrides {
10941095
pub include_view_image_tool: Option<bool>,
10951096
pub show_raw_agent_reasoning: Option<bool>,
10961097
pub tools_web_search_request: Option<bool>,
1098+
/// Additional directories that should be treated as writable roots for this session.
1099+
pub additional_writable_roots: Vec<PathBuf>,
10971100
}
10981101

10991102
impl Config {
@@ -1122,6 +1125,7 @@ impl Config {
11221125
include_view_image_tool: include_view_image_tool_override,
11231126
show_raw_agent_reasoning,
11241127
tools_web_search_request: override_tools_web_search_request,
1128+
additional_writable_roots,
11251129
} = overrides;
11261130

11271131
let active_profile_name = config_profile_key
@@ -1169,11 +1173,32 @@ impl Config {
11691173
}
11701174
}
11711175
};
1176+
let additional_writable_roots: Vec<PathBuf> = additional_writable_roots
1177+
.into_iter()
1178+
.map(|path| {
1179+
let absolute = if path.is_absolute() {
1180+
path
1181+
} else {
1182+
resolved_cwd.join(path)
1183+
};
1184+
match canonicalize(&absolute) {
1185+
Ok(canonical) => canonical,
1186+
Err(_) => absolute,
1187+
}
1188+
})
1189+
.collect();
11721190
let active_project = cfg
11731191
.get_active_project(&resolved_cwd)
11741192
.unwrap_or(ProjectConfig { trust_level: None });
11751193

1176-
let sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode, &resolved_cwd);
1194+
let mut sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode, &resolved_cwd);
1195+
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
1196+
for path in additional_writable_roots {
1197+
if !writable_roots.iter().any(|existing| existing == &path) {
1198+
writable_roots.push(path);
1199+
}
1200+
}
1201+
}
11771202
let mut approval_policy = approval_policy_override
11781203
.or(config_profile.approval_policy)
11791204
.or(cfg.approval_policy)
@@ -1613,6 +1638,46 @@ trust_level = "trusted"
16131638
);
16141639
}
16151640

1641+
#[test]
1642+
fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<()> {
1643+
let temp_dir = TempDir::new()?;
1644+
let frontend = temp_dir.path().join("frontend");
1645+
let backend = temp_dir.path().join("backend");
1646+
std::fs::create_dir_all(&frontend)?;
1647+
std::fs::create_dir_all(&backend)?;
1648+
1649+
let overrides = ConfigOverrides {
1650+
cwd: Some(frontend),
1651+
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
1652+
additional_writable_roots: vec![PathBuf::from("../backend"), backend.clone()],
1653+
..Default::default()
1654+
};
1655+
1656+
let config = Config::load_from_base_config_with_overrides(
1657+
ConfigToml::default(),
1658+
overrides,
1659+
temp_dir.path().to_path_buf(),
1660+
)?;
1661+
1662+
let expected_backend = canonicalize(&backend).expect("canonicalize backend directory");
1663+
match config.sandbox_policy {
1664+
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
1665+
assert_eq!(
1666+
writable_roots
1667+
.iter()
1668+
.filter(|root| **root == expected_backend)
1669+
.count(),
1670+
1,
1671+
"expected single writable root entry for {}",
1672+
expected_backend.display()
1673+
);
1674+
}
1675+
other => panic!("expected workspace-write policy, got {other:?}"),
1676+
}
1677+
1678+
Ok(())
1679+
}
1680+
16161681
#[test]
16171682
fn approve_all_feature_forces_on_request_policy() -> std::io::Result<()> {
16181683
let cfg = r#"

codex-rs/exec/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
181181
include_view_image_tool: None,
182182
show_raw_agent_reasoning: oss.then_some(true),
183183
tools_web_search_request: None,
184+
additional_writable_roots: Vec::new(),
184185
};
185186
// Parse `-c` overrides.
186187
let cli_kv_overrides = match config_overrides.parse_overrides() {

codex-rs/mcp-server/src/codex_tool_config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ impl CodexToolCallParam {
164164
include_view_image_tool: None,
165165
show_raw_agent_reasoning: None,
166166
tools_web_search_request: None,
167+
additional_writable_roots: Vec::new(),
167168
};
168169

169170
let cli_overrides = cli_overrides

codex-rs/tui/src/cli.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use clap::Parser;
2+
use clap::ValueHint;
23
use codex_common::ApprovalModeCliArg;
34
use codex_common::CliConfigOverrides;
45
use std::path::PathBuf;
@@ -72,6 +73,10 @@ pub struct Cli {
7273
#[arg(long = "search", default_value_t = false)]
7374
pub web_search: bool,
7475

76+
/// Additional directories that should be writable alongside the primary workspace.
77+
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
78+
pub add_dir: Vec<PathBuf>,
79+
7580
#[clap(skip)]
7681
pub config_overrides: CliConfigOverrides,
7782
}

codex-rs/tui/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ pub async fn run_main(
161161

162162
// canonicalize the cwd
163163
let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p));
164+
let additional_dirs = cli.add_dir.clone();
164165

165166
let overrides = ConfigOverrides {
166167
model,
@@ -177,6 +178,7 @@ pub async fn run_main(
177178
include_view_image_tool: None,
178179
show_raw_agent_reasoning: cli.oss.then_some(true),
179180
tools_web_search_request: cli.web_search.then_some(true),
181+
additional_writable_roots: additional_dirs,
180182
};
181183
let raw_overrides = cli.config_overrides.raw_overrides.clone();
182184
let overrides_cli = codex_common::CliConfigOverrides { raw_overrides };

0 commit comments

Comments
 (0)