@@ -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+
214236fn 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