fix(install): extend buildHookCommand to .sh hooks — absolute quoted paths (#2049)

* fix(autonomous): add Agent to allowed-tools in gsd-autonomous skill

Closes #2043

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(install): extend buildHookCommand to .sh hooks — absolute quoted paths

- Extend buildHookCommand() to branch on .sh suffix, using 'bash' runner
  instead of 'node', so all hook paths go through the same quoted-path
  construction: bash "/absolute/path/hooks/gsd-*.sh"
- Replace three manual 'bash ' + targetDir + '...' concatenations for
  gsd-validate-commit.sh, gsd-session-state.sh, gsd-phase-boundary.sh
  with buildHookCommand(targetDir, hookName) for the global-install branch
- Global .sh hook paths are now double-quoted, fixing invocation failure
  when the config dir path contains spaces (Windows usernames, #2045)
- Adds regression tests in tests/sh-hook-paths.test.cjs

Closes #2045
Closes #2046

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-10 13:55:27 -04:00
committed by GitHub
parent 6960fd28fe
commit 50537e5f67
2 changed files with 189 additions and 4 deletions

View File

@@ -443,7 +443,13 @@ function expandTilde(filePath) {
function buildHookCommand(configDir, hookName) {
// Use forward slashes for Node.js compatibility on all platforms
const hooksPath = configDir.replace(/\\/g, '/') + '/hooks/' + hookName;
return `node "${hooksPath}"`;
// .sh hooks use bash; .js hooks use node. Both wrap the path in double quotes
// so that paths with spaces (e.g. Windows "C:/Users/First Last/") work correctly
// (fixes #2045). Routing .sh hooks through this function also ensures they always
// receive an absolute path rather than the bare relative string that the old manual
// concatenation produced (fixes #2046).
const runner = hookName.endsWith('.sh') ? 'bash' : 'node';
return `${runner} "${hooksPath}"`;
}
/**
@@ -6038,7 +6044,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'
? buildHookCommand(targetDir, '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'))
@@ -6065,7 +6071,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'
? buildHookCommand(targetDir, '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'))
@@ -6087,7 +6093,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'
? buildHookCommand(targetDir, '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'))