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 <contact@jakea.net>
This commit is contained in:
Jake Abendroth
2026-08-21 04:21:09 -07:00
parent 5882be29f7
commit 23dfea58b1
4 changed files with 55 additions and 12 deletions
+5 -4
View File
@@ -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 `<binary> 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) {
+1 -1
View File
@@ -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);
+3 -3
View File
@@ -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;
+46 -4
View File
@@ -21,6 +21,7 @@
#include <foundation/constants.h>
#include <foundation/platform.h>
#include <mcp/mcp.h>
#include <pipeline/pipeline.h>
#include <foundation/yaml.h>
#include <store/store.h>
#include <yyjson/yyjson.h>
@@ -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);