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
5 changes: 5 additions & 0 deletions src/cli/cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,11 @@ int cbm_cmd_config(int argc, char **argv);
* path returns 0 with no stdout output. */
int cbm_cmd_hook_augment(void);

/* True for an absolute path the augmenter can walk up: POSIX "/..." or Windows
* drive "X:/..." (callers normalize '\\' to '/' first). Exposed for tests —
* regression coverage for the Windows drive-letter no-op (#618). */
bool cbm_hook_path_is_abs(const char *path);

/* Build the agent.install.plan.v1 install receipt for <home> (issue #388):
* a machine-readable JSON list of the config/instruction/hook files `install`
* would write, produced WITHOUT mutating anything. Returns a heap JSON string
Expand Down
42 changes: 34 additions & 8 deletions src/cli/hook_augment.c
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,19 @@ static void ha_emit(const char *text) {
yyjson_mut_doc_free(doc);
}

/* True for an absolute path we can walk up: POSIX "/..." or Windows drive
* "X:/..." (callers normalize '\\' to '/' first). Declared in cli.h so the
* Windows drive-letter handling (#618) has direct regression coverage. */
bool cbm_hook_path_is_abs(const char *d) {
if (!d || !d[0]) {
return false;
}
if (d[0] == '/') {
return true;
}
return isalpha((unsigned char)d[0]) && d[1] == ':' && (d[2] == '/' || d[2] == '\0');
}

/* Walk up from `start`, deriving a project name at each level and querying
* search_graph until an indexed project is found (or the walk is exhausted).
* Stops at the first non-error result: a valid project with zero hits is a
Expand All @@ -251,7 +264,7 @@ static char *ha_resolve_and_query(cbm_mcp_server_t *srv, const char *start, cons
char dir[4096];
snprintf(dir, sizeof(dir), "%s", start);

for (int level = 0; level < HA_MAX_WALKUP && dir[0] == '/'; level++) {
for (int level = 0; level < HA_MAX_WALKUP && cbm_hook_path_is_abs(dir); level++) {
char *project = cbm_project_name_from_path(dir);
if (project) {
char *args = ha_build_args(project, token);
Expand All @@ -275,7 +288,10 @@ static char *ha_resolve_and_query(cbm_mcp_server_t *srv, const char *start, cons
/* Not indexed at this level — climb to the parent. */
char *slash = strrchr(dir, '/');
if (!slash || slash == dir) {
break;
break; /* POSIX root "/" */
}
if (slash == dir + 2 && dir[1] == ':') {
break; /* Windows drive root "X:/" — don't strip to "X:" */
}
*slash = '\0';
}
Expand Down Expand Up @@ -313,9 +329,9 @@ int cbm_cmd_hook_augment(void) {
}

const char *cwd = ha_obj_str(root, "cwd");
#ifndef _WIN32
char cwdbuf[4096];
if (!cwd || cwd[0] != '/') {
#ifndef _WIN32
if (!cwd || !cbm_hook_path_is_abs(cwd)) {
if (!getcwd(cwdbuf, sizeof(cwdbuf))) {
yyjson_doc_free(doc);
free(input);
Expand All @@ -324,10 +340,20 @@ int cbm_cmd_hook_augment(void) {
cwd = cwdbuf;
}
#else
/* Windows: Claude Code passes cwd in the hook payload. The walk-up loop
* below requires POSIX-style absolute paths ('/'-prefixed), so without a
* usable cwd there is nothing to augment — fail open cleanly. */
if (!cwd || cwd[0] != '/') {
/* Windows: Claude Code passes an absolute drive-letter cwd in the hook
* payload (e.g. C:\repo). Normalize '\\' -> '/' and require an absolute
* path; the walk-up loop handles POSIX and "X:/..." roots alike. Without
* a usable cwd there is nothing to augment — fail open cleanly. */
if (cwd) {
snprintf(cwdbuf, sizeof(cwdbuf), "%s", cwd);
for (char *p = cwdbuf; *p; p++) {
if (*p == '\\') {
*p = '/';
}
}
cwd = cwdbuf;
}
if (!cwd || !cbm_hook_path_is_abs(cwd)) {
yyjson_doc_free(doc);
free(input);
return 0;
Expand Down
21 changes: 21 additions & 0 deletions tests/test_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -2215,6 +2215,26 @@ TEST(cli_hook_gate_script_no_predictable_tmp_issue384) {
PASS();
}

/* issue #618: hook-augment was a structural no-op on Windows because its path
* guards required POSIX-style '/'-prefixed absolute paths, so a drive-letter
* cwd (C:/repo) was rejected before any search_graph query. The predicate must
* accept POSIX and Windows drive roots alike (callers normalize '\\' to '/'). */
TEST(cli_hook_augment_path_is_abs) {
/* POSIX absolute (unchanged behavior) */
ASSERT(cbm_hook_path_is_abs("/home/u/proj"));
/* Windows drive roots — the #618 regression */
ASSERT(cbm_hook_path_is_abs("C:/Users/me/proj"));
ASSERT(cbm_hook_path_is_abs("C:/"));
ASSERT(cbm_hook_path_is_abs("C:"));
ASSERT(cbm_hook_path_is_abs("d:/lowercase/drive"));
/* Not absolute → augmenter no-ops cleanly */
ASSERT(!cbm_hook_path_is_abs("relative/path"));
ASSERT(!cbm_hook_path_is_abs("proj"));
ASSERT(!cbm_hook_path_is_abs(""));
ASSERT(!cbm_hook_path_is_abs(NULL));
PASS();
}

TEST(cli_upsert_claude_hook_existing) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
Expand Down Expand Up @@ -2784,6 +2804,7 @@ SUITE(cli) {

/* Claude Code hooks (5 tests — group D) */
RUN_TEST(cli_hook_gate_script_no_predictable_tmp_issue384);
RUN_TEST(cli_hook_augment_path_is_abs);
RUN_TEST(cli_upsert_claude_hook_fresh);
RUN_TEST(cli_upsert_claude_hook_existing);
RUN_TEST(cli_upsert_claude_hook_replace);
Expand Down
Loading