Skip to content

Commit fc203c6

Browse files
committed
fix(cli): remove owned Claude scripts with no-hooks
Signed-off-by: Andy11-cpu <canada11@duck.com>
1 parent 4407046 commit fc203c6

3 files changed

Lines changed: 134 additions & 83 deletions

File tree

‎scripts/smoke-test.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,6 +1692,7 @@ echo "OK 8e: shim installed, non-blocking, delegates to hook-augment"
16921692
HOME="$FAKE_HOME" XDG_CONFIG_HOME="$FAKE_HOME/.config" APPDATA="$FAKE_HOME/AppData/Roaming" LOCALAPPDATA="$FAKE_HOME/AppData/Local" PATH="$FAKE_HOME/.local/bin:$PATH" \
16931693
"$BINARY" install --no-hooks --skip-binary --clients=claude -y >/dev/null
16941694
if grep -qE 'cbm-(code-discovery-gate|session-reminder|subagent-reminder)' "$FAKE_HOME/.claude/settings.json" 2>/dev/null ||
1695+
[ -e "$FAKE_HOME/.claude/hooks/cbm-code-discovery-gate" ] ||
16951696
! grep -q 'codebase-memory-mcp' "$FAKE_HOME/.claude.json" 2>/dev/null; then
16961697
echo "FAIL 8e-i: --no-hooks did not remove owned Claude hooks while preserving MCP"
16971698
exit 1

‎src/cli/cli.c‎

Lines changed: 89 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -7646,6 +7646,93 @@ static void uninstall_tiered_profile_prompts(const char *label, const char *veri
76467646
cbm_graph_profile_dialect_t dialect,
76477647
const char *legacy_verify_content, bool dry_run);
76487648

7649+
static int remove_claude_owned_hook_scripts(const char *config_dir, const char *binary_path,
7650+
bool uninstall) {
7651+
char current_gate[CLI_BUF_8K];
7652+
char current_session[CLI_BUF_8K];
7653+
char current_subagent[CLI_BUF_8K];
7654+
char released_gate[CLI_BUF_8K];
7655+
const char *const gate_legacy[] = {released_gate};
7656+
const char *const session_legacy[] = {cmm_released_session_script};
7657+
const char *const subagent_legacy[] = {cmm_released_subagent_script};
7658+
size_t gate_legacy_count =
7659+
cbm_build_released_gate_script(binary_path, released_gate, sizeof(released_gate)) == CLI_OK
7660+
? 1U
7661+
: 0U;
7662+
static const struct {
7663+
const char *name;
7664+
const char *legacy_name;
7665+
const char *prefix;
7666+
} hook_types[] = {
7667+
{CMM_HOOK_GATE_SCRIPT, CMM_HOOK_GATE_SCRIPT_LEGACY, cmm_gate_script_prefix},
7668+
{CMM_SESSION_REMINDER_SCRIPT, CMM_SESSION_REMINDER_SCRIPT_LEGACY,
7669+
cmm_session_script_prefix},
7670+
{CMM_SUBAGENT_REMINDER_SCRIPT, CMM_SUBAGENT_REMINDER_SCRIPT_LEGACY,
7671+
cmm_subagent_script_prefix},
7672+
};
7673+
struct {
7674+
const char *name;
7675+
const char *legacy_name;
7676+
const char *current;
7677+
const char *const *legacy;
7678+
size_t legacy_count;
7679+
bool current_valid;
7680+
} owned_scripts[] = {
7681+
{hook_types[0].name, hook_types[0].legacy_name, current_gate, gate_legacy,
7682+
gate_legacy_count,
7683+
cbm_build_current_hook_script(hook_types[0].prefix, binary_path, current_gate,
7684+
sizeof(current_gate)) == CLI_OK},
7685+
{hook_types[1].name, hook_types[1].legacy_name, current_session, session_legacy, 1U,
7686+
cbm_build_current_hook_script(hook_types[1].prefix, binary_path, current_session,
7687+
sizeof(current_session)) == CLI_OK},
7688+
{hook_types[2].name, hook_types[2].legacy_name, current_subagent, subagent_legacy, 1U,
7689+
cbm_build_current_hook_script(hook_types[2].prefix, binary_path, current_subagent,
7690+
sizeof(current_subagent)) == CLI_OK},
7691+
};
7692+
char hooks_dir[CLI_BUF_1K];
7693+
int hooks_written = snprintf(hooks_dir, sizeof(hooks_dir), "%s/hooks", config_dir);
7694+
bool hooks_dir_valid = hooks_written > 0 && (size_t)hooks_written < sizeof(hooks_dir);
7695+
int rc = CLI_OK;
7696+
const char *op = uninstall ? "hook_script_uninstall" : "hook_script_disable";
7697+
for (size_t i = 0; i < sizeof(owned_scripts) / sizeof(owned_scripts[0]); i++) {
7698+
char script_path[CLI_BUF_1K];
7699+
int script_written = hooks_dir_valid ? snprintf(script_path, sizeof(script_path), "%s/%s",
7700+
hooks_dir, owned_scripts[i].name)
7701+
: CLI_ERR;
7702+
bool script_path_valid = script_written > 0 && (size_t)script_written < sizeof(script_path);
7703+
if (!owned_scripts[i].current_valid) {
7704+
record_agent_config_error(uninstall, "Claude Code", op, owned_scripts[i].name);
7705+
rc = CLI_ERR;
7706+
continue;
7707+
}
7708+
if (!script_path_valid ||
7709+
cbm_remove_owned_hook_script(script_path, owned_scripts[i].current,
7710+
owned_scripts[i].legacy,
7711+
owned_scripts[i].legacy_count) < CLI_OK) {
7712+
record_agent_config_error(uninstall, "Claude Code", op,
7713+
script_path_valid ? script_path : owned_scripts[i].name);
7714+
rc = CLI_ERR;
7715+
}
7716+
#ifdef _WIN32
7717+
if (!hooks_dir_valid ||
7718+
cbm_remove_owned_legacy_hook_script(hooks_dir, owned_scripts[i].legacy_name,
7719+
owned_scripts[i].current, owned_scripts[i].legacy,
7720+
owned_scripts[i].legacy_count) != CLI_OK) {
7721+
char legacy_path[CLI_BUF_1K];
7722+
int written = hooks_dir_valid ? snprintf(legacy_path, sizeof(legacy_path), "%s/%s",
7723+
hooks_dir, owned_scripts[i].legacy_name)
7724+
: CLI_ERR;
7725+
record_agent_config_error(uninstall, "Claude Code", "legacy_hook_script_uninstall",
7726+
written > 0 && (size_t)written < sizeof(legacy_path)
7727+
? legacy_path
7728+
: owned_scripts[i].legacy_name);
7729+
rc = CLI_ERR;
7730+
}
7731+
#endif
7732+
}
7733+
return rc;
7734+
}
7735+
76497736
static void install_claude_code_config(const char *home, const char *binary_path, bool force,
76507737
bool dry_run) {
76517738
char config_dir[CLI_BUF_1K];
@@ -7735,6 +7822,7 @@ static void install_claude_code_config(const char *home, const char *binary_path
77357822
if (cbm_remove_claude_subagent_hooks(settings_path) != CLI_OK) {
77367823
record_agent_config_error(false, "Claude Code", "hook_disable", settings_path);
77377824
}
7825+
remove_claude_owned_hook_scripts(config_dir, binary_path, false);
77387826
}
77397827
print_hooks_skipped();
77407828
goto claude_hooks_done;
@@ -10707,86 +10795,7 @@ static void uninstall_claude_code(const char *home, bool dry_run) {
1070710795
record_agent_config_error(true, "Claude Code", "subagent_hook_uninstall",
1070810796
settings_path);
1070910797
}
10710-
char current_gate[CLI_BUF_8K];
10711-
char current_session[CLI_BUF_8K];
10712-
char current_subagent[CLI_BUF_8K];
10713-
char released_gate[CLI_BUF_8K];
10714-
const char *const gate_legacy[] = {released_gate};
10715-
const char *const session_legacy[] = {cmm_released_session_script};
10716-
const char *const subagent_legacy[] = {cmm_released_subagent_script};
10717-
size_t gate_legacy_count = cbm_build_released_gate_script(installed_binary, released_gate,
10718-
sizeof(released_gate)) == CLI_OK
10719-
? 1U
10720-
: 0U;
10721-
static const struct {
10722-
const char *name;
10723-
const char *legacy_name;
10724-
const char *prefix;
10725-
} hook_types[] = {
10726-
{CMM_HOOK_GATE_SCRIPT, CMM_HOOK_GATE_SCRIPT_LEGACY, cmm_gate_script_prefix},
10727-
{CMM_SESSION_REMINDER_SCRIPT, CMM_SESSION_REMINDER_SCRIPT_LEGACY,
10728-
cmm_session_script_prefix},
10729-
{CMM_SUBAGENT_REMINDER_SCRIPT, CMM_SUBAGENT_REMINDER_SCRIPT_LEGACY,
10730-
cmm_subagent_script_prefix},
10731-
};
10732-
struct {
10733-
const char *name;
10734-
const char *legacy_name;
10735-
const char *current;
10736-
const char *const *legacy;
10737-
size_t legacy_count;
10738-
bool current_valid;
10739-
} owned_scripts[] = {
10740-
{hook_types[0].name, hook_types[0].legacy_name, current_gate, gate_legacy,
10741-
gate_legacy_count,
10742-
cbm_build_current_hook_script(hook_types[0].prefix, installed_binary, current_gate,
10743-
sizeof(current_gate)) == CLI_OK},
10744-
{hook_types[1].name, hook_types[1].legacy_name, current_session, session_legacy, 1U,
10745-
cbm_build_current_hook_script(hook_types[1].prefix, installed_binary, current_session,
10746-
sizeof(current_session)) == CLI_OK},
10747-
{hook_types[2].name, hook_types[2].legacy_name, current_subagent, subagent_legacy, 1U,
10748-
cbm_build_current_hook_script(hook_types[2].prefix, installed_binary, current_subagent,
10749-
sizeof(current_subagent)) == CLI_OK},
10750-
};
10751-
char hooks_dir[CLI_BUF_1K];
10752-
int hooks_written = snprintf(hooks_dir, sizeof(hooks_dir), "%s/hooks", config_dir);
10753-
bool hooks_dir_valid = hooks_written > 0 && (size_t)hooks_written < sizeof(hooks_dir);
10754-
for (size_t i = 0; i < sizeof(owned_scripts) / sizeof(owned_scripts[0]); i++) {
10755-
char script_path[CLI_BUF_1K];
10756-
int script_written = hooks_dir_valid
10757-
? snprintf(script_path, sizeof(script_path), "%s/%s",
10758-
hooks_dir, owned_scripts[i].name)
10759-
: CLI_ERR;
10760-
bool script_path_valid =
10761-
script_written > 0 && (size_t)script_written < sizeof(script_path);
10762-
if (!owned_scripts[i].current_valid) {
10763-
record_agent_config_error(true, "Claude Code", "hook_script_uninstall",
10764-
owned_scripts[i].name);
10765-
continue;
10766-
}
10767-
if (!script_path_valid ||
10768-
cbm_remove_owned_hook_script(script_path, owned_scripts[i].current,
10769-
owned_scripts[i].legacy,
10770-
owned_scripts[i].legacy_count) < CLI_OK) {
10771-
record_agent_config_error(true, "Claude Code", "hook_script_uninstall",
10772-
script_path_valid ? script_path : owned_scripts[i].name);
10773-
}
10774-
#ifdef _WIN32
10775-
if (!hooks_dir_valid ||
10776-
cbm_remove_owned_legacy_hook_script(
10777-
hooks_dir, owned_scripts[i].legacy_name, owned_scripts[i].current,
10778-
owned_scripts[i].legacy, owned_scripts[i].legacy_count) != CLI_OK) {
10779-
char legacy_path[CLI_BUF_1K];
10780-
int written = hooks_dir_valid ? snprintf(legacy_path, sizeof(legacy_path), "%s/%s",
10781-
hooks_dir, owned_scripts[i].legacy_name)
10782-
: CLI_ERR;
10783-
record_agent_config_error(true, "Claude Code", "legacy_hook_script_uninstall",
10784-
written > 0 && (size_t)written < sizeof(legacy_path)
10785-
? legacy_path
10786-
: owned_scripts[i].legacy_name);
10787-
}
10788-
#endif
10789-
}
10798+
remove_claude_owned_hook_scripts(config_dir, installed_binary, true);
1079010799
}
1079110800
printf(" removed PreToolUse + SessionStart + SubagentStart hooks\n");
1079210801
}

‎tests/test_cli.c‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,11 +1175,23 @@ TEST(cli_install_hooks_preference_persists_and_migrates_owned_state) {
11751175
char settings_path[768];
11761176
char mcp_path[768];
11771177
char db_path[768];
1178+
char gate_path[768];
1179+
char session_path[768];
1180+
char subagent_path[768];
11781181
snprintf(cache_dir, sizeof(cache_dir), "%s/cache", tmpdir);
11791182
snprintf(claude_dir, sizeof(claude_dir), "%s/.claude", tmpdir);
11801183
snprintf(settings_path, sizeof(settings_path), "%s/settings.json", claude_dir);
11811184
snprintf(mcp_path, sizeof(mcp_path), "%s/.claude.json", tmpdir);
11821185
snprintf(db_path, sizeof(db_path), "%s/_config.db", cache_dir);
1186+
#ifdef _WIN32
1187+
snprintf(gate_path, sizeof(gate_path), "%s/hooks/cbm-code-discovery-gate.cmd", claude_dir);
1188+
snprintf(session_path, sizeof(session_path), "%s/hooks/cbm-session-reminder.cmd", claude_dir);
1189+
snprintf(subagent_path, sizeof(subagent_path), "%s/hooks/cbm-subagent-reminder.cmd", claude_dir);
1190+
#else
1191+
snprintf(gate_path, sizeof(gate_path), "%s/hooks/cbm-code-discovery-gate", claude_dir);
1192+
snprintf(session_path, sizeof(session_path), "%s/hooks/cbm-session-reminder", claude_dir);
1193+
snprintf(subagent_path, sizeof(subagent_path), "%s/hooks/cbm-subagent-reminder", claude_dir);
1194+
#endif
11831195
test_mkdirp(claude_dir);
11841196
write_test_file(settings_path,
11851197
"{\"hooks\":{\"PreToolUse\":[{\"matcher\":\"Bash\",\"hooks\":[{\"type\":"
@@ -1202,6 +1214,8 @@ TEST(cli_install_hooks_preference_persists_and_migrates_owned_state) {
12021214
char *default_state = read_test_file_alloc(settings_path);
12031215
bool default_hook = default_state && strstr(default_state, "cbm-code-discovery-gate") &&
12041216
strstr(default_state, "/usr/bin/foreign-hook");
1217+
bool default_scripts = cbm_file_exists(gate_path) && cbm_file_exists(session_path) &&
1218+
cbm_file_exists(subagent_path);
12051219
free(default_state);
12061220

12071221
char *no_hooks_plan_args[] = {"--plan", "--no-hooks"};
@@ -1237,6 +1251,8 @@ TEST(cli_install_hooks_preference_persists_and_migrates_owned_state) {
12371251
bool disabled_ok = disabled_state && strstr(disabled_state, "/usr/bin/foreign-hook") &&
12381252
!strstr(disabled_state, "cbm-code-discovery-gate") && disabled_mcp &&
12391253
strstr(disabled_mcp, "codebase-memory-mcp");
1254+
bool no_hooks_scripts_gone = !cbm_file_exists(gate_path) && !cbm_file_exists(session_path) &&
1255+
!cbm_file_exists(subagent_path);
12401256
free(disabled_state);
12411257
free(disabled_mcp);
12421258

@@ -1245,6 +1261,9 @@ TEST(cli_install_hooks_preference_persists_and_migrates_owned_state) {
12451261
disabled_state = read_test_file_alloc(settings_path);
12461262
bool bare_disabled = disabled_state && strstr(disabled_state, "/usr/bin/foreign-hook") &&
12471263
!strstr(disabled_state, "cbm-code-discovery-gate");
1264+
bool bare_scripts_gone =
1265+
!cbm_file_exists(gate_path) && !cbm_file_exists(session_path) &&
1266+
!cbm_file_exists(subagent_path);
12481267
free(disabled_state);
12491268

12501269
char *hooks_plan_args[] = {"--plan", "--hooks"};
@@ -1263,14 +1282,36 @@ TEST(cli_install_hooks_preference_persists_and_migrates_owned_state) {
12631282
char *enabled_state = read_test_file_alloc(settings_path);
12641283
bool enabled_ok = enabled_state && strstr(enabled_state, "cbm-code-discovery-gate") &&
12651284
strstr(enabled_state, "/usr/bin/foreign-hook");
1285+
bool hooks_scripts_back = cbm_file_exists(gate_path) && cbm_file_exists(session_path) &&
1286+
cbm_file_exists(subagent_path);
12661287
free(enabled_state);
12671288
int bare_enabled_rc = cli_test_cmd_install(3, bare_args);
1289+
bool bare_scripts_enabled = cbm_file_exists(gate_path) && cbm_file_exists(session_path) &&
1290+
cbm_file_exists(subagent_path);
1291+
1292+
const char *foreign_session_script =
1293+
#ifdef _WIN32
1294+
"@echo off\r\nC:\\tmp\\user-owned-session-hook.cmd\r\n";
1295+
#else
1296+
"#!/bin/sh\n/usr/bin/user-owned-session-hook\n";
1297+
#endif
1298+
write_test_file(session_path, foreign_session_script);
1299+
int foreign_no_hooks_rc = cli_test_cmd_install(4, no_hooks_args);
1300+
char *preserved_session = read_test_file_alloc(session_path);
1301+
bool foreign_session_preserved =
1302+
foreign_no_hooks_rc == 0 && preserved_session &&
1303+
strcmp(preserved_session, foreign_session_script) == 0 && !cbm_file_exists(gate_path) &&
1304+
!cbm_file_exists(subagent_path);
1305+
free(preserved_session);
1306+
12681307
bool results_ok =
12691308
conflict_a_rc == 1 && conflict_b_rc == 1 && default_rc == 0 && default_hook &&
1270-
no_hooks_plan_rc == 0 && disabled_plan_ok && no_hooks_dry_rc == 0 && previews_read_only &&
1271-
no_hooks_rc == 0 && stored_false && disabled_ok && bare_disabled_rc == 0 && bare_disabled &&
1309+
default_scripts && no_hooks_plan_rc == 0 && disabled_plan_ok && no_hooks_dry_rc == 0 &&
1310+
previews_read_only && no_hooks_rc == 0 && stored_false && disabled_ok &&
1311+
no_hooks_scripts_gone && bare_disabled_rc == 0 && bare_disabled && bare_scripts_gone &&
12721312
hooks_plan_rc == 0 && hooks_dry_rc == 0 && previews_retained_false && hooks_rc == 0 &&
1273-
stored_true && enabled_ok && bare_enabled_rc == 0 && fake.mutation_reserve_count > 0;
1313+
stored_true && enabled_ok && hooks_scripts_back && bare_enabled_rc == 0 &&
1314+
bare_scripts_enabled && foreign_session_preserved && fake.mutation_reserve_count > 0;
12741315

12751316
cbm_cli_set_activation_ops_for_test(NULL);
12761317
cbm_set_auto_answer_for_test(0);

0 commit comments

Comments
 (0)