From 23dfea58b17ee37397403effe56ff2252e9ff88e Mon Sep 17 00:00:00 2001 From: Jake Abendroth Date: Fri, 21 Aug 2026 04:21:09 -0700 Subject: [PATCH] fix(hook-augment): register Bash search matcher Wire Bash into the installed Claude PreToolUse matcher, add end-to-end reachability coverage, migrate the previous Grep|Glob matcher, and remove the unused rtk_grep parameter. Signed-off-by: Jake Abendroth --- src/cli/cli.c | 9 ++++---- src/cli/cli.h | 2 +- src/cli/hook_augment.c | 6 ++--- tests/test_cli.c | 50 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/cli/cli.c b/src/cli/cli.c index 8010e2258..6a8515c3b 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -3819,9 +3819,9 @@ static int cbm_remove_vibe_mcp_owned(const char *binary_path, const char *config /* ── Claude Code pre-tool hooks ───────────────────────────────── */ -/* Search augmentation runs before Grep/Glob; exact coverage context runs after +/* Search augmentation runs before Grep/Glob/Bash; exact coverage context runs after * Read. Both adapters are context-only and fail open. */ -#define CMM_HOOK_SEARCH_MATCHER "Grep|Glob" +#define CMM_HOOK_SEARCH_MATCHER "Grep|Glob|Bash" #define CMM_HOOK_READ_MATCHER "Read" /* Basename only; the full command path is resolved at install time via * cbm_resolve_hook_command so $CLAUDE_CONFIG_DIR is honored. */ @@ -3845,6 +3845,7 @@ static int cbm_remove_vibe_mcp_owned(const char *binary_path, const char *config static const char *const cmm_claude_old_matchers[] = { "Grep|Glob|Read|Search", "Grep|Glob|Read", + "Grep|Glob", NULL, }; static const char *const cmm_gemini_old_matchers[] = { @@ -4961,7 +4962,7 @@ static int cbm_remove_owned_hook_script(const char *path, const char *expected_c /* Install the search-augmenter shim to ~/.claude/hooks/. * The shim is a thin wrapper that delegates to ` hook-augment`, - * which adds graph context to Grep/Glob calls. It NEVER blocks a tool call: + * which adds graph context to Grep/Glob/Bash search calls. It NEVER blocks a tool call: * a missing/old/hung binary results in a silent exit 0 (issue #362/#288). * The legacy filename `cbm-code-discovery-gate` is retained so existing * settings.json entries and uninstall keep working with zero migration. */ @@ -7204,7 +7205,7 @@ static void install_claude_code_config(const char *home, const char *binary_path } } if (gate_ok) { - printf(" hooks: PreToolUse Grep/Glob search augmentation + PostToolUse Read coverage " + printf(" hooks: PreToolUse Grep/Glob/Bash search augmentation + PostToolUse Read coverage " "(non-blocking)\n"); } if (session_ok) { diff --git a/src/cli/cli.h b/src/cli/cli.h index 4857be2a5..2cec80c05 100644 --- a/src/cli/cli.h +++ b/src/cli/cli.h @@ -459,7 +459,7 @@ int cbm_cmd_config(int argc, char **argv); /* hook-augment: stdin-driven Claude Code PreToolUse augmenter. * Reads the hook JSON from stdin and emits hookSpecificOutput.additionalContext - * with search_graph hits for Grep/Glob calls. NEVER blocks: every failure + * with search_graph hits for Grep/Glob/Bash search calls. NEVER blocks: every failure * path returns 0 with no stdout output. */ int cbm_cmd_hook_augment(int argc, char **argv); diff --git a/src/cli/hook_augment.c b/src/cli/hook_augment.c index 9ac8015d4..0a12538ea 100644 --- a/src/cli/hook_augment.c +++ b/src/cli/hook_augment.c @@ -13,7 +13,7 @@ * * The underlying query is `search_graph` (pure SQLite, shell-free) — chosen * over `search_code` (which shells out to grep|xargs) so the hook stays cheap - * enough to run before every Grep/Glob. + * enough to run before every Grep/Glob/Bash call. */ #include "cli/cli.h" @@ -1079,7 +1079,7 @@ static bool ha_is_env_assign(const char *t) { typedef enum { HA_BIN_GREP, HA_BIN_RG, HA_BIN_AG, HA_BIN_ACK, HA_BIN_UGREP } ha_bin_t; -static const char *ha_search_bin_val_flags(ha_bin_t bin, bool rtk_grep) { +static const char *ha_search_bin_val_flags(ha_bin_t bin) { switch (bin) { case HA_BIN_RG: return "ABCmtTgMP"; @@ -1147,7 +1147,7 @@ static bool ha_parse_bash_search_pattern(const char *cmd, char *out, size_t out_ return false; } - const char *val_flags = ha_search_bin_val_flags(bin, rtk && bin == HA_BIN_GREP); + const char *val_flags = ha_search_bin_val_flags(bin); const char *pattern = NULL; int e_count = 0; bool end_of_flags = false; diff --git a/tests/test_cli.c b/tests/test_cli.c index fe2eb39aa..6ab6e44b2 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -8791,6 +8792,44 @@ TEST(cli_hook_augment_context_tracks_search_json_shape) { PASS(); } +/* Exercise the real PreToolUse + Bash route instead of calling only the + * tokenizer seam. This pins the event guard and the graph lookup path together. + */ +TEST(cli_hook_augment_bash_pretooluse_reaches_augmenter) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + cbm_store_t *st = cbm_mcp_server_store(srv); + ASSERT_NOT_NULL(st); + char *project = cbm_project_name_from_path("/tmp/hookproj"); + ASSERT_NOT_NULL(project); + cbm_mcp_server_set_project(srv, project); + cbm_store_upsert_project(st, project, "/tmp/hookproj"); + char qualified_name[256]; + snprintf(qualified_name, sizeof(qualified_name), "%s.mod.someIndexedSymbol", project); + cbm_node_t node = {.project = project, + .label = "Function", + .name = "someIndexedSymbol", + .qualified_name = qualified_name, + .file_path = "mod.py", + .start_line = 1, + .end_line = 4}; + ASSERT_GT(cbm_store_upsert_node(st, &node), 0); + + const char *input = "{\"hook_event_name\":\"PreToolUse\",\"tool_name\":\"Bash\"," + "\"cwd\":\"/tmp/hookproj\",\"tool_input\":{" + "\"command\":\"rg -n someIndexedSymbol .\"}}"; + char *output = cbm_hook_augment_process(srv, input); + bool reached = output && strstr(output, "hookSpecificOutput") && + strstr(output, "someIndexedSymbol") && strstr(output, "mod.py"); + free(output); + cbm_mcp_server_free(srv); + free(project); + + if (!reached) + FAIL("PreToolUse Bash must reach graph search augmentation"); + PASS(); +} + TEST(cli_hook_augment_bash_pattern_extractor) { char out[256]; @@ -10439,7 +10478,7 @@ TEST(cli_upsert_claude_hook_fresh) { ASSERT_NOT_NULL(data); ASSERT(strstr(data, "PreToolUse") != NULL); ASSERT(strstr(data, "PostToolUse") != NULL); - ASSERT(strstr(data, "\"Grep|Glob\"") != NULL); + ASSERT(strstr(data, "\"Grep|Glob|Bash\"") != NULL); ASSERT(strstr(data, "\"Read\"") != NULL); ASSERT(strstr(data, "\"Grep|Glob|Read\"") == NULL); ASSERT_EQ(test_count_substring(data, "cbm-code-discovery-gate"), 2U); @@ -10953,7 +10992,7 @@ TEST(cli_upsert_claude_hook_existing) { const char *data = read_test_file(settingspath); ASSERT_NOT_NULL(data); - ASSERT(strstr(data, "\"Grep|Glob\"") != NULL); + ASSERT(strstr(data, "\"Grep|Glob|Bash\"") != NULL); ASSERT(strstr(data, "PostToolUse") != NULL); ASSERT(strstr(data, "\"Read\"") != NULL); /* Existing hook preserved */ @@ -11034,7 +11073,8 @@ TEST(cli_upsert_claude_hook_replace) { const char *data = read_test_file(settingspath); ASSERT_NOT_NULL(data); ASSERT(strstr(data, "\"Grep|Glob|Read\"") == NULL); - ASSERT(strstr(data, "\"Grep|Glob\"") != NULL); + ASSERT(strstr(data, "\"Grep|Glob|Bash\"") != NULL); + ASSERT(strstr(data, "\"Grep|Glob\"") == NULL); ASSERT(strstr(data, "PostToolUse") != NULL); ASSERT_EQ(test_count_substring(data, "cbm-code-discovery-gate"), 2U); @@ -11086,7 +11126,8 @@ TEST(cli_remove_claude_hooks) { const char *data = read_test_file(settingspath); ASSERT_NOT_NULL(data); - ASSERT(strstr(data, "Grep|Glob|Read") == NULL); + ASSERT(strstr(data, "\"Grep|Glob|Bash\"") == NULL); + ASSERT(strstr(data, "\"Grep|Glob\"") == NULL); ASSERT(strstr(data, "cbm-code-discovery-gate") == NULL); test_rmdir_r(tmpdir); @@ -12039,6 +12080,7 @@ SUITE(cli) { RUN_TEST(cli_claude_hook_commands_shell_quote_custom_config_dir); RUN_TEST(cli_codex_migrates_to_single_hook_representation); RUN_TEST(cli_hook_augment_context_tracks_search_json_shape); + RUN_TEST(cli_hook_augment_bash_pretooluse_reaches_augmenter); RUN_TEST(cli_hook_augment_bash_pattern_extractor); RUN_TEST(cli_hook_augment_lifecycle_output_contract); RUN_TEST(cli_hook_augment_subagent_tier_router_contract);