Skip to content

Commit 6c76410

Browse files
fix(hooks): ignore unrelated hook cwd roots
1 parent a815454 commit 6c76410

1 file changed

Lines changed: 67 additions & 9 deletions

File tree

src/mcp/hook_events.rs

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ fn plan_shell_hook_event(
137137
return HookEventPlan::Noop;
138138
};
139139
let cwd = event.cwd.as_deref().unwrap_or(project_root);
140-
let hook_project_root = hook_project_root(cwd, project_root);
140+
let Some(hook_project_root) = hook_project_root(cwd, project_root) else {
141+
return HookEventPlan::Noop;
142+
};
141143
if !crate::hooks::cursor_shell_command_targets_project(command, cwd, &hook_project_root) {
142144
return HookEventPlan::Noop;
143145
}
@@ -180,19 +182,20 @@ fn plan_shell_hook_event(
180182
}
181183
}
182184

183-
fn hook_project_root(cwd: &Path, project_root: &Path) -> PathBuf {
185+
fn hook_project_root(cwd: &Path, project_root: &Path) -> Option<PathBuf> {
184186
if let Some(root) = crate::config::discover_project_root(cwd) {
185-
return root;
187+
if root_belongs_to_project(&root, project_root) {
188+
return Some(root);
189+
}
190+
return None;
186191
}
187192
let Some(worktree_root) = crate::worktree::git_worktree_root(cwd) else {
188-
return project_root.to_path_buf();
193+
return path_is_inside(cwd, project_root).then(|| project_root.to_path_buf());
189194
};
190-
let cwd_common = crate::worktree::git_common_dir(&worktree_root);
191-
let project_common = crate::worktree::git_common_dir(project_root);
192-
if cwd_common.is_some() && cwd_common == project_common {
193-
worktree_root
195+
if git_roots_share_common_dir(&worktree_root, project_root) {
196+
Some(worktree_root)
194197
} else {
195-
project_root.to_path_buf()
198+
None
196199
}
197200
}
198201

@@ -211,6 +214,25 @@ fn branch_plan_for_root(
211214
}
212215
}
213216

217+
fn root_belongs_to_project(root: &Path, project_root: &Path) -> bool {
218+
paths_same(root, project_root) || git_roots_share_common_dir(root, project_root)
219+
}
220+
221+
fn path_is_inside(path: &Path, root: &Path) -> bool {
222+
let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
223+
let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
224+
path.starts_with(root)
225+
}
226+
227+
fn git_roots_share_common_dir(a: &Path, b: &Path) -> bool {
228+
let a_common = crate::worktree::git_common_dir(a);
229+
let b_common = crate::worktree::git_common_dir(b);
230+
a_common
231+
.as_ref()
232+
.zip(b_common.as_ref())
233+
.is_some_and(|(a_common, b_common)| paths_same(a_common, b_common))
234+
}
235+
214236
fn paths_same(a: &Path, b: &Path) -> bool {
215237
let a = a.canonicalize().unwrap_or_else(|_| a.to_path_buf());
216238
let b = b.canonicalize().unwrap_or_else(|_| b.to_path_buf());
@@ -308,6 +330,16 @@ mod tests {
308330
assert_eq!(branch, expected_branch);
309331
}
310332

333+
fn write_project_marker(root: &Path) {
334+
let db_path = crate::config::get_project_db_path(root);
335+
let Some(parent) = db_path.parent() else {
336+
panic!("db path should have parent");
337+
};
338+
std::fs::create_dir_all(parent)
339+
.unwrap_or_else(|e| panic!("project marker dir should create: {e}"));
340+
std::fs::write(db_path, b"").unwrap_or_else(|e| panic!("project marker should write: {e}"));
341+
}
342+
311343
#[test]
312344
fn parses_agent_and_event_kind_from_hook_notification() {
313345
let params = json!({
@@ -470,6 +502,32 @@ mod tests {
470502
);
471503
}
472504

505+
#[test]
506+
fn ignores_shell_branch_add_from_unrelated_project_root() {
507+
let base = tempfile::tempdir().unwrap_or_else(|e| panic!("tempdir should create: {e}"));
508+
let project_root = base.path().join("project");
509+
let unrelated_root = base.path().join("unrelated");
510+
std::fs::create_dir_all(&project_root)
511+
.unwrap_or_else(|e| panic!("project root should create: {e}"));
512+
std::fs::create_dir_all(&unrelated_root)
513+
.unwrap_or_else(|e| panic!("unrelated root should create: {e}"));
514+
write_project_marker(&project_root);
515+
write_project_marker(&unrelated_root);
516+
517+
let params = json!({
518+
"agent": "codex",
519+
"event": "postToolUseShell",
520+
"command": "git switch feature/unrelated",
521+
"cwd": unrelated_root
522+
});
523+
let event = parse_or_panic(&params);
524+
525+
assert_eq!(
526+
plan_hook_event(&event, &project_root, Some("feature/unrelated")),
527+
HookEventPlan::Noop
528+
);
529+
}
530+
473531
#[test]
474532
fn plans_worktree_add_against_new_worktree_root() {
475533
let params = json!({

0 commit comments

Comments
 (0)