From 4edf42ce9f457d9678a35bd751c84acc3470c4a6 Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 7 Sep 2026 22:23:06 +0200 Subject: [PATCH] fix(test): run the secret guard via its own shebang, not through sh The suite piped hook input into `sh "$GUARD"`, which overrides the hook's `#!/usr/bin/env bash` and tests an interpreter production never uses. Claude Code executes the hook as a program. On Debian /bin/sh is dash, which has no `set -o pipefail`, so the guard died on line 23 before reading a byte of input: secret-guard.sh: 23: set: Illegal option -o pipefail secret_guard: 0 passed, 20 failed while passing 20/20 on a host where /bin/sh happens to be bash. A security gate that tests green and cannot start is the worst of both, and the test was the only thing standing between those two states. Found because baked-v16 added jq to the runner: this suite needs jq to build its hook JSON, so it had been silently doing nothing in CI and started running for the first time. Same shape as installing zip and immediately learning the gitea backup check had never run -- a missing tool does not announce that it is suppressing a whole suite. Also asserts the hook is executable, since direct invocation depends on the exec bit, and that assertion is what stops this regressing to `sh "$GUARD"`. Checked the rest of the suite: every other `sh "$SUT"` targets a POSIX script, which is correct. hooks/pre-task-ritual.sh is the other bash-shebang hook and has no test at all -- filed separately rather than fixed here. Bump-Type: patch Co-Authored-By: Claude Opus 5 --- tests/secret_guard_test.sh | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/secret_guard_test.sh b/tests/secret_guard_test.sh index e6715ae..7e4966f 100644 --- a/tests/secret_guard_test.sh +++ b/tests/secret_guard_test.sh @@ -24,7 +24,7 @@ decision_for() { _tool=${2:-Bash} jq -n --arg t "$_tool" --arg c "$_cmd" \ '{tool_name: $t, tool_input: {command: $c}}' \ - | sh "$GUARD" \ + | "$GUARD" \ | jq -r '.hookSpecificOutput.permissionDecision // "MISSING"' } @@ -42,6 +42,20 @@ expect() { fi } +# Invoked as a PROGRAM above, never as `sh "$GUARD"`. Claude Code executes the +# hook directly, so its `#!/usr/bin/env bash` shebang decides the interpreter; +# piping it into `sh` overrides that and tests something production never runs. +# +# It is not academic. On Debian /bin/sh is dash, which has no `set -o +# pipefail`, so the guard died on line 23 before reading a byte of input and +# every deny case returned empty -- 20 of 20 assertions failed in CI while +# passing on a host where /bin/sh happens to be bash. A security gate that +# tests green and cannot start is the worst of both. +if [ ! -x "$GUARD" ]; then + printf 'FAIL: %s is not executable -- it must run via its own shebang\n' "$GUARD" + exit 1 +fi + if [ ! -f "$GUARD" ]; then printf 'FAIL: %s does not exist\n' "$GUARD" exit 1