diff --git a/codex-rs/core-skills/src/loader/environment.rs b/codex-rs/core-skills/src/loader/environment.rs index da525d3362..8d400d7cdc 100644 --- a/codex-rs/core-skills/src/loader/environment.rs +++ b/codex-rs/core-skills/src/loader/environment.rs @@ -160,6 +160,7 @@ pub async fn load_environment_skills_from_root( max_directories: MAX_SKILLS_DIRS_PER_ROOT, max_entries: MAX_SKILLS_ENTRIES_PER_ROOT, follow_directory_symlinks: true, + prune_hidden_directories: false, }, /*sandbox*/ None, ) diff --git a/codex-rs/exec-server/tests/file_system/shared.rs b/codex-rs/exec-server/tests/file_system/shared.rs index 261c6b07d0..236a9623ee 100644 --- a/codex-rs/exec-server/tests/file_system/shared.rs +++ b/codex-rs/exec-server/tests/file_system/shared.rs @@ -402,6 +402,7 @@ async fn file_system_walk_returns_a_bounded_tree( max_directories: 10, max_entries: 10, follow_directory_symlinks: false, + prune_hidden_directories: false, }, /*sandbox*/ None, ) @@ -447,6 +448,7 @@ async fn file_system_walk_returns_a_bounded_tree( max_directories: 10, max_entries: 10, follow_directory_symlinks: false, + prune_hidden_directories: false, }, /*sandbox*/ None, ) @@ -469,6 +471,7 @@ async fn file_system_walk_returns_a_bounded_tree( max_directories: 1, max_entries: 10, follow_directory_symlinks: false, + prune_hidden_directories: false, }, /*sandbox*/ None, ) @@ -491,6 +494,7 @@ async fn file_system_walk_returns_a_bounded_tree( max_directories: 10, max_entries: 1, follow_directory_symlinks: false, + prune_hidden_directories: false, }, /*sandbox*/ None, ) @@ -535,6 +539,7 @@ async fn file_system_walk_honors_read_sandbox( max_directories: 2, max_entries: 2, follow_directory_symlinks: false, + prune_hidden_directories: false, }, Some(&sandbox), ) diff --git a/codex-rs/exec-server/tests/file_system_unix.rs b/codex-rs/exec-server/tests/file_system_unix.rs index f658045441..c3fc680787 100644 --- a/codex-rs/exec-server/tests/file_system_unix.rs +++ b/codex-rs/exec-server/tests/file_system_unix.rs @@ -299,6 +299,7 @@ async fn file_system_walk_handles_directory_symlinks( max_directories: 4, max_entries: 8, follow_directory_symlinks: false, + prune_hidden_directories: false, }, /*sandbox*/ None, ) @@ -321,6 +322,7 @@ async fn file_system_walk_handles_directory_symlinks( max_directories: 4, max_entries: 8, follow_directory_symlinks: true, + prune_hidden_directories: false, }, /*sandbox*/ None, ) @@ -351,6 +353,68 @@ async fn file_system_walk_handles_directory_symlinks( Ok(()) } +#[test_case(FileSystemImplementation::Local ; "local")] +#[test_case(FileSystemImplementation::Remote ; "remote")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn file_system_walk_prunes_hidden_directories_without_claiming_visible_aliases( + implementation: FileSystemImplementation, +) -> Result<()> { + let context = create_file_system_context(implementation).await?; + let file_system = context.file_system; + + let tmp = TempDir::new()?; + let root = tmp.path().join("root"); + let hidden = root.join(".hidden"); + let hidden_nested = hidden.join("nested"); + let visible = root.join("visible"); + std::fs::create_dir_all(&hidden_nested)?; + std::fs::write(hidden_nested.join("note.txt"), "visible through alias")?; + symlink(&hidden, &visible)?; + + let outcome = file_system + .walk( + &PathUri::from_host_native_path(&root)?, + WalkOptions { + max_depth: 3, + max_directories: 3, + max_entries: 6, + follow_directory_symlinks: true, + prune_hidden_directories: true, + }, + /*sandbox*/ None, + ) + .await + .with_context(|| format!("mode={implementation}"))?; + + assert_eq!( + outcome, + WalkOutcome { + entries: vec![ + WalkEntry { + path: PathUri::from_host_native_path(hidden)?, + kind: WalkEntryKind::Directory, + }, + WalkEntry { + path: PathUri::from_host_native_path(&visible)?, + kind: WalkEntryKind::Directory, + }, + WalkEntry { + path: PathUri::from_host_native_path(visible.join("nested"))?, + kind: WalkEntryKind::Directory, + }, + WalkEntry { + path: PathUri::from_host_native_path(visible.join("nested/note.txt"))?, + kind: WalkEntryKind::File, + }, + ], + errors: Vec::new(), + truncated: false, + } + ); + + Ok(()) +} + #[test_case(FileSystemImplementation::Local ; "local")] #[test_case(FileSystemImplementation::Remote ; "remote")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/codex-rs/file-system/src/lib.rs b/codex-rs/file-system/src/lib.rs index 737412f463..2ef3bcf710 100644 --- a/codex-rs/file-system/src/lib.rs +++ b/codex-rs/file-system/src/lib.rs @@ -81,6 +81,9 @@ pub struct WalkOptions { pub max_entries: usize, /// Whether directory symlinks should be followed. pub follow_directory_symlinks: bool, + /// Whether directories whose names start with `.` should be returned but not traversed. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub prune_hidden_directories: bool, } /// Type of a filesystem entry returned by a walk. @@ -463,6 +466,9 @@ async fn walk_via_directory_reads( }); if kind == WalkEntryKind::Directory && depth < options.max_depth { + if options.prune_hidden_directories && entry.file_name.starts_with('.') { + continue; + } let directory_identity = if options.follow_directory_symlinks { match file_system.canonicalize(&path, sandbox).await { Ok(path) => path,