fix(install): anchor local hook paths to $CLAUDE_PROJECT_DIR (#1906) (#1917)

Local installs wrote bare relative paths (e.g. `node .claude/hooks/...`)
into settings.json. Claude Code persists the shell's cwd between tool
calls, so a single `cd subdir` broke every hook for the rest of the
session.

Prefix all 9 local hook commands with "$CLAUDE_PROJECT_DIR"/ so path
resolution is always anchored to the project root regardless of cwd.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tibsfox
2026-04-07 14:26:29 -07:00
committed by GitHub
parent 7bc6668504
commit 8021e86038
2 changed files with 94 additions and 9 deletions

View File

@@ -5547,21 +5547,24 @@ 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;
const statuslineCommand = isGlobal
? buildHookCommand(targetDir, 'gsd-statusline.js')
: 'node ' + dirName + '/hooks/gsd-statusline.js';
: 'node ' + localPrefix + '/hooks/gsd-statusline.js';
const updateCheckCommand = isGlobal
? buildHookCommand(targetDir, 'gsd-check-update.js')
: 'node ' + dirName + '/hooks/gsd-check-update.js';
: 'node ' + localPrefix + '/hooks/gsd-check-update.js';
const contextMonitorCommand = isGlobal
? buildHookCommand(targetDir, 'gsd-context-monitor.js')
: 'node ' + dirName + '/hooks/gsd-context-monitor.js';
: 'node ' + localPrefix + '/hooks/gsd-context-monitor.js';
const promptGuardCommand = isGlobal
? buildHookCommand(targetDir, 'gsd-prompt-guard.js')
: 'node ' + dirName + '/hooks/gsd-prompt-guard.js';
: 'node ' + localPrefix + '/hooks/gsd-prompt-guard.js';
const readGuardCommand = isGlobal
? buildHookCommand(targetDir, 'gsd-read-guard.js')
: 'node ' + dirName + '/hooks/gsd-read-guard.js';
: 'node ' + localPrefix + '/hooks/gsd-read-guard.js';
// Enable experimental agents for Gemini CLI (required for custom sub-agents)
if (isGemini) {
@@ -5714,7 +5717,7 @@ function install(isGlobal, runtime = 'claude') {
// /gsd-quick or /gsd-fast for state-tracked changes. Advisory only.
const workflowGuardCommand = isGlobal
? buildHookCommand(targetDir, 'gsd-workflow-guard.js')
: 'node ' + dirName + '/hooks/gsd-workflow-guard.js';
: 'node ' + localPrefix + '/hooks/gsd-workflow-guard.js';
const hasWorkflowGuardHook = settings.hooks[preToolEvent].some(entry =>
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-workflow-guard'))
);
@@ -5739,7 +5742,7 @@ function install(isGlobal, runtime = 'claude') {
// Configure commit validation hook (Conventional Commits enforcement, opt-in)
const validateCommitCommand = isGlobal
? 'bash ' + targetDir.replace(/\\/g, '/') + '/hooks/gsd-validate-commit.sh'
: 'bash ' + dirName + '/hooks/gsd-validate-commit.sh';
: 'bash ' + localPrefix + '/hooks/gsd-validate-commit.sh';
const hasValidateCommitHook = settings.hooks[preToolEvent].some(entry =>
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-validate-commit'))
);
@@ -5766,7 +5769,7 @@ function install(isGlobal, runtime = 'claude') {
// Configure session state orientation hook (opt-in)
const sessionStateCommand = isGlobal
? 'bash ' + targetDir.replace(/\\/g, '/') + '/hooks/gsd-session-state.sh'
: 'bash ' + dirName + '/hooks/gsd-session-state.sh';
: 'bash ' + localPrefix + '/hooks/gsd-session-state.sh';
const hasSessionStateHook = settings.hooks.SessionStart.some(entry =>
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-session-state'))
);
@@ -5788,7 +5791,7 @@ function install(isGlobal, runtime = 'claude') {
// Configure phase boundary detection hook (opt-in)
const phaseBoundaryCommand = isGlobal
? 'bash ' + targetDir.replace(/\\/g, '/') + '/hooks/gsd-phase-boundary.sh'
: 'bash ' + dirName + '/hooks/gsd-phase-boundary.sh';
: 'bash ' + localPrefix + '/hooks/gsd-phase-boundary.sh';
const hasPhaseBoundaryHook = settings.hooks[postToolEvent].some(entry =>
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-phase-boundary'))
);