Skip to content

Commit 4203752

Browse files
authored
Merge pull request #1435 from DeusData/fix/1387-install-preserves-hooks
fix(install): never remove existing hook entries on a refused script rewrite (#1387)
2 parents ff891d0 + 55c2994 commit 4203752

2 files changed

Lines changed: 110 additions & 3 deletions

File tree

‎src/cli/cli.c‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7177,9 +7177,13 @@ static void install_claude_code_config(const char *home, const char *binary_path
71777177
char hook_path[CLI_BUF_1K];
71787178
gate_ok = cbm_install_hook_gate_script(home, binary_path);
71797179
snprintf(hook_path, sizeof(hook_path), "%s/hooks/%s", config_dir, CMM_HOOK_GATE_SCRIPT);
7180+
/* #1387: a failed script (re)write must never remove existing hook
7181+
* entries. The common failure is TEXT_UNOWNED - a script the user
7182+
* modified or a manual install wrote with another binary path - and
7183+
* that script still works; deleting the registration turns a skipped
7184+
* update into config loss. Entry removal belongs to uninstall only. */
71807185
if (!gate_ok) {
71817186
record_agent_config_error(false, "Claude Code", "hook_script_install", hook_path);
7182-
(void)cbm_remove_claude_hooks(settings_path);
71837187
} else if (cbm_upsert_claude_hooks(settings_path) != CLI_OK) {
71847188
gate_ok = false;
71857189
record_agent_config_error(false, "Claude Code", "hook_register", settings_path);
@@ -7190,7 +7194,6 @@ static void install_claude_code_config(const char *home, const char *binary_path
71907194
CMM_SESSION_REMINDER_SCRIPT);
71917195
if (!session_ok) {
71927196
record_agent_config_error(false, "Claude Code", "hook_script_install", hook_path);
7193-
(void)cbm_remove_session_hooks(settings_path);
71947197
} else if (cbm_upsert_session_hooks(settings_path) != CLI_OK) {
71957198
session_ok = false;
71967199
record_agent_config_error(false, "Claude Code", "hook_register", settings_path);
@@ -7201,7 +7204,6 @@ static void install_claude_code_config(const char *home, const char *binary_path
72017204
CMM_SUBAGENT_REMINDER_SCRIPT);
72027205
if (!subagent_ok) {
72037206
record_agent_config_error(false, "Claude Code", "hook_script_install", hook_path);
7204-
(void)cbm_remove_claude_subagent_hooks(settings_path);
72057207
} else if (cbm_upsert_claude_subagent_hooks(settings_path) != CLI_OK) {
72067208
subagent_ok = false;
72077209
record_agent_config_error(false, "Claude Code", "hook_register", settings_path);

‎tests/test_cli.c‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4670,6 +4670,108 @@ TEST(cli_agent_reinstall_preserves_foreign_policy_entries) {
46704670
PASS();
46714671
}
46724672

4673+
#ifndef _WIN32
4674+
/* Regression for #1387: installing over an existing setup whose hook scripts
4675+
* are not byte-owned (manual install with a custom binary location, or a
4676+
* user-modified reminder) must NOT remove the existing, working hook entries
4677+
* from settings.json. The refused script rewrite is reported as an error and
4678+
* both the scripts and the entries survive byte-identically. */
4679+
TEST(cli_install_preserves_hook_entries_when_scripts_unowned_issue1387) {
4680+
char tmpdir[256];
4681+
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hooks-preserve-XXXXXX");
4682+
if (!cbm_mkdtemp(tmpdir))
4683+
FAIL("cbm_mkdtemp failed");
4684+
4685+
char hooks_dir[512];
4686+
snprintf(hooks_dir, sizeof(hooks_dir), "%s/.claude/hooks", tmpdir);
4687+
if (!cbm_mkdir_p(hooks_dir, 0755))
4688+
FAIL("mkdir hooks_dir failed");
4689+
4690+
/* Gate script written by a manual install: BIN outside the managed target,
4691+
* so its bytes match no current or released installer-owned shape. */
4692+
static const char unowned_gate[] =
4693+
"#!/usr/bin/env bash\n"
4694+
"# codebase-memory-mcp search augmenter (Claude Code PreToolUse).\n"
4695+
"BIN=\"/opt/tools/cbm/codebase-memory-mcp\"\n"
4696+
"[ -x \"$BIN\" ] || exit 0\n"
4697+
"\"$BIN\" hook-augment 2>/dev/null\n"
4698+
"exit 0\n";
4699+
char gate_path[768];
4700+
snprintf(gate_path, sizeof(gate_path), "%s/cbm-code-discovery-gate", hooks_dir);
4701+
write_test_file(gate_path, unowned_gate);
4702+
4703+
/* Session reminder carrying a user-added line. */
4704+
static const char unowned_session[] =
4705+
"#!/usr/bin/env bash\n"
4706+
"# SessionStart hook: remind agent to use codebase-memory-mcp tools.\n"
4707+
"echo my-extra-team-reminder\n";
4708+
char session_path[768];
4709+
snprintf(session_path, sizeof(session_path), "%s/cbm-session-reminder", hooks_dir);
4710+
write_test_file(session_path, unowned_session);
4711+
4712+
static const char settings_before[] =
4713+
"{\n"
4714+
" \"hooks\": {\n"
4715+
" \"PreToolUse\": [\n"
4716+
" {\"matcher\": \"Grep|Glob\", \"hooks\": [{\"type\": \"command\", "
4717+
"\"command\": \"~/.claude/hooks/cbm-code-discovery-gate\", \"timeout\": 5}]},\n"
4718+
" {\"matcher\": \"Bash\", \"hooks\": [{\"type\": \"command\", "
4719+
"\"command\": \"/usr/local/bin/my-own-guard\"}]}\n"
4720+
" ],\n"
4721+
" \"SessionStart\": [\n"
4722+
" {\"matcher\": \"startup\", \"hooks\": [{\"type\": \"command\", "
4723+
"\"command\": \"~/.claude/hooks/cbm-session-reminder\"}]},\n"
4724+
" {\"matcher\": \"resume\", \"hooks\": [{\"type\": \"command\", "
4725+
"\"command\": \"~/.claude/hooks/cbm-session-reminder\"}]},\n"
4726+
" {\"matcher\": \"clear\", \"hooks\": [{\"type\": \"command\", "
4727+
"\"command\": \"~/.claude/hooks/cbm-session-reminder\"}]},\n"
4728+
" {\"matcher\": \"compact\", \"hooks\": [{\"type\": \"command\", "
4729+
"\"command\": \"~/.claude/hooks/cbm-session-reminder\"}]}\n"
4730+
" ]\n"
4731+
" }\n"
4732+
"}\n";
4733+
char settings_path[768];
4734+
snprintf(settings_path, sizeof(settings_path), "%s/.claude/settings.json", tmpdir);
4735+
write_test_file(settings_path, settings_before);
4736+
4737+
const char *const env_names[] = {"HOME", "PATH", "CLAUDE_CONFIG_DIR"};
4738+
char *saved[sizeof(env_names) / sizeof(env_names[0])];
4739+
for (size_t i = 0U; i < sizeof(env_names) / sizeof(env_names[0]); i++) {
4740+
saved[i] = save_test_env(env_names[i]);
4741+
cbm_unsetenv(env_names[i]);
4742+
}
4743+
cbm_setenv("HOME", tmpdir, 1);
4744+
cbm_setenv("PATH", tmpdir, 1);
4745+
4746+
int install_rc =
4747+
cbm_install_agent_configs(tmpdir, "/opt/other/codebase-memory-mcp", false, false);
4748+
4749+
char *settings = read_test_file_alloc(settings_path);
4750+
char *gate = read_test_file_alloc(gate_path);
4751+
char *session = read_test_file_alloc(session_path);
4752+
bool preserved =
4753+
install_rc != 0 /* the refused rewrites are reported, not silent */
4754+
&& settings && strstr(settings, "~/.claude/hooks/cbm-code-discovery-gate") &&
4755+
strstr(settings, "Grep|Glob") && strstr(settings, "/usr/local/bin/my-own-guard") &&
4756+
strstr(settings, "\"startup\"") && strstr(settings, "\"resume\"") &&
4757+
strstr(settings, "\"clear\"") && strstr(settings, "\"compact\"") &&
4758+
strstr(settings, "~/.claude/hooks/cbm-session-reminder") && gate &&
4759+
strcmp(gate, unowned_gate) == 0 && session && strcmp(session, unowned_session) == 0;
4760+
free(settings);
4761+
free(gate);
4762+
free(session);
4763+
4764+
for (size_t i = 0U; i < sizeof(env_names) / sizeof(env_names[0]); i++) {
4765+
restore_test_env(env_names[i], saved[i]);
4766+
}
4767+
test_rmdir_r(tmpdir);
4768+
if (!preserved)
4769+
FAIL("install must preserve existing hook entries and scripts when the on-disk "
4770+
"scripts are unowned (#1387)");
4771+
PASS();
4772+
}
4773+
#endif
4774+
46734775
TEST(cli_existing_agents_install_durable_child_context) {
46744776
char tmpdir[256];
46754777
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-durable-agents-XXXXXX");
@@ -11936,6 +12038,9 @@ SUITE(cli) {
1193612038
RUN_TEST(cli_new_agent_install_plans_use_documented_paths);
1193712039
RUN_TEST(cli_new_agent_configs_use_documented_schemas);
1193812040
RUN_TEST(cli_agent_reinstall_preserves_foreign_policy_entries);
12041+
#ifndef _WIN32
12042+
RUN_TEST(cli_install_preserves_hook_entries_when_scripts_unowned_issue1387);
12043+
#endif
1193912044
RUN_TEST(cli_existing_agents_install_durable_child_context);
1194012045
RUN_TEST(cli_durable_profiles_follow_current_vendor_paths);
1194112046
RUN_TEST(cli_cline_data_dir_only_redirects_data_state);

0 commit comments

Comments
 (0)