From ab59cab07f5a3b36eda8f624a9d216275f48e88d Mon Sep 17 00:00:00 2001 From: Joseph Fung Date: Sat, 25 Apr 2026 22:22:33 -0400 Subject: [PATCH 1/2] fix: use continue:true in block_with_hint so blocked commands don't terminate session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit continue:false caused Claude Code to shut down the agent loop after any blocked command, requiring manual user intervention to resume. The intent of the hook is to reject the command and surface a split-command hint so Claude can retry — not to end the session. Changing to continue:true keeps the agent loop running after the rejection, so Claude reads the stopReason hint and can immediately issue the commands as separate Bash calls without user intervention. Update assert_blocked in the test suite to check for stopReason presence rather than the now-absent continue:false field. Closes #22 --- hooks/no-chaining.sh | 6 +++++- hooks/no-chaining.test.sh | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/hooks/no-chaining.sh b/hooks/no-chaining.sh index cb00be6..df97d68 100755 --- a/hooks/no-chaining.sh +++ b/hooks/no-chaining.sh @@ -122,6 +122,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:" @@ -132,7 +136,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 27180c5..0254b07 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 From fcf60d7bebe1fbfa2d1b88a410f60b96de9fdb3a Mon Sep 17 00:00:00 2001 From: Joseph Fung Date: Sat, 25 Apr 2026 22:26:34 -0400 Subject: [PATCH 2/2] fix: update bats tests to check stopReason instead of continue:false --- tests/hooks/no-chaining.bats | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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' }