@@ -38,14 +38,14 @@ use crate::cli::agent::hook::{
3838 HookTrigger ,
3939} ;
4040use crate :: cli:: agent:: is_mcp_tool_ref;
41- use crate :: util:: MCP_SERVER_TOOL_DELIMITER ;
4241use crate :: cli:: chat:: consts:: AGENT_FORMAT_HOOKS_DOC_URL ;
4342use crate :: cli:: chat:: util:: truncate_safe;
4443use crate :: cli:: chat:: {
4544 ChatError ,
4645 ChatSession ,
4746 ChatState ,
4847} ;
48+ use crate :: util:: MCP_SERVER_TOOL_DELIMITER ;
4949use crate :: util:: pattern_matching:: matches_any_pattern;
5050
5151/// Hook execution result: (exit_code, output)
@@ -58,26 +58,29 @@ fn hook_matches_tool(hook: &Hook, tool_name: &str) -> bool {
5858 None => true , // No matcher means the hook runs for all tools
5959 Some ( pattern) => {
6060 match pattern. as_str ( ) {
61- "*" => true , // Wildcard matches all tools
61+ "*" => true , // Wildcard matches all tools
6262 "@builtin" => !is_mcp_tool_ref ( tool_name) , // Built-in tools are not MCP tools
6363 _ => {
6464 // If tool_name is MCP, check server pattern first
6565 if is_mcp_tool_ref ( tool_name) {
66- if let Some ( server_name) = tool_name. strip_prefix ( '@' ) . and_then ( |s| s. split ( MCP_SERVER_TOOL_DELIMITER ) . next ( ) ) {
66+ if let Some ( server_name) = tool_name
67+ . strip_prefix ( '@' )
68+ . and_then ( |s| s. split ( MCP_SERVER_TOOL_DELIMITER ) . next ( ) )
69+ {
6770 let server_pattern = format ! ( "@{}" , server_name) ;
6871 if pattern == & server_pattern {
6972 return true ;
7073 }
7174 }
7275 }
73-
76+
7477 // Use matches_any_pattern for both MCP and built-in tools
7578 let mut patterns = std:: collections:: HashSet :: new ( ) ;
7679 patterns. insert ( pattern. clone ( ) ) ;
7780 matches_any_pattern ( & patterns, tool_name)
78- }
81+ } ,
7982 }
80- }
83+ } ,
8184 }
8285}
8386
@@ -133,7 +136,7 @@ impl HookExecutor {
133136 continue ; // Skip this hook - doesn't match tool
134137 }
135138 }
136-
139+
137140 if let Some ( cache) = self . get_cache ( & hook) {
138141 // Note: we only cache successful hook run. hence always using 0 as exit code for cached hook
139142 cached. push ( ( hook. clone ( ) , ( 0 , cache) ) ) ;
@@ -203,7 +206,11 @@ impl HookExecutor {
203206 style:: Print ( & hook. 1 . command) ,
204207 style:: Print ( "\" " ) ,
205208 style:: SetForegroundColor ( style:: Color :: Red ) ,
206- style:: Print ( format!( " failed with exit code: {}, stderr: {})\n " , exit_code, hook_output. trim_end( ) ) ) ,
209+ style:: Print ( format!(
210+ " failed with exit code: {}, stderr: {})\n " ,
211+ exit_code,
212+ hook_output. trim_end( )
213+ ) ) ,
207214 style:: ResetColor ,
208215 ) ?;
209216 } else {
@@ -437,11 +444,16 @@ impl HooksArgs {
437444
438445#[ cfg( test) ]
439446mod tests {
440- use super :: * ;
441447 use std:: collections:: HashMap ;
442- use crate :: cli :: agent :: hook :: { Hook , HookTrigger } ;
448+
443449 use tempfile:: TempDir ;
444450
451+ use super :: * ;
452+ use crate :: cli:: agent:: hook:: {
453+ Hook ,
454+ HookTrigger ,
455+ } ;
456+
445457 #[ test]
446458 fn test_hook_matches_tool ( ) {
447459 let hook_no_matcher = Hook {
@@ -452,7 +464,7 @@ mod tests {
452464 matcher : None ,
453465 source : crate :: cli:: agent:: hook:: Source :: Session ,
454466 } ;
455-
467+
456468 let fs_write_hook = Hook {
457469 command : "echo test" . to_string ( ) ,
458470 timeout_ms : 5000 ,
@@ -461,7 +473,7 @@ mod tests {
461473 matcher : Some ( "fs_write" . to_string ( ) ) ,
462474 source : crate :: cli:: agent:: hook:: Source :: Session ,
463475 } ;
464-
476+
465477 let fs_wildcard_hook = Hook {
466478 command : "echo test" . to_string ( ) ,
467479 timeout_ms : 5000 ,
@@ -470,7 +482,7 @@ mod tests {
470482 matcher : Some ( "fs_*" . to_string ( ) ) ,
471483 source : crate :: cli:: agent:: hook:: Source :: Session ,
472484 } ;
473-
485+
474486 let all_tools_hook = Hook {
475487 command : "echo test" . to_string ( ) ,
476488 timeout_ms : 5000 ,
@@ -479,7 +491,7 @@ mod tests {
479491 matcher : Some ( "*" . to_string ( ) ) ,
480492 source : crate :: cli:: agent:: hook:: Source :: Session ,
481493 } ;
482-
494+
483495 let builtin_hook = Hook {
484496 command : "echo test" . to_string ( ) ,
485497 timeout_ms : 5000 ,
@@ -488,7 +500,7 @@ mod tests {
488500 matcher : Some ( "@builtin" . to_string ( ) ) ,
489501 source : crate :: cli:: agent:: hook:: Source :: Session ,
490502 } ;
491-
503+
492504 let git_server_hook = Hook {
493505 command : "echo test" . to_string ( ) ,
494506 timeout_ms : 5000 ,
@@ -497,7 +509,7 @@ mod tests {
497509 matcher : Some ( "@git" . to_string ( ) ) ,
498510 source : crate :: cli:: agent:: hook:: Source :: Session ,
499511 } ;
500-
512+
501513 let git_status_hook = Hook {
502514 command : "echo test" . to_string ( ) ,
503515 timeout_ms : 5000 ,
@@ -506,36 +518,36 @@ mod tests {
506518 matcher : Some ( "@git/status" . to_string ( ) ) ,
507519 source : crate :: cli:: agent:: hook:: Source :: Session ,
508520 } ;
509-
521+
510522 // No matcher should match all tools
511523 assert ! ( hook_matches_tool( & hook_no_matcher, "fs_write" ) ) ;
512524 assert ! ( hook_matches_tool( & hook_no_matcher, "execute_bash" ) ) ;
513525 assert ! ( hook_matches_tool( & hook_no_matcher, "@git/status" ) ) ;
514-
526+
515527 // Exact matcher should only match exact tool
516528 assert ! ( hook_matches_tool( & fs_write_hook, "fs_write" ) ) ;
517529 assert ! ( !hook_matches_tool( & fs_write_hook, "fs_read" ) ) ;
518-
530+
519531 // Wildcard matcher should match pattern
520532 assert ! ( hook_matches_tool( & fs_wildcard_hook, "fs_write" ) ) ;
521533 assert ! ( hook_matches_tool( & fs_wildcard_hook, "fs_read" ) ) ;
522534 assert ! ( !hook_matches_tool( & fs_wildcard_hook, "execute_bash" ) ) ;
523-
535+
524536 // * should match all tools
525537 assert ! ( hook_matches_tool( & all_tools_hook, "fs_write" ) ) ;
526538 assert ! ( hook_matches_tool( & all_tools_hook, "execute_bash" ) ) ;
527539 assert ! ( hook_matches_tool( & all_tools_hook, "@git/status" ) ) ;
528-
540+
529541 // @builtin should match built-in tools only
530542 assert ! ( hook_matches_tool( & builtin_hook, "fs_write" ) ) ;
531543 assert ! ( hook_matches_tool( & builtin_hook, "execute_bash" ) ) ;
532544 assert ! ( !hook_matches_tool( & builtin_hook, "@git/status" ) ) ;
533-
545+
534546 // @git should match all git server tools
535547 assert ! ( hook_matches_tool( & git_server_hook, "@git/status" ) ) ;
536548 assert ! ( !hook_matches_tool( & git_server_hook, "@other/tool" ) ) ;
537549 assert ! ( !hook_matches_tool( & git_server_hook, "fs_write" ) ) ;
538-
550+
539551 // @git/status should match exact MCP tool
540552 assert ! ( hook_matches_tool( & git_status_hook, "@git/status" ) ) ;
541553 assert ! ( !hook_matches_tool( & git_status_hook, "@git/commit" ) ) ;
@@ -546,18 +558,18 @@ mod tests {
546558 async fn test_hook_executor_with_tool_context ( ) {
547559 let mut executor = HookExecutor :: new ( ) ;
548560 let mut output = Vec :: new ( ) ;
549-
561+
550562 // Create temp directory and file
551563 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
552564 let test_file = temp_dir. path ( ) . join ( "hook_output.json" ) ;
553565 let test_file_str = test_file. to_string_lossy ( ) ;
554-
566+
555567 // Create a simple hook that writes JSON input to a file
556568 #[ cfg( unix) ]
557569 let command = format ! ( "cat > {}" , test_file_str) ;
558570 #[ cfg( windows) ]
559571 let command = format ! ( "type > {}" , test_file_str) ;
560-
572+
561573 let hook = Hook {
562574 command,
563575 timeout_ms : 5000 ,
@@ -566,10 +578,10 @@ mod tests {
566578 matcher : Some ( "fs_write" . to_string ( ) ) ,
567579 source : crate :: cli:: agent:: hook:: Source :: Session ,
568580 } ;
569-
581+
570582 let mut hooks = HashMap :: new ( ) ;
571583 hooks. insert ( HookTrigger :: PreToolUse , vec ! [ hook] ) ;
572-
584+
573585 let tool_context = ToolContext {
574586 tool_name : "fs_write" . to_string ( ) ,
575587 tool_input : serde_json:: json!( {
@@ -578,18 +590,14 @@ mod tests {
578590 } ) ,
579591 tool_response : None ,
580592 } ;
581-
593+
582594 // Run the hook
583- let result = executor. run_hooks (
584- hooks,
585- & mut output,
586- "." ,
587- None ,
588- Some ( tool_context)
589- ) . await ;
590-
595+ let result = executor
596+ . run_hooks ( hooks, & mut output, "." , None , Some ( tool_context) )
597+ . await ;
598+
591599 assert ! ( result. is_ok( ) ) ;
592-
600+
593601 // Verify the hook wrote the JSON input to the file
594602 if let Ok ( content) = std:: fs:: read_to_string ( & test_file) {
595603 let json: serde_json:: Value = serde_json:: from_str ( & content) . unwrap ( ) ;
@@ -605,7 +613,7 @@ mod tests {
605613 async fn test_hook_filtering_no_match ( ) {
606614 let mut executor = HookExecutor :: new ( ) ;
607615 let mut output = Vec :: new ( ) ;
608-
616+
609617 // Hook that matches execute_bash (should NOT run for fs_write tool call)
610618 let execute_bash_hook = Hook {
611619 command : "echo 'should not run'" . to_string ( ) ,
@@ -615,31 +623,33 @@ mod tests {
615623 matcher : Some ( "execute_bash" . to_string ( ) ) ,
616624 source : crate :: cli:: agent:: hook:: Source :: Session ,
617625 } ;
618-
626+
619627 let mut hooks = HashMap :: new ( ) ;
620628 hooks. insert ( HookTrigger :: PostToolUse , vec ! [ execute_bash_hook] ) ;
621-
629+
622630 let tool_context = ToolContext {
623631 tool_name : "fs_write" . to_string ( ) ,
624632 tool_input : serde_json:: json!( { "command" : "create" } ) ,
625633 tool_response : Some ( serde_json:: json!( { "success" : true } ) ) ,
626634 } ;
627-
635+
628636 // Run the hooks
629- let result = executor. run_hooks (
630- hooks,
631- & mut output,
632- "." , // cwd - using current directory for now
633- None , // prompt - no user prompt for this test
634- Some ( tool_context)
635- ) . await ;
636-
637+ let result = executor
638+ . run_hooks (
639+ hooks,
640+ & mut output,
641+ "." , // cwd - using current directory for now
642+ None , // prompt - no user prompt for this test
643+ Some ( tool_context) ,
644+ )
645+ . await ;
646+
637647 assert ! ( result. is_ok( ) ) ;
638648 let hook_results = result. unwrap ( ) ;
639-
649+
640650 // Should run 0 hooks because matcher doesn't match tool_name
641651 assert_eq ! ( hook_results. len( ) , 0 ) ;
642-
652+
643653 // Output should be empty since no hooks ran
644654 assert ! ( output. is_empty( ) ) ;
645655 }
@@ -654,7 +664,7 @@ mod tests {
654664 let command = "echo 'Tool execution blocked by security policy' >&2; exit 2" ;
655665 #[ cfg( windows) ]
656666 let command = "echo Tool execution blocked by security policy 1>&2 & exit /b 2" ;
657-
667+
658668 let hook = Hook {
659669 command : command. to_string ( ) ,
660670 timeout_ms : 5000 ,
@@ -664,9 +674,7 @@ mod tests {
664674 source : crate :: cli:: agent:: hook:: Source :: Session ,
665675 } ;
666676
667- let hooks = HashMap :: from ( [
668- ( HookTrigger :: PreToolUse , vec ! [ hook] )
669- ] ) ;
677+ let hooks = HashMap :: from ( [ ( HookTrigger :: PreToolUse , vec ! [ hook] ) ] ) ;
670678
671679 let tool_context = ToolContext {
672680 tool_name : "fs_write" . to_string ( ) ,
@@ -677,17 +685,20 @@ mod tests {
677685 tool_response : None ,
678686 } ;
679687
680- let results = executor. run_hooks (
681- hooks,
682- & mut output,
683- "." , // cwd
684- None , // prompt
685- Some ( tool_context)
686- ) . await . unwrap ( ) ;
688+ let results = executor
689+ . run_hooks (
690+ hooks,
691+ & mut output,
692+ "." , // cwd
693+ None , // prompt
694+ Some ( tool_context) ,
695+ )
696+ . await
697+ . unwrap ( ) ;
687698
688699 // Should have one result
689700 assert_eq ! ( results. len( ) , 1 ) ;
690-
701+
691702 let ( ( trigger, _hook) , ( exit_code, hook_output) ) = & results[ 0 ] ;
692703 assert_eq ! ( * trigger, HookTrigger :: PreToolUse ) ;
693704 assert_eq ! ( * exit_code, 2 ) ;
0 commit comments