Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion hooks/no-chaining.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <reason-prefix> <delimiter-regex> <original-command>
#
# 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:"
Expand All @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions hooks/no-chaining.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions tests/hooks/no-chaining.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}

Expand Down
Loading