From fba040c72c33b763667a6c59df4dc6d8de2778ab Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 22 Apr 2026 12:04:06 -0400 Subject: [PATCH] fix(#2557): Gemini/Antigravity local hook commands use relative paths, not \$CLAUDE_PROJECT_DIR (#2589) \$CLAUDE_PROJECT_DIR is Claude Code-specific. Gemini CLI doesn't set it, and on Windows its path-join logic doubled the value producing unresolvable paths like D:\Projects\GSD\'D:\Projects\GSD'. Gemini runs project hooks with project root as cwd, so bare relative paths (e.g. node .gemini/hooks/gsd-check-update.js) are cross-platform and correct. Claude Code and others still use the env var. Co-authored-by: Claude Sonnet 4.6 --- bin/install.js | 10 ++- tests/bug-1906-hook-relative-paths.test.cjs | 8 +- .../bug-2557-gemini-local-hook-paths.test.cjs | 74 +++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 tests/bug-2557-gemini-local-hook-paths.test.cjs diff --git a/bin/install.js b/bin/install.js index aeb55f987..275fe93bd 100755 --- a/bin/install.js +++ b/bin/install.js @@ -6075,9 +6075,13 @@ function install(isGlobal, runtime = 'claude') { return; } const settings = validateHookFields(cleanupOrphanedHooks(rawSettings)); - // Local installs anchor paths to $CLAUDE_PROJECT_DIR so hooks resolve - // correctly regardless of the shell's current working directory (#1906). - const localPrefix = '"$CLAUDE_PROJECT_DIR"/' + dirName; + // Local installs anchor hook paths so they resolve regardless of cwd (#1906). + // Claude Code sets $CLAUDE_PROJECT_DIR; Gemini/Antigravity do not — and on + // Windows their own substitution logic doubles the path (#2557). Those runtimes + // run project hooks with the project dir as cwd, so bare relative paths work. + const localPrefix = (runtime === 'gemini' || runtime === 'antigravity') + ? dirName + : '"$CLAUDE_PROJECT_DIR"/' + dirName; const hookOpts = { portableHooks: hasPortableHooks }; const statuslineCommand = isGlobal ? buildHookCommand(targetDir, 'gsd-statusline.js', hookOpts) diff --git a/tests/bug-1906-hook-relative-paths.test.cjs b/tests/bug-1906-hook-relative-paths.test.cjs index f3c112414..6216c97fe 100644 --- a/tests/bug-1906-hook-relative-paths.test.cjs +++ b/tests/bug-1906-hook-relative-paths.test.cjs @@ -42,9 +42,11 @@ describe('bug #1906: local hook commands use $CLAUDE_PROJECT_DIR', () => { src = fs.readFileSync(INSTALL_SRC, 'utf-8'); }); - test('localPrefix variable is defined with $CLAUDE_PROJECT_DIR', () => { - assert.match(src, /const localPrefix\s*=\s*['"]\"\$CLAUDE_PROJECT_DIR['"]\s*\//, - 'localPrefix should be defined using $CLAUDE_PROJECT_DIR'); + test('localPrefix definition includes $CLAUDE_PROJECT_DIR for non-Gemini runtimes', () => { + // localPrefix is now a ternary — Gemini/Antigravity use bare dirName (#2557), + // all other runtimes use "$CLAUDE_PROJECT_DIR"/ to anchor hook paths. + assert.match(src, /const localPrefix\s*=[\s\S]*?"\$CLAUDE_PROJECT_DIR"/, + 'localPrefix definition must include "$CLAUDE_PROJECT_DIR" branch for non-Gemini runtimes'); }); for (const hook of HOOKS) { diff --git a/tests/bug-2557-gemini-local-hook-paths.test.cjs b/tests/bug-2557-gemini-local-hook-paths.test.cjs new file mode 100644 index 000000000..e1a20c70a --- /dev/null +++ b/tests/bug-2557-gemini-local-hook-paths.test.cjs @@ -0,0 +1,74 @@ +'use strict'; + +/** + * Bug #2557: Gemini CLI local hook commands must NOT use $CLAUDE_PROJECT_DIR. + * + * $CLAUDE_PROJECT_DIR is a Claude Code-specific env variable. Gemini CLI does + * not set it. On Windows, Gemini's own variable-substitution + path-join logic + * produced a doubled path like `D:\Projects\GSD\'D:\Projects\GSD'`, causing + * every local project hook to fail at SessionStart. + * + * Fix: localPrefix is now runtime-conditional. Gemini/Antigravity use bare + * dirName (relative path) since they always run project hooks with the project + * dir as cwd. Claude Code and others still use "$CLAUDE_PROJECT_DIR"/ (#1906). + */ + +const { describe, test, before } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js'); + +describe('bug #2557: Gemini/Antigravity local hooks use relative paths (not $CLAUDE_PROJECT_DIR)', () => { + let src; + + before(() => { + src = fs.readFileSync(INSTALL_SRC, 'utf-8'); + }); + + test('localPrefix is a ternary that branches on Gemini/Antigravity', () => { + assert.match( + src, + /const localPrefix\s*=\s*\(runtime\s*===\s*['"]gemini['"]/, + 'localPrefix must branch on runtime === "gemini"', + ); + }); + + test('Gemini/Antigravity branch of localPrefix uses bare dirName (relative path)', () => { + // The ternary must assign `dirName` (not `"$CLAUDE_PROJECT_DIR"/` + dirName) + // for the Gemini branch so hooks use a relative path on all platforms. + assert.match( + src, + /const localPrefix\s*=\s*\(runtime\s*===\s*['"]gemini['"]\s*\|\|\s*runtime\s*===\s*['"]antigravity['"]\)\s*\n?\s*\?\s*dirName/, + 'Gemini/Antigravity branch must resolve to bare dirName', + ); + }); + + test('non-Gemini branch of localPrefix uses "$CLAUDE_PROJECT_DIR"/', () => { + // The else branch must still use "$CLAUDE_PROJECT_DIR"/ to fix #1906 for + // Claude Code and other runtimes that do set the variable. + assert.match( + src, + /:\s*['"]\"\$CLAUDE_PROJECT_DIR\"\//, + 'non-Gemini branch must use "$CLAUDE_PROJECT_DIR"/ prefix', + ); + }); + + test('Gemini/Antigravity hook commands do not contain "$CLAUDE_PROJECT_DIR" literal', () => { + // Since localPrefix is now dirName for Gemini/Antigravity, no command + // string built via `localPrefix` should contain the variable literal. + // We verify by checking that the only occurrence of $CLAUDE_PROJECT_DIR + // in the localPrefix definition is in the non-Gemini (else) branch. + const lines = src.split('\n'); + const prefixDefIdx = lines.findIndex(l => /const localPrefix\s*=/.test(l)); + assert.ok(prefixDefIdx >= 0, 'localPrefix definition not found'); + // The Gemini (truthy) branch is the line right after the ternary condition. + // It must NOT contain $CLAUDE_PROJECT_DIR. + const geminiLine = lines[prefixDefIdx + 1] || ''; + assert.ok( + !geminiLine.includes('$CLAUDE_PROJECT_DIR'), + 'Gemini branch of localPrefix must not reference $CLAUDE_PROJECT_DIR', + ); + }); +});