fix(#2236): add hookShell parameter for PowerShell call operator (#2261)

* fix(#2236): add hookShell parameter for PowerShell call operator

Windows Claude Code with a PowerShell hook runner failed on every hook
with 'Unexpected token' — the installer emitted bare quoted paths that
PowerShell parses as string literals, not commands.

The root cause was that hookCommandNeedsPowerShellCallOperator returned
false unconditionally — the &/no-& decision was keyed on (platform,
runtime) only, with no signal for the effective hook-execution shell.
One runtime (Claude Code) hosts either Git Bash or PowerShell on Windows.

Fix: thread a new hookShell parameter through the projection chain.
When hookShell='powershell', the & call operator is prepended. Default
(Git Bash, no prefix) is unchanged and regression-locked.

* docs: backfill changeset PR number (#2261)

* merge: keep up to date with next

* fix: regenerate stale capability-registry after next merge
This commit is contained in:
Tom Boucher
2026-07-14 15:16:55 -04:00
committed by GitHub
parent 27415c780e
commit e35534c3ec
4 changed files with 90 additions and 18 deletions

View File

@@ -534,6 +534,67 @@ describe('bug #3413: Shell Command Projection Module uses runtime-aware hook pol
});
});
describe('bug #2236: hookShell parameter routes PowerShell call operator', () => {
test('hookShell=powershell on Windows returns true for any runtime', () => {
assert.equal(
hookCommandNeedsPowerShellCallOperator({ platform: 'win32', runtime: 'claude', hookShell: 'powershell' }),
true,
'PowerShell hook shell must trigger the call operator regardless of runtime',
);
assert.equal(
hookCommandNeedsPowerShellCallOperator({ platform: 'win32', runtime: 'antigravity', hookShell: 'powershell' }),
true,
);
});
test('hookShell=bash (default) stays false — Git-Bash form preserved', () => {
assert.equal(
hookCommandNeedsPowerShellCallOperator({ platform: 'win32', runtime: 'claude', hookShell: 'bash' }),
false,
);
assert.equal(
hookCommandNeedsPowerShellCallOperator({ platform: 'win32', runtime: 'claude' }),
false,
);
});
test('formatHookCommandForRuntime prepends & when hookShell=powershell', () => {
const cmd = '"C:/Program Files/nodejs/node.exe" "C:/Users/me/.claude/hooks/gsd-context-monitor.js"';
assert.equal(
formatHookCommandForRuntime(cmd, { platform: 'win32', runtime: 'claude', hookShell: 'powershell' }),
`& ${cmd}`,
);
});
test('formatHookCommandForRuntime omits & when hookShell unset (backward compat)', () => {
const cmd = '"C:/Program Files/nodejs/node.exe" "C:/Users/me/.claude/hooks/gsd-context-monitor.js"';
assert.equal(
formatHookCommandForRuntime(cmd, { platform: 'win32', runtime: 'claude' }),
cmd,
);
});
test('buildHookCommand emits & prefix when hookShell=powershell', () => {
const cmd = buildHookCommand('C:/Users/me/.claude', 'gsd-check-update.js', {
platform: 'win32',
runtime: 'claude',
hookShell: 'powershell',
});
assert.ok(cmd, 'buildHookCommand should return a command');
assert.ok(cmd.startsWith('& '), `PowerShell hook command must start with '& ': ${cmd}`);
});
test('buildHookCommand omits & prefix when hookShell=bash (regression lock)', () => {
const cmd = buildHookCommand('C:/Users/me/.claude', 'gsd-check-update.js', {
platform: 'win32',
runtime: 'claude',
hookShell: 'bash',
});
assert.ok(cmd, 'buildHookCommand should return a command');
assert.equal(cmd.startsWith('& '), false, `Git-Bash hook command must NOT start with '& ': ${cmd}`);
});
});
describe('bug #3413: installer hook surfaces consume runtime-aware projection', () => {
test('buildHookCommand emits shell-neutral Claude hook command on Windows', () => {
const cmd = buildHookCommand('C:/Users/me/.claude', 'gsd-check-update.js', {