fix: use #!/usr/bin/env bash in community .sh hooks for distro portability
The three opt-in bash hooks (gsd-phase-boundary.sh, gsd-session-state.sh,
gsd-validate-commit.sh) shipped with #!/bin/bash, which fails on distros
that don't ship bash at /bin/bash (NixOS, minimal Alpine images, some
container runtimes). POSIX guarantees /bin/sh but not /bin/bash.
This is latent in the default install path because Claude Code wires the
hooks as `bash <path>` from settings.json (PATH-resolved — the script's
own shebang is read as a comment by bash). The fix matters when scripts
are run directly: tests, future installer changes, or manual debugging.
Changes:
- hooks/gsd-{phase-boundary,session-state,validate-commit}.sh: shebang
switched to #!/usr/bin/env bash, matching the convention already used
in scripts/*.sh.
- tests/bug-2136-sh-hook-version.test.cjs: assertion updated to expect
the new shebang; comment updated to spell out the rationale.
- tests/bug-2979-hook-absolute-node.test.cjs: doc-comment updated — the
prior wording cited "POSIX std PATH always has /bin" as the reason
bare `bash` is OK. The actual reason is that bare `bash` is
PATH-resolved, which is portable across distros that don't ship
/bin/bash. POSIX std PATH guarantees /bin/sh, not /bin/bash.
- bin/install.js::buildHookCommand: comment block clarifying the same.
No behavior change in this file — bare `bash` was already correct.
- .changeset/portable-bash-shebang-hooks.md: changeset entry.
Verified locally on NixOS:
- npm run build:hooks: hooks/dist/*.sh shebangs propagate correctly.
- node --test tests/bug-2136-*.cjs tests/bug-2979-*.cjs
tests/bug-1817-*.cjs tests/bug-1834-*.cjs tests/bug-1906-*.cjs
tests/bug-2557-*.cjs tests/bug-3017-*.cjs tests/security-scan.test.cjs
tests/hooks-doc-parity.test.cjs: 126/126 pass.
- node scripts/run-tests.cjs (full suite): 6944 pass / 0 fail / 5 skip.
This commit is contained in:
committed by
Tom Boucher
parent
94f835af40
commit
8ca86b5e24
@@ -709,10 +709,16 @@ function rewriteLegacyCodexHookBlock(content, absoluteRunner) {
|
||||
*/
|
||||
function buildHookCommand(configDir, hookName, opts) {
|
||||
if (!opts) opts = {};
|
||||
// .sh hooks run under /bin/bash (POSIX std PATH always includes /bin),
|
||||
// so bare `bash` is fine. .js hooks need the absolute node path because
|
||||
// GUI-launched runtimes start with a minimal PATH that does not include
|
||||
// nvm/Homebrew/Volta-installed node binaries (#2979).
|
||||
// .sh hooks run under bare `bash` (PATH-resolved). POSIX guarantees
|
||||
// /bin/sh but not /bin/bash, and distros like NixOS do not ship
|
||||
// /bin/bash by default — so PATH-resolved `bash` is more portable than
|
||||
// an absolute /bin/bash. The wrapping `bash <path>` invocation also
|
||||
// means the script's own shebang (#!/usr/bin/env bash) is read as a
|
||||
// comment in this code path; it only matters when the script is run
|
||||
// directly (e.g. tests or future installer changes). .js hooks still
|
||||
// need the absolute node path because GUI-launched runtimes start with
|
||||
// a minimal PATH that does not include nvm/Homebrew/Volta-installed
|
||||
// node binaries (#2979).
|
||||
const nodeRunner = resolveNodeRunner();
|
||||
const runner = hookName.endsWith('.sh') ? 'bash' : nodeRunner;
|
||||
// resolveNodeRunner returns null when process.execPath is unavailable.
|
||||
|
||||
Reference in New Issue
Block a user