Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 41 additions & 28 deletions src/cli/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -2990,7 +2990,7 @@ static void plan_record(const char *agent, const char *kind, const char *path) {
}

static void install_claude_code_config(const char *home, const char *binary_path, bool force,
bool dry_run) {
bool dry_run, bool install_hooks) {
char config_dir[CLI_BUF_1K];
cbm_claude_config_dir(home, config_dir, sizeof(config_dir));
char user_root[CLI_BUF_1K];
Expand All @@ -3010,9 +3010,13 @@ static void install_claude_code_config(const char *home, const char *binary_path
snprintf(p, sizeof(p), "%s/settings.json", config_dir);
plan_record("Claude Code", "mcp_config", p);
snprintf(p, sizeof(p), "%s/hooks/%s", config_dir, CMM_HOOK_GATE_SCRIPT);
plan_record("Claude Code", "hook", p);
if (install_hooks) {
plan_record("Claude Code", "hook", p);
}
snprintf(p, sizeof(p), "%s/hooks/%s", config_dir, CMM_SESSION_REMINDER_SCRIPT);
plan_record("Claude Code", "hook", p);
if (install_hooks) {
plan_record("Claude Code", "hook", p);
}
return;
}

Expand Down Expand Up @@ -3041,14 +3045,16 @@ static void install_claude_code_config(const char *home, const char *binary_path

char settings_path[CLI_BUF_1K];
snprintf(settings_path, sizeof(settings_path), "%s/settings.json", config_dir);
if (!dry_run) {
if (!dry_run && install_hooks) {
cbm_upsert_claude_hooks(settings_path);
cbm_install_hook_gate_script(home, binary_path);
cbm_install_session_reminder_script(home);
cbm_upsert_session_hooks(settings_path);
printf(" hooks: PreToolUse (Grep/Glob search-graph augmenter, non-blocking)\n");
printf(" hooks: SessionStart (MCP usage reminder on startup/resume/clear/compact)\n");
} else if (!dry_run) {
printf(" hooks: skipped (pass --hooks to install search augment hooks)\n");
}
printf(" hooks: PreToolUse (Grep/Glob search-graph augmenter, non-blocking)\n");
printf(" hooks: SessionStart (MCP usage reminder on startup/resume/clear/compact)\n");

/* Migration nudge: when CLAUDE_CONFIG_DIR is set and a legacy ~/.claude tree
* still exists, mention it so users can clean up stale artifacts. */
Expand Down Expand Up @@ -3093,26 +3099,29 @@ static void install_generic_agent_config(const char *label, const char *binary_p

/* Install MCP configs for CLI-based agents (Codex, Gemini, OpenCode, Antigravity, Aider). */
/* Install Gemini CLI config with hooks. */
static void install_gemini_config(const char *home, const char *binary_path, bool dry_run) {
static void install_gemini_config(const char *home, const char *binary_path, bool dry_run,
bool install_hooks) {
char cp[CLI_BUF_1K];
char ip[CLI_BUF_1K];
snprintf(cp, sizeof(cp), "%s/.gemini/settings.json", home);
snprintf(ip, sizeof(ip), "%s/.gemini/GEMINI.md", home);
install_generic_agent_config("Gemini CLI", binary_path, cp, ip, dry_run,
cbm_install_editor_mcp);
if (g_install_plan) {
plan_record("Gemini CLI", "hook", cp); /* BeforeTool + SessionStart in settings.json */
if (install_hooks) {
plan_record("Gemini CLI", "hook", cp); /* BeforeTool + SessionStart in settings.json */
}
return;
}
if (!dry_run) {
if (!dry_run && install_hooks) {
cbm_upsert_gemini_hooks(cp);
cbm_upsert_gemini_session_hooks(cp);
printf(" hooks: BeforeTool + SessionStart (codebase-memory-mcp reminder)\n");
}
printf(" hooks: BeforeTool + SessionStart (codebase-memory-mcp reminder)\n");
}

static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const char *home,
const char *binary_path, bool dry_run) {
const char *binary_path, bool dry_run, bool install_hooks) {
if (agents->codex) {
char cp[CLI_BUF_1K];
char ip[CLI_BUF_1K];
Expand All @@ -3121,16 +3130,16 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const
install_generic_agent_config("Codex CLI", binary_path, cp, ip, dry_run,
cbm_upsert_codex_mcp);
if (g_install_plan) {
plan_record("Codex CLI", "hook", cp);
} else {
if (!dry_run) {
cbm_upsert_codex_hooks(cp);
if (install_hooks) {
plan_record("Codex CLI", "hook", cp);
}
} else if (!dry_run && install_hooks) {
cbm_upsert_codex_hooks(cp);
printf(" hooks: SessionStart (codebase-memory-mcp reminder)\n");
}
}
if (agents->gemini) {
install_gemini_config(home, binary_path, dry_run);
install_gemini_config(home, binary_path, dry_run, install_hooks);
}
if (agents->opencode) {
char cp[CLI_BUF_1K];
Expand Down Expand Up @@ -3160,11 +3169,11 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const
char sp[CLI_BUF_1K];
snprintf(sp, sizeof(sp), "%s/.gemini/antigravity-cli/settings.json", home);
if (g_install_plan) {
plan_record("Antigravity", "hook", sp);
} else {
if (!dry_run) {
cbm_upsert_gemini_session_hooks(sp);
if (install_hooks) {
plan_record("Antigravity", "hook", sp);
}
} else if (!dry_run && install_hooks) {
cbm_upsert_gemini_session_hooks(sp);
printf(" hooks: SessionStart (codebase-memory-mcp reminder)\n");
}
}
Expand Down Expand Up @@ -3250,16 +3259,16 @@ static void install_editor_agent_configs(const cbm_detected_agents_t *agents, co
}

static void cbm_install_agent_configs(const char *home, const char *binary_path, bool force,
bool dry_run) {
bool dry_run, bool install_hooks) {
cbm_detected_agents_t agents = cbm_detect_agents(home);
if (!g_install_plan) {
print_detected_agents(&agents);
}

if (agents.claude_code) {
install_claude_code_config(home, binary_path, force, dry_run);
install_claude_code_config(home, binary_path, force, dry_run, install_hooks);
}
install_cli_agent_configs(&agents, home, binary_path, dry_run);
install_cli_agent_configs(&agents, home, binary_path, dry_run, install_hooks);
install_editor_agent_configs(&agents, home, binary_path, dry_run);
}

Expand Down Expand Up @@ -3317,7 +3326,7 @@ static void cbm_detect_self_path(char *buf, size_t buf_sz, const char *home) {
* the config / instruction / hook files `install` WOULD write, produced by
* running the real install dispatch in record-only mode (no mutation, no
* network). Returns a heap JSON string (caller frees) or NULL. */
char *cbm_build_install_plan_json(const char *home, const char *binary_path) {
char *cbm_build_install_plan_json(const char *home, const char *binary_path, bool install_hooks) {
if (!home || !binary_path) {
return NULL;
}
Expand All @@ -3326,7 +3335,7 @@ char *cbm_build_install_plan_json(const char *home, const char *binary_path) {
* site records into `plan` — so the receipt cannot drift from behavior. */
cbm_install_plan_t plan = {0};
g_install_plan = &plan;
cbm_install_agent_configs(home, binary_path, false, true);
cbm_install_agent_configs(home, binary_path, false, true, install_hooks);
g_install_plan = NULL;

cbm_detected_agents_t det = cbm_detect_agents(home);
Expand Down Expand Up @@ -3395,6 +3404,7 @@ int cbm_cmd_install(int argc, char **argv) {
bool dry_run = false;
bool force = false;
bool plan = false;
bool install_hooks = false;
for (int i = 0; i < argc; i++) {
if (strcmp(argv[i], "--dry-run") == 0) {
dry_run = true;
Expand All @@ -3405,6 +3415,9 @@ int cbm_cmd_install(int argc, char **argv) {
if (strcmp(argv[i], "--plan") == 0) {
plan = true;
}
if (strcmp(argv[i], "--hooks") == 0) {
install_hooks = true;
}
}

const char *home = cbm_get_home_dir();
Expand All @@ -3419,7 +3432,7 @@ int cbm_cmd_install(int argc, char **argv) {
if (plan) {
char self_path[CLI_BUF_1K] = {0};
cbm_detect_self_path(self_path, sizeof(self_path), home);
char *json = cbm_build_install_plan_json(home, self_path);
char *json = cbm_build_install_plan_json(home, self_path, install_hooks);
if (!json) {
(void)fprintf(stderr, "error: failed to build install plan\n");
return CLI_TRUE;
Expand Down Expand Up @@ -3515,7 +3528,7 @@ int cbm_cmd_install(int argc, char **argv) {
#endif

/* Step 3: Install/refresh all agent configs, pointing at the install target. */
cbm_install_agent_configs(home, bin_target, force, dry_run);
cbm_install_agent_configs(home, bin_target, force, dry_run, install_hooks);

/* Step 4: Ensure PATH */
char bin_dir[CLI_BUF_1K];
Expand Down Expand Up @@ -4110,7 +4123,7 @@ int cbm_cmd_update(int argc, char **argv) {

/* Step 6: Refresh all agent configs (skills, MCP entries, hooks) */
printf("Refreshing agent configurations...\n");
cbm_install_agent_configs(home, bin_dest, true, false);
cbm_install_agent_configs(home, bin_dest, true, false, false);

/* Step 7: Verify new version (exec directly, no shell interpretation) */
printf("\nUpdate complete. Verifying:\n");
Expand Down
2 changes: 1 addition & 1 deletion src/cli/cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,6 @@ int cbm_cmd_hook_augment(void);
* a machine-readable JSON list of the config/instruction/hook files `install`
* would write, produced WITHOUT mutating anything. Returns a heap JSON string
* (caller frees) or NULL on error. Exposed for `install --plan` and testing. */
char *cbm_build_install_plan_json(const char *home, const char *binary_path);
char *cbm_build_install_plan_json(const char *home, const char *binary_path, bool install_hooks);

#endif /* CBM_CLI_H */
2 changes: 1 addition & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ static void print_help(void) {
printf("Usage:\n");
printf(" codebase-memory-mcp Run MCP server on stdio\n");
printf(" codebase-memory-mcp cli <tool> [json] Run a single tool\n");
printf(" codebase-memory-mcp install [-y|-n] [--force] [--dry-run]\n");
printf(" codebase-memory-mcp install [-y|-n] [--force] [--hooks] [--dry-run]\n");
printf(" codebase-memory-mcp uninstall [-y|-n] [--dry-run]\n");
printf(" codebase-memory-mcp update [-y|-n]\n");
printf(" codebase-memory-mcp config <list|get|set|reset>\n");
Expand Down
30 changes: 29 additions & 1 deletion tests/test_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -1592,7 +1592,7 @@ TEST(cli_install_plan_receipt_no_mutation_issue388) {
snprintf(dir, sizeof(dir), "%s/.codex", tmpdir);
test_mkdirp(dir);

char *json = cbm_build_install_plan_json(tmpdir, "/usr/local/bin/codebase-memory-mcp");
char *json = cbm_build_install_plan_json(tmpdir, "/usr/local/bin/codebase-memory-mcp", false);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "agent.install.plan.v1") != NULL);
ASSERT(strstr(json, "writes_started") != NULL);
Expand All @@ -1614,6 +1614,33 @@ TEST(cli_install_plan_receipt_no_mutation_issue388) {
PASS();
}

TEST(cli_install_plan_hooks_opt_in_default) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-plan-hooks-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");

char dir[512];
snprintf(dir, sizeof(dir), "%s/.claude", tmpdir);
test_mkdirp(dir);

char *json = cbm_build_install_plan_json(tmpdir, "/usr/local/bin/codebase-memory-mcp", false);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "hooks_planned") != NULL);
ASSERT_NULL(strstr(json, "cbm-code-discovery-gate"));
ASSERT_NULL(strstr(json, "cbm-session-reminder"));
free(json);

json = cbm_build_install_plan_json(tmpdir, "/usr/local/bin/codebase-memory-mcp", true);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "cbm-code-discovery-gate") != NULL);
ASSERT(strstr(json, "cbm-session-reminder") != NULL);
free(json);

test_rmdir_r(tmpdir);
PASS();
}

/* issue #330: Codex SessionStart reminder hook in config.toml — installed,
* idempotent, preserves other content, and cleanly removed. */
TEST(cli_codex_session_hook_issue330) {
Expand Down Expand Up @@ -2749,6 +2776,7 @@ SUITE(cli) {
RUN_TEST(cli_detect_agents_finds_codex);
RUN_TEST(cli_detect_agents_finds_cursor_issue222);
RUN_TEST(cli_install_plan_receipt_no_mutation_issue388);
RUN_TEST(cli_install_plan_hooks_opt_in_default);
RUN_TEST(cli_codex_session_hook_issue330);
RUN_TEST(cli_gemini_session_hook_parity);
RUN_TEST(cli_detect_agents_finds_gemini);
Expand Down