[codex] preserve fsmonitor for worktree Git reads (#26880)

Codex forces `core.fsmonitor=false` on internal Git commands so a
repository cannot select an executable fsmonitor helper. This also
disables Git's built-in daemon for `status`, `diff`, and `ls-files`,
turning those worktree reads into full scans in large repositories.

Read the raw effective `core.fsmonitor` value and preserve it only when
Git interprets it as true and advertises built-in daemon support through
`git version --build-options`. Query uncommon boolean spellings back
through Git using the exact effective value. Unset, false, helper paths,
malformed values, probe failures, and unsupported Git builds continue to
force `core.fsmonitor=false`.

Centralize this policy in `git-utils` while keeping process execution in
the existing local and workspace-command adapters. Probe once per
worktree workflow and reuse the result for its Git commands, including
the TUI `/diff` path. Metadata-only commands and repository discovery
remain disabled without probing. Each probe and requested Git process
keeps its own existing timeout, and the decision is not cached because
layered and conditional Git configuration can change while Codex runs.

---------

Co-authored-by: Chris Bookholt <bookholt@openai.com>
This commit is contained in:
Tamir Duberstein
2026-06-08 21:32:46 -07:00
committed by GitHub
Unverified
parent 08cb633c06
commit dffc4bf75d
6 changed files with 831 additions and 203 deletions
+129
View File
@@ -0,0 +1,129 @@
//! Policy for preserving Git's built-in filesystem monitor.
//!
//! Codex overrides `core.fsmonitor` so repository configuration cannot select
//! an executable helper. Preserve the built-in daemon only when the effective
//! value is boolean true and Git advertises daemon support.
//!
//! The daemon avoids scanning every tracked file and untracked directory:
//! https://github.com/git/git/blob/94f057755b7941b321fd11fec1b2e3ca5313a4e0/Documentation/git-fsmonitor--daemon.adoc#L49-L57
//! https://github.com/git/git/blob/94f057755b7941b321fd11fec1b2e3ca5313a4e0/Documentation/git-update-index.adoc#L545-L550
use std::future::Future;
/// The safe `core.fsmonitor` override for an internal Git command.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FsmonitorOverride {
/// Disable repository-selected filesystem monitor helpers.
Disabled,
/// Preserve Git's built-in filesystem monitor daemon.
BuiltIn,
}
impl FsmonitorOverride {
/// Returns the complete Git configuration override.
pub const fn git_config_arg(self) -> &'static str {
match self {
Self::Disabled => "core.fsmonitor=false",
Self::BuiltIn => "core.fsmonitor=true",
}
}
}
/// Executes the Git commands required by [`detect_fsmonitor_override`].
///
/// Implementations must return stdout only when Git exits successfully.
/// Timeouts, spawn or transport failures, signal termination, and nonzero exit
/// statuses must return `None`.
pub trait FsmonitorProbeRunner: Send {
/// Runs one bounded probe in the target repository.
fn run_probe(&mut self, args: &[&str]) -> impl Future<Output = Option<Vec<u8>>> + Send;
}
/// Returns the safe filesystem monitor override for the target repository.
///
/// This intentionally probes every time. Effective Git configuration is
/// layered, may use conditional includes, and can change while Codex is
/// running:
/// https://git-scm.com/docs/git-config#SCOPES
/// https://git-scm.com/docs/git-config#_conditional_includes
pub async fn detect_fsmonitor_override(
runner: &mut impl FsmonitorProbeRunner,
) -> FsmonitorOverride {
// A typed query converts every matching value before `--get` selects the
// effective one. A shadowed helper path can therefore make a repository-
// local true fail conversion. Query the raw effective value first.
// https://github.com/git/git/blob/94f057755b7941b321fd11fec1b2e3ca5313a4e0/builtin/config.c#L482-L514
// https://github.com/git/git/blob/94f057755b7941b321fd11fec1b2e3ca5313a4e0/builtin/config.c#L611-L614
let Some(config) = runner
.run_probe(&["config", "--null", "--get", "core.fsmonitor"])
.await
else {
return FsmonitorOverride::Disabled;
};
let Some(config) = config.strip_suffix(b"\0") else {
return FsmonitorOverride::Disabled;
};
if config.contains(&0) {
return FsmonitorOverride::Disabled;
}
let Ok(config) = str::from_utf8(config) else {
return FsmonitorOverride::Disabled;
};
// Git accepts these case-insensitive spellings directly, as well as
// valueless keys and nonzero integers. Ask Git to normalize uncommon
// spellings, filtering by the raw effective value before conversion so a
// shadowed helper pathname cannot make the query fail.
// https://github.com/git/git/blob/94f057755b7941b321fd11fec1b2e3ca5313a4e0/parse.c#L158-L181
// https://github.com/git/git/blob/94f057755b7941b321fd11fec1b2e3ca5313a4e0/builtin/config.c#L264-L279
// https://github.com/git/git/blob/94f057755b7941b321fd11fec1b2e3ca5313a4e0/builtin/config.c#L496-L507
let configured = if ["true", "yes", "on"]
.iter()
.any(|value| config.eq_ignore_ascii_case(value))
{
true
} else if ["false", "no", "off"]
.iter()
.any(|value| config.eq_ignore_ascii_case(value))
{
false
} else {
let typed_args = [
"config",
"--null",
"--type=bool",
"--fixed-value",
"--get",
"core.fsmonitor",
config,
];
matches!(
runner.run_probe(&typed_args).await.as_deref(),
Some(b"true\0")
)
};
if !configured {
return FsmonitorOverride::Disabled;
}
// Git 2.35.1 and older interpret "true" as a hook pathname. Before Git
// 2.26, a successful empty hook response can hide tracked changes. Require
// the feature line Git added specifically for capability checks.
// https://github.com/git/git/blob/94f057755b7941b321fd11fec1b2e3ca5313a4e0/Documentation/config/core.adoc#L90-L99
// https://github.com/git/git/commit/dd77cf61a1a2fbf52c94d0cd986d555ad2ba8a4b
let Some(build_options) = runner.run_probe(&["version", "--build-options"]).await else {
return FsmonitorOverride::Disabled;
};
if build_options
.split(|byte| *byte == b'\n')
.any(|line| line.trim_ascii() == b"feature: fsmonitor--daemon")
{
FsmonitorOverride::BuiltIn
} else {
FsmonitorOverride::Disabled
}
}
#[cfg(test)]
#[path = "fsmonitor_tests.rs"]
mod tests;
+139
View File
@@ -0,0 +1,139 @@
use std::collections::VecDeque;
use std::future::Future;
use pretty_assertions::assert_eq;
use super::FsmonitorOverride;
use super::FsmonitorProbeRunner;
use super::detect_fsmonitor_override;
struct ProbeResponse {
args: Vec<&'static str>,
output: Option<Vec<u8>>,
}
struct FakeRunner {
responses: VecDeque<ProbeResponse>,
}
impl FsmonitorProbeRunner for FakeRunner {
fn run_probe(&mut self, args: &[&str]) -> impl Future<Output = Option<Vec<u8>>> + Send {
let response = self.responses.pop_front().expect("missing probe response");
assert_eq!(args, response.args);
std::future::ready(response.output)
}
}
#[tokio::test]
async fn detects_supported_builtin_fsmonitor_values() {
let cases = [
(
"missing config",
vec![response(config_args(), /*output*/ None)],
FsmonitorOverride::Disabled,
),
(
"helper path",
vec![
response(config_args(), Some(b"/tmp/fsmonitor-helper\0")),
response(
typed_config_args("/tmp/fsmonitor-helper"),
/*output*/ None,
),
],
FsmonitorOverride::Disabled,
),
(
"false spelling",
vec![response(config_args(), Some(b"OFF\0"))],
FsmonitorOverride::Disabled,
),
(
"unsupported Git",
vec![
response(config_args(), Some(b"yes\0")),
response(capability_args(), Some(b"")),
],
FsmonitorOverride::Disabled,
),
(
"common true spelling",
vec![
response(config_args(), Some(b"On\0")),
response(capability_args(), Some(fsmonitor_capability())),
],
FsmonitorOverride::BuiltIn,
),
(
"numeric true",
vec![
response(config_args(), Some(b"2k\0")),
response(typed_config_args("2k"), Some(b"true\0")),
response(capability_args(), Some(fsmonitor_capability())),
],
FsmonitorOverride::BuiltIn,
),
(
"valueless true",
vec![
response(config_args(), Some(b"\0")),
response(typed_config_args(""), Some(b"true\0")),
response(capability_args(), Some(fsmonitor_capability())),
],
FsmonitorOverride::BuiltIn,
),
(
"explicit empty false",
vec![
response(config_args(), Some(b"\0")),
response(typed_config_args(""), Some(b"false\0")),
],
FsmonitorOverride::Disabled,
),
];
for (name, responses, expected) in cases {
let mut runner = FakeRunner {
responses: responses.into(),
};
let actual = detect_fsmonitor_override(&mut runner).await;
assert_eq!(
(actual, runner.responses.len()),
(expected, 0),
"case: {name}"
);
}
}
fn response(args: Vec<&'static str>, output: Option<&[u8]>) -> ProbeResponse {
ProbeResponse {
args,
output: output.map(<[u8]>::to_vec),
}
}
fn config_args() -> Vec<&'static str> {
vec!["config", "--null", "--get", "core.fsmonitor"]
}
fn typed_config_args(value: &'static str) -> Vec<&'static str> {
vec![
"config",
"--null",
"--type=bool",
"--fixed-value",
"--get",
"core.fsmonitor",
value,
]
}
fn capability_args() -> Vec<&'static str> {
vec!["version", "--build-options"]
}
fn fsmonitor_capability() -> &'static [u8] {
b"feature: fsmonitor--daemon\n"
}
+209 -10
View File
@@ -279,7 +279,10 @@ fn trim_git_suffix(value: &str) -> &str {
}
pub async fn get_has_changes(cwd: &Path) -> Option<bool> {
let output = run_git_command_with_timeout(&["status", "--porcelain"], cwd).await?;
let git = Path::new("git");
let fsmonitor = detect_local_fsmonitor_override(git, cwd).await;
let output =
run_git_command_with_timeout_from(git, &["status", "--porcelain"], cwd, fsmonitor).await?;
if !output.status.success() {
return None;
}
@@ -390,12 +393,53 @@ pub async fn git_diff_to_remote(cwd: &Path) -> Option<GitDiffToRemote> {
/// Run a git command with a timeout to prevent blocking on large repositories
async fn run_git_command_with_timeout(args: &[&str], cwd: &Path) -> Option<std::process::Output> {
let mut command = Command::new("git");
// These callers only inspect repository metadata. Worktree workflows probe
// once and pass their override directly to the lower-level runner.
run_git_command_with_timeout_from(
Path::new("git"),
args,
cwd,
crate::FsmonitorOverride::Disabled,
)
.await
}
struct LocalFsmonitorProbeRunner<'a> {
git: &'a Path,
cwd: &'a Path,
}
impl crate::FsmonitorProbeRunner for LocalFsmonitorProbeRunner<'_> {
async fn run_probe(&mut self, args: &[&str]) -> Option<Vec<u8>> {
// Both probes are fast, bounded metadata queries that do not inspect the
// worktree or index, so do not reduce the requested command's timeout.
let mut command = Command::new(self.git);
command.args(args).current_dir(self.cwd).kill_on_drop(true);
match timeout(GIT_COMMAND_TIMEOUT, command.output()).await {
Ok(Ok(output)) if output.status.success() => Some(output.stdout),
_ => None,
}
}
}
async fn detect_local_fsmonitor_override(git: &Path, cwd: &Path) -> crate::FsmonitorOverride {
let mut runner = LocalFsmonitorProbeRunner { git, cwd };
crate::detect_fsmonitor_override(&mut runner).await
}
async fn run_git_command_with_timeout_from(
git: &Path,
args: &[&str],
cwd: &Path,
fsmonitor: crate::FsmonitorOverride,
) -> Option<std::process::Output> {
let mut command = Command::new(git);
command
.env("GIT_OPTIONAL_LOCKS", "0")
// Keep internal Git helper commands independent of configured hook directories.
// Keep internal Git commands independent of repository-selected hooks
// and fsmonitor helpers while preserving built-in fsmonitor acceleration.
.args(["-c", &format!("core.hooksPath={DISABLED_HOOKS_PATH}")])
.args(["-c", "core.fsmonitor=false"])
.args(["-c", fsmonitor.git_config_arg()])
.args(args)
.current_dir(cwd)
.kill_on_drop(true);
@@ -684,9 +728,15 @@ async fn find_closest_sha(cwd: &Path, branches: &[String], remotes: &[String]) -
}
async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option<String> {
let output =
run_git_command_with_timeout(&["diff", "--no-textconv", "--no-ext-diff", &sha.0], cwd)
.await?;
let git = Path::new("git");
let fsmonitor = detect_local_fsmonitor_override(git, cwd).await;
let output = run_git_command_with_timeout_from(
git,
&["diff", "--no-textconv", "--no-ext-diff", &sha.0],
cwd,
fsmonitor,
)
.await?;
// 0 is success and no diff.
// 1 is success but there is a diff.
let exit_ok = output.status.code().is_some_and(|c| c == 0 || c == 1);
@@ -695,8 +745,13 @@ async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option<String> {
}
let mut diff = String::from_utf8(output.stdout).ok()?;
if let Some(untracked_output) =
run_git_command_with_timeout(&["ls-files", "--others", "--exclude-standard"], cwd).await
if let Some(untracked_output) = run_git_command_with_timeout_from(
git,
&["ls-files", "--others", "--exclude-standard"],
cwd,
fsmonitor,
)
.await
&& untracked_output.status.success()
{
let untracked: Vec<String> = String::from_utf8(untracked_output.stdout)
@@ -722,7 +777,7 @@ async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option<String> {
null_device,
&file_owned,
];
run_git_command_with_timeout(&args_vec, cwd).await
run_git_command_with_timeout_from(git, &args_vec, cwd, fsmonitor).await
});
let results = join_all(futures_iter).await;
for extra in results.into_iter().flatten() {
@@ -849,6 +904,8 @@ pub async fn current_branch_name(cwd: &Path) -> Option<String> {
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[test]
fn canonicalize_git_remote_url_normalizes_github_variants() {
@@ -886,4 +943,146 @@ mod tests {
assert_eq!(canonicalize_git_remote_url(remote), None);
}
}
#[cfg(unix)]
#[tokio::test]
async fn fsmonitor_override_rejects_configured_helper() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
let git = temp_dir.path().join("git");
let log = temp_dir.path().join("git.log");
std::fs::write(
&git,
"#!/bin/sh\n\
printf '%s\\n' \"$*\" >>\"$0.log\"\n\
case \"$1\" in\n\
config) printf '/tmp/fsmonitor-helper\\000' ;;\n\
*) printf 'worktree output\\n' ;;\n\
esac\n",
)
.expect("write fake Git");
let mut permissions = std::fs::metadata(&git)
.expect("read fake Git metadata")
.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&git, permissions).expect("mark fake Git executable");
// The config response mirrors:
// git -c core.fsmonitor=/tmp/fsmonitor-helper \
// config --null --get core.fsmonitor
let fsmonitor = detect_local_fsmonitor_override(&git, temp_dir.path()).await;
let output = run_git_command_with_timeout_from(
&git,
&["status", "--porcelain"],
temp_dir.path(),
fsmonitor,
)
.await
.expect("run fake Git");
assert_eq!(
(output.status.code(), output.stdout),
(Some(0), b"worktree output\n".to_vec())
);
let disabled_hooks = format!("core.hooksPath={DISABLED_HOOKS_PATH}");
assert_eq!(
std::fs::read_to_string(log)
.expect("read fake Git log")
.lines()
.map(str::to_string)
.collect::<Vec<_>>(),
vec![
"config --null --get core.fsmonitor".to_string(),
"config --null --type=bool --fixed-value --get core.fsmonitor /tmp/fsmonitor-helper"
.to_string(),
format!("-c {disabled_hooks} -c core.fsmonitor=false status --porcelain"),
]
);
}
#[cfg(unix)]
#[tokio::test]
async fn fsmonitor_override_uses_effective_layered_config_value() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
let repo = temp_dir.path().join("repo");
std::fs::create_dir(&repo).expect("create repository directory");
let init_status = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(&repo)
.status()
.expect("initialize test repository");
assert_eq!(init_status.code(), Some(0), "initialize test repository");
let git = temp_dir.path().join("git");
let global_config = temp_dir.path().join("git.global");
let log = temp_dir.path().join("git.log");
std::fs::write(
&git,
"#!/bin/sh\n\
printf '%s\\n' \"$*\" >>\"$0.log\"\n\
case \"$1\" in\n\
config)\n\
GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=\"$0.global\" exec git \"$@\"\n\
;;\n\
version) printf 'feature: fsmonitor--daemon\\n' ;;\n\
*) printf 'worktree output\\n' ;;\n\
esac\n",
)
.expect("write layered-config Git");
let mut permissions = std::fs::metadata(&git)
.expect("read layered-config Git metadata")
.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&git, permissions).expect("mark layered-config Git executable");
let global_status = std::process::Command::new("git")
.args([
"config",
"--file",
global_config.to_str().expect("global config path"),
"core.fsmonitor",
"/tmp/fsmonitor-helper",
])
.status()
.expect("write global fsmonitor helper");
assert_eq!(
global_status.code(),
Some(0),
"write global fsmonitor helper"
);
let local_status = std::process::Command::new("git")
.args(["config", "core.fsmonitor", "true"])
.current_dir(&repo)
.status()
.expect("write local built-in fsmonitor config");
assert_eq!(
local_status.code(),
Some(0),
"write local built-in fsmonitor config"
);
let fsmonitor = detect_local_fsmonitor_override(&git, repo.as_path()).await;
let output = run_git_command_with_timeout_from(
&git,
&["status", "--porcelain"],
repo.as_path(),
fsmonitor,
)
.await
.expect("run Git with layered config");
assert_eq!(
(output.status.code(), output.stdout),
(Some(0), b"worktree output\n".to_vec())
);
let actual = std::fs::read_to_string(log).expect("read layered-config Git log");
let disabled_hooks = format!("core.hooksPath={DISABLED_HOOKS_PATH}");
assert_eq!(
actual.lines().map(str::to_string).collect::<Vec<_>>(),
vec![
"config --null --get core.fsmonitor".to_string(),
"version --build-options".to_string(),
format!("-c {disabled_hooks} -c core.fsmonitor=true status --porcelain"),
]
);
}
}
+4
View File
@@ -2,6 +2,7 @@ mod apply;
mod baseline;
mod branch;
mod errors;
mod fsmonitor;
mod info;
mod operations;
mod platform;
@@ -21,6 +22,9 @@ pub use baseline::reset_git_repository;
pub use branch::merge_base_with_head;
pub use codex_protocol::protocol::GitSha;
pub use errors::GitToolingError;
pub use fsmonitor::FsmonitorOverride;
pub use fsmonitor::FsmonitorProbeRunner;
pub use fsmonitor::detect_fsmonitor_override;
pub use info::CommitLogEntry;
pub use info::GitDiffToRemote;
pub use info::GitInfo;