@@ -1015,6 +1015,204 @@ static bool ha_dialect_event_supported(ha_lifecycle_dialect_t dialect, const cha
10151015 return ha_lifecycle_event_supported (event );
10161016}
10171017
1018+ /* ── Bash search-command pattern extractor ────────────────────────────────
1019+ * Tokenises and walks a Bash tool command to extract a search pattern for
1020+ * graph augmentation. Returns true and fills out when one clear pattern is
1021+ * found; false on unrecognised binary, -f pattern-file, multiple -e, or any
1022+ * other ambiguity. Never executes or rewrites the command. */
1023+
1024+ #define HA_BASH_TOK_MAX 32
1025+ #define HA_BASH_TOK_SZ 256
1026+
1027+ static int ha_tokenize (const char * cmd , char toks [][HA_BASH_TOK_SZ ], int max ) {
1028+ int n = 0 ;
1029+ const char * p = cmd ;
1030+ while (* p && n < max ) {
1031+ while (* p && isspace ((unsigned char )* p ))
1032+ p ++ ;
1033+ if (!* p )
1034+ break ;
1035+ char * d = toks [n ];
1036+ int dlen = 0 ;
1037+ while (* p && !isspace ((unsigned char )* p )) {
1038+ if (* p == '\'' ) {
1039+ for (p ++ ; * p && * p != '\'' ; p ++ )
1040+ if (dlen < HA_BASH_TOK_SZ - 1 )
1041+ d [dlen ++ ] = * p ;
1042+ if (* p == '\'' )
1043+ p ++ ;
1044+ } else if (* p == '"' ) {
1045+ for (p ++ ; * p && * p != '"' ; p ++ ) {
1046+ if (* p == '\\' && p [1 ] && strchr ("\\\"$`" , p [1 ]))
1047+ p ++ ;
1048+ if (dlen < HA_BASH_TOK_SZ - 1 )
1049+ d [dlen ++ ] = * p ;
1050+ }
1051+ if (* p == '"' )
1052+ p ++ ;
1053+ } else if (* p == '\\' && p [1 ]) {
1054+ p ++ ;
1055+ if (dlen < HA_BASH_TOK_SZ - 1 )
1056+ d [dlen ++ ] = * p ++ ;
1057+ } else {
1058+ if (dlen < HA_BASH_TOK_SZ - 1 )
1059+ d [dlen ++ ] = * p ++ ;
1060+ }
1061+ }
1062+ d [dlen ] = '\0' ;
1063+ if (dlen > 0 )
1064+ n ++ ;
1065+ }
1066+ return n ;
1067+ }
1068+
1069+ static bool ha_is_env_assign (const char * t ) {
1070+ if (!t || !t [0 ])
1071+ return false;
1072+ if (!isalpha ((unsigned char )t [0 ]) && t [0 ] != '_' )
1073+ return false;
1074+ const char * p = t + 1 ;
1075+ while (isalnum ((unsigned char )* p ) || * p == '_' )
1076+ p ++ ;
1077+ return * p == '=' ;
1078+ }
1079+
1080+ typedef enum { HA_BIN_GREP , HA_BIN_RG , HA_BIN_AG , HA_BIN_ACK , HA_BIN_UGREP } ha_bin_t ;
1081+
1082+ static const char * ha_search_bin_val_flags (ha_bin_t bin , bool rtk_grep ) {
1083+ switch (bin ) {
1084+ case HA_BIN_RG :
1085+ return "ABCmtTgMP" ;
1086+ case HA_BIN_AG :
1087+ return "ABCmpG" ;
1088+ default :
1089+ return rtk_grep ? "ABCmdDl" : "ABCmdD" ;
1090+ }
1091+ }
1092+
1093+ static bool ha_parse_bash_search_pattern (const char * cmd , char * out , size_t out_sz ) {
1094+ if (!cmd || !out || out_sz == 0 )
1095+ return false;
1096+ char toks [HA_BASH_TOK_MAX ][HA_BASH_TOK_SZ ];
1097+ int n = ha_tokenize (cmd , toks , HA_BASH_TOK_MAX );
1098+ if (n == 0 )
1099+ return false;
1100+
1101+ int i = 0 ;
1102+ while (i < n && ha_is_env_assign (toks [i ]))
1103+ i ++ ;
1104+ if (i >= n )
1105+ return false;
1106+
1107+ bool rtk = false;
1108+ for (;;) {
1109+ const char * t = toks [i ];
1110+ if (strcmp (t , "env" ) == 0 || strcmp (t , "nice" ) == 0 || strcmp (t , "time" ) == 0 ||
1111+ strcmp (t , "command" ) == 0 ) {
1112+ i ++ ;
1113+ } else if (strcmp (t , "rtk" ) == 0 ) {
1114+ rtk = true;
1115+ i ++ ;
1116+ } else if (strcmp (t , "tokf" ) == 0 && i + 1 < n && strcmp (toks [i + 1 ], "run" ) == 0 ) {
1117+ i += 2 ;
1118+ } else {
1119+ break ;
1120+ }
1121+ while (i < n && ha_is_env_assign (toks [i ]))
1122+ i ++ ;
1123+ if (i >= n )
1124+ return false;
1125+ }
1126+
1127+ const char * bin_tok = toks [i ++ ];
1128+ ha_bin_t bin ;
1129+
1130+ if (strcmp (bin_tok , "grep" ) == 0 || strcmp (bin_tok , "egrep" ) == 0 ||
1131+ strcmp (bin_tok , "fgrep" ) == 0 ) {
1132+ bin = HA_BIN_GREP ;
1133+ } else if (strcmp (bin_tok , "rg" ) == 0 ) {
1134+ bin = HA_BIN_RG ;
1135+ } else if (strcmp (bin_tok , "ag" ) == 0 ) {
1136+ bin = HA_BIN_AG ;
1137+ } else if (strcmp (bin_tok , "ack" ) == 0 ) {
1138+ bin = HA_BIN_ACK ;
1139+ } else if (strcmp (bin_tok , "ugrep" ) == 0 || strcmp (bin_tok , "ug" ) == 0 ) {
1140+ bin = HA_BIN_UGREP ;
1141+ } else if (strcmp (bin_tok , "git" ) == 0 ) {
1142+ if (i >= n || strcmp (toks [i ], "grep" ) != 0 )
1143+ return false;
1144+ i ++ ;
1145+ bin = HA_BIN_GREP ;
1146+ } else {
1147+ return false;
1148+ }
1149+
1150+ const char * val_flags = ha_search_bin_val_flags (bin , rtk && bin == HA_BIN_GREP );
1151+ const char * pattern = NULL ;
1152+ int e_count = 0 ;
1153+ bool end_of_flags = false;
1154+
1155+ for (; i < n ; i ++ ) {
1156+ const char * t = toks [i ];
1157+
1158+ if (end_of_flags || t [0 ] != '-' || t [1 ] == '\0' ) {
1159+ if (!pattern )
1160+ pattern = t ;
1161+ else
1162+ break ;
1163+ continue ;
1164+ }
1165+
1166+ if (t [1 ] == '-' ) {
1167+ if (t [2 ] == '\0' ) {
1168+ end_of_flags = true;
1169+ continue ;
1170+ }
1171+ const char * name = t + 2 ;
1172+ const char * eq = strchr (name , '=' );
1173+ size_t nlen = eq ? (size_t )(eq - name ) : strlen (name );
1174+ if ((nlen == 6 && strncmp (name , "regexp" , 6 ) == 0 ) ||
1175+ (nlen == 7 && strncmp (name , "pattern" , 7 ) == 0 )) {
1176+ pattern = eq ? eq + 1 : (i + 1 < n ? toks [++ i ] : NULL );
1177+ e_count ++ ;
1178+ } else if (nlen == 4 && strncmp (name , "file" , 4 ) == 0 ) {
1179+ return false;
1180+ }
1181+ continue ;
1182+ }
1183+
1184+ const char * f = t + 1 ;
1185+ bool consumed_next = false;
1186+ while (* f ) {
1187+ char flag = * f ++ ;
1188+ if (flag == 'e' ) {
1189+ if (* f ) {
1190+ pattern = f ;
1191+ f += strlen (f );
1192+ } else if (!consumed_next && i + 1 < n ) {
1193+ pattern = toks [++ i ];
1194+ consumed_next = true;
1195+ }
1196+ e_count ++ ;
1197+ } else if (flag == 'f' ) {
1198+ return false;
1199+ } else if (strchr (val_flags , flag )) {
1200+ if (* f ) {
1201+ f += strlen (f );
1202+ } else if (!consumed_next && i + 1 < n ) {
1203+ i ++ ;
1204+ consumed_next = true;
1205+ }
1206+ }
1207+ }
1208+ }
1209+
1210+ if (e_count > 1 || !pattern || !pattern [0 ])
1211+ return false;
1212+ int w = snprintf (out , out_sz , "%s" , pattern );
1213+ return w > 0 && (size_t )w < out_sz ;
1214+ }
1215+
10181216static bool ha_tool_event_supported (ha_lifecycle_dialect_t dialect , const char * event ,
10191217 const char * tool , bool * coverage ) {
10201218 if (coverage ) {
@@ -1025,7 +1223,8 @@ static bool ha_tool_event_supported(ha_lifecycle_dialect_t dialect, const char *
10251223 }
10261224 if (dialect == HA_DIALECT_EVENT ) {
10271225 if (strcmp (event , "PreToolUse" ) == 0 &&
1028- (strcmp (tool , "Grep" ) == 0 || strcmp (tool , "Glob" ) == 0 )) {
1226+ (strcmp (tool , "Grep" ) == 0 || strcmp (tool , "Glob" ) == 0 ||
1227+ strcmp (tool , "Bash" ) == 0 )) {
10291228 return true;
10301229 }
10311230 if (strcmp (event , "PostToolUse" ) == 0 && strcmp (tool , "Read" ) == 0 ) {
@@ -1310,6 +1509,10 @@ bool cbm_hook_path_contains_for_testing(const char *root, const char *candidate,
13101509const char * cbm_hook_no_project_index_guidance_for_testing (const char * event ) {
13111510 return ha_no_project_index_guidance (event );
13121511}
1512+
1513+ bool cbm_hook_augment_parse_bash_pattern_for_testing (const char * cmd , char * out , size_t out_sz ) {
1514+ return ha_parse_bash_search_pattern (cmd , out , out_sz );
1515+ }
13131516#endif
13141517
13151518static char * ha_process (cbm_mcp_server_t * srv , const char * input_json , const char * forced_event ,
@@ -1358,7 +1561,18 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
13581561 return NULL ;
13591562 }
13601563
1361- const char * pattern = ha_obj_str (tin , "pattern" );
1564+ char bash_pattern [HA_BASH_TOK_SZ ];
1565+ const char * pattern ;
1566+ if (strcmp (tool , "Bash" ) == 0 ) {
1567+ const char * cmd = ha_obj_str (tin , "command" );
1568+ if (!ha_parse_bash_search_pattern (cmd , bash_pattern , sizeof (bash_pattern ))) {
1569+ yyjson_doc_free (doc );
1570+ return NULL ;
1571+ }
1572+ pattern = bash_pattern ;
1573+ } else {
1574+ pattern = ha_obj_str (tin , "pattern" );
1575+ }
13621576 char token [HA_MAX_TOKEN + 1 ];
13631577 if (!ha_extract_token (pattern , token , sizeof (token ))) {
13641578 yyjson_doc_free (doc );
0 commit comments