test(hooks): guard PreToolUse gate shim against predictable temp file (#384)

The symlink-attack vector (predictable /tmp/cbm-code-discovery-gate-$PPID)
was removed in c29e6d5 when the blocking bash gate became a stateless
compiled augmenter writing to stdout only. Add a regression guard so a
predictable temp/state file can never be reintroduced: assert the emitted
gate shim contains no /tmp path and no $PPID, and delegates to hook-augment.

Export cbm_install_hook_gate_script (was static) so the security property
is directly testable.
This commit is contained in:
Martin Vogel committed 2026-05-31 13:32:08 +02:00
1 parent f88d0315ac
commit 61453e928f
3 files changed
+33 -1

No files matched your search

+1 -1
View File
@@ -1746,7 +1746,7 @@ int cbm_remove_claude_hooks(const char *settings_path) {
* 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. */
static void cbm_install_hook_gate_script(const char *home, const char *binary_path) {
void cbm_install_hook_gate_script(const char *home, const char *binary_path) {
if (!home || !binary_path) {
return;
}
+6
View File
@@ -175,6 +175,12 @@ int cbm_upsert_claude_hooks(const char *settings_path);
* Returns 0 on success. */
int cbm_remove_claude_hooks(const char *settings_path);
/* Write the PreToolUse gate shim to <home>/.claude/hooks/. The shim is a thin
* wrapper that invokes the compiled `hook-augment` and writes to stdout only —
* it must never create a predictable temp/state file (issue #384). Exposed for
* testing that security property. */
void cbm_install_hook_gate_script(const char *home, const char *binary_path);
/* Upsert a BeforeTool hook in ~/.gemini/settings.json for Gemini CLI / Antigravity.
* Returns 0 on success. */
int cbm_upsert_gemini_hooks(const char *settings_path);
+26
View File
@@ -1988,6 +1988,31 @@ TEST(cli_upsert_claude_hook_fresh) {
PASS();
}
/* issue #384: the PreToolUse gate shim must never use a predictable /tmp
* filename (the old `/tmp/cbm-code-discovery-gate-$PPID` was a symlink-attack
* vector). The shim is now a stateless wrapper around the compiled augmenter. */
TEST(cli_hook_gate_script_no_predictable_tmp_issue384) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-gate-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
cbm_install_hook_gate_script(tmpdir, "/usr/local/bin/codebase-memory-mcp");
char script_path[512];
snprintf(script_path, sizeof(script_path), "%s/.claude/hooks/cbm-code-discovery-gate", tmpdir);
const char *data = read_test_file(script_path);
ASSERT_NOT_NULL(data);
/* No predictable temp/state file and no PPID-derived path. */
ASSERT(strstr(data, "/tmp") == NULL);
ASSERT(strstr(data, "PPID") == NULL);
/* It delegates to the stateless compiled augmenter (stdout only). */
ASSERT(strstr(data, "hook-augment") != NULL);
test_rmdir_r(tmpdir);
PASS();
}
TEST(cli_upsert_claude_hook_existing) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
@@ -2548,6 +2573,7 @@ SUITE(cli) {
RUN_TEST(cli_agent_instructions_content);
/* Claude Code hooks (5 tests — group D) */
RUN_TEST(cli_hook_gate_script_no_predictable_tmp_issue384);
RUN_TEST(cli_upsert_claude_hook_fresh);
RUN_TEST(cli_upsert_claude_hook_existing);
RUN_TEST(cli_upsert_claude_hook_replace);