11use std:: future:: Future ;
2+ use std:: path:: Path ;
23use std:: sync:: Arc ;
34
5+ use codex_analytics:: CodexHookSource ;
46use codex_analytics:: HookRunFact ;
57use codex_analytics:: build_track_events_context;
68use codex_hooks:: PostToolUseOutcome ;
@@ -24,6 +26,7 @@ use serde_json::Value;
2426
2527use crate :: codex:: Session ;
2628use crate :: codex:: TurnContext ;
29+ use crate :: config_loader:: system_config_toml_file;
2730use crate :: event_mapping:: parse_turn_item;
2831
2932pub ( crate ) struct HookRuntimeOutcome {
@@ -358,14 +361,41 @@ fn hook_run_analytics_payload(
358361 ) ,
359362 HookRunFact {
360363 event_name : completed. run . event_name ,
361- source_path : completed. run . source_path . to_path_buf ( ) ,
362- cwd : turn_context. cwd . to_path_buf ( ) ,
364+ hook_source : hook_source_for_path (
365+ completed. run . source_path . as_path ( ) ,
366+ turn_context. config . codex_home . as_path ( ) ,
367+ turn_context. cwd . as_path ( ) ,
368+ ) ,
363369 status : completed. run . status ,
364- duration_ms : completed. run . duration_ms ,
365370 } ,
366371 )
367372}
368373
374+ fn hook_source_for_path ( source_path : & Path , codex_home : & Path , cwd : & Path ) -> CodexHookSource {
375+ if let Ok ( system_config_path) = system_config_toml_file ( )
376+ && let Some ( system_config_dir) = system_config_path. as_path ( ) . parent ( )
377+ && source_path. starts_with ( system_config_dir)
378+ {
379+ return CodexHookSource :: System ;
380+ }
381+
382+ if source_path. starts_with ( codex_home) {
383+ return CodexHookSource :: User ;
384+ }
385+
386+ // Project hooks are loaded from a `.codex/hooks.json` rooted at or above the
387+ // current working directory, so classify by walking cwd ancestors.
388+ if source_path. ends_with ( Path :: new ( ".codex" ) . join ( "hooks.json" ) )
389+ && cwd
390+ . ancestors ( )
391+ . any ( |ancestor| source_path. starts_with ( ancestor. join ( ".codex" ) ) )
392+ {
393+ return CodexHookSource :: Project ;
394+ }
395+
396+ CodexHookSource :: Unknown
397+ }
398+
369399fn hook_permission_mode ( turn_context : & TurnContext ) -> String {
370400 match turn_context. approval_policy . value ( ) {
371401 AskForApproval :: Never => "bypassPermissions" ,
@@ -379,14 +409,14 @@ fn hook_permission_mode(turn_context: &TurnContext) -> String {
379409
380410#[ cfg( test) ]
381411mod tests {
412+ use codex_analytics:: CodexHookSource ;
382413 use codex_protocol:: models:: ContentItem ;
383414 use codex_protocol:: protocol:: HookEventName ;
384415 use codex_protocol:: protocol:: HookExecutionMode ;
385416 use codex_protocol:: protocol:: HookHandlerType ;
386417 use codex_protocol:: protocol:: HookRunStatus ;
387418 use codex_protocol:: protocol:: HookScope ;
388419 use pretty_assertions:: assert_eq;
389- use std:: path:: PathBuf ;
390420
391421 use super :: additional_context_messages;
392422 use super :: hook_run_analytics_payload;
@@ -445,10 +475,8 @@ mod tests {
445475 assert_eq ! ( tracking. turn_id, "turn-from-hook" ) ;
446476 assert_eq ! ( tracking. model_slug, turn_context. model_info. slug) ;
447477 assert_eq ! ( hook. event_name, HookEventName :: Stop ) ;
448- assert_eq ! ( hook. source_path, PathBuf :: from( "/tmp/hooks.json" ) ) ;
449- assert_eq ! ( hook. cwd, turn_context. cwd. to_path_buf( ) ) ;
478+ assert_eq ! ( hook. hook_source, CodexHookSource :: Unknown ) ;
450479 assert_eq ! ( hook. status, HookRunStatus :: Blocked ) ;
451- assert_eq ! ( hook. duration_ms, Some ( 27 ) ) ;
452480 }
453481
454482 #[ tokio:: test]
@@ -466,6 +494,53 @@ mod tests {
466494 assert_eq ! ( hook. status, HookRunStatus :: Failed ) ;
467495 }
468496
497+ #[ test]
498+ fn hook_source_for_path_classifies_user_project_and_unknown ( ) {
499+ let codex_home = test_path_buf ( "/tmp/custom-codex-home" ) . abs ( ) ;
500+ let cwd = test_path_buf ( "/tmp/worktree/src" ) . abs ( ) ;
501+
502+ let system_hooks_path = super :: system_config_toml_file ( )
503+ . expect ( "system config path" )
504+ . parent ( )
505+ . expect ( "system config directory" )
506+ . join ( "hooks.json" ) ;
507+
508+ assert_eq ! (
509+ super :: hook_source_for_path(
510+ system_hooks_path. as_path( ) ,
511+ codex_home. as_path( ) ,
512+ cwd. as_path( ) ,
513+ ) ,
514+ CodexHookSource :: System ,
515+ ) ;
516+ assert_eq ! (
517+ super :: hook_source_for_path(
518+ codex_home. join( "hooks.json" ) . as_path( ) ,
519+ codex_home. as_path( ) ,
520+ cwd. as_path( ) ,
521+ ) ,
522+ CodexHookSource :: User ,
523+ ) ;
524+ assert_eq ! (
525+ super :: hook_source_for_path(
526+ test_path_buf( "/tmp/worktree/.codex/hooks.json" )
527+ . abs( )
528+ . as_path( ) ,
529+ codex_home. as_path( ) ,
530+ cwd. as_path( ) ,
531+ ) ,
532+ CodexHookSource :: Project ,
533+ ) ;
534+ assert_eq ! (
535+ super :: hook_source_for_path(
536+ test_path_buf( "/tmp/hooks.json" ) . abs( ) . as_path( ) ,
537+ codex_home. as_path( ) ,
538+ cwd. as_path( ) ,
539+ ) ,
540+ CodexHookSource :: Unknown ,
541+ ) ;
542+ }
543+
469544 fn sample_hook_run ( status : HookRunStatus ) -> HookRunSummary {
470545 HookRunSummary {
471546 id : "stop:0:/tmp/hooks.json" . to_string ( ) ,
0 commit comments