diff --git a/hooks/no-chaining.sh b/hooks/no-chaining.sh index 8016187..d8a1789 100755 --- a/hooks/no-chaining.sh +++ b/hooks/no-chaining.sh @@ -135,6 +135,10 @@ all_segments_safe() { # Emit a block response with the original commands split and numbered, so the # agent can run them as separate Bash calls without re-parsing the original. # Usage: block_with_hint +# +# Uses "continue":true so that Claude Code rejects the command but keeps the +# agent loop running — Claude can read the hint and retry with separate calls. +# "continue":false would terminate the session entirely, which is too disruptive. block_with_hint() { local prefix="$1" delim="$2" orig="$3" local n=1 msg="${prefix} Run each as a separate Bash call:" @@ -145,7 +149,7 @@ block_with_hint() { ${n}. ${seg}" n=$((n+1)) done <<< "$(printf '%s' "$orig" | sed "s/[[:space:]]*${delim}[[:space:]]*/\n/g")" - printf '%s\n' "{\"continue\":false,\"stopReason\":$(printf '%s' "$msg" | jq -Rs .)}" + printf '%s\n' "{\"continue\":true,\"stopReason\":$(printf '%s' "$msg" | jq -Rs .)}" } # Check for && — always blocked. diff --git a/hooks/no-chaining.test.sh b/hooks/no-chaining.test.sh index d56f6f4..aeeaf7c 100644 --- a/hooks/no-chaining.test.sh +++ b/hooks/no-chaining.test.sh @@ -37,12 +37,14 @@ assert_allowed() { fi } -# Assert that the hook BLOCKS the command (output contains "continue":false). +# Assert that the hook BLOCKS the command (output contains a stopReason). +# The hook uses "continue":true so Claude can retry; blocking is signalled by +# the presence of a stopReason, not by "continue":false. assert_blocked() { local desc="$1" cmd="$2" local output output=$(run_hook "$cmd") - if printf '%s' "$output" | grep -qF '"continue":false'; then + if printf '%s' "$output" | grep -qF '"stopReason"'; then printf 'PASS %s\n' "$desc" passed=$((passed + 1)) else diff --git a/tests/hooks/no-chaining.bats b/tests/hooks/no-chaining.bats index 7a5f50d..da664c1 100644 --- a/tests/hooks/no-chaining.bats +++ b/tests/hooks/no-chaining.bats @@ -10,18 +10,18 @@ setup() { @test "blocks && chaining" { run bash -c 'printf "%s" "{\"tool_input\":{\"command\":\"cd /tmp && git status\"}}" | bash "$1"' -- "$HOOK" - assert_output --partial '"continue":false' + assert_output --partial '"stopReason"' } @test "blocks || chaining" { run bash -c 'printf "%s" "{\"tool_input\":{\"command\":\"git pull || echo failed\"}}" | bash "$1"' -- "$HOOK" - assert_output --partial '"continue":false' + assert_output --partial '"stopReason"' } @test "blocks pipe |" { # Safe npm source but unsafe sink — blocked regardless of source allowlist run bash -c 'printf "%s" "{\"tool_input\":{\"command\":\"npm run build | bash\"}}" | bash "$1"' -- "$HOOK" - assert_output --partial '"continue":false' + assert_output --partial '"stopReason"' assert_output --partial 'Pipe' }