feat: add circuit breaker to prevent Stop hook infinite loop

- Track block attempts per session in .attempts file
- After 3 attempts (configurable via REVIEW_MAX_ATTEMPTS), trip breaker
- Circuit breaker allows exit with warning instead of crashing
- Clear attempts on approve or when breaker trips
- Add 3 new tests for circuit breaker behavior (46 total)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dan 2026-01-10 10:33:24 -08:00
parent 9df3aedc2f
commit 0356ed237c
2 changed files with 59 additions and 7 deletions

View file

@ -40,24 +40,44 @@ get_state_file() {
echo "${STATE_DIR}/${session_id}.json"
}
# Circuit breaker settings
MAX_BLOCK_ATTEMPTS="${REVIEW_MAX_ATTEMPTS:-3}"
# Commands
cmd_check() {
local session_id=$(get_session_id "${1:-}")
local state_file=$(get_state_file "$session_id")
local attempts_file="${STATE_DIR}/${session_id}.attempts"
# Check for stop_hook_active to prevent infinite loops
# Claude Code passes JSON on stdin with this flag when we're in a continuation
# Check for stop_hook_active (continuation after previous block)
local hook_input=""
local stop_hook_active="false"
if [[ ! -t 0 ]]; then
# stdin is not a terminal, try to read hook input
hook_input=$(cat 2>/dev/null || true)
fi
if [[ -n "$hook_input" ]]; then
local stop_hook_active=$(echo "$hook_input" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
stop_hook_active=$(echo "$hook_input" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
fi
# Track block attempts for circuit breaker
local attempts=0
if [[ -f "$attempts_file" ]]; then
attempts=$(cat "$attempts_file" 2>/dev/null || echo "0")
fi
# Circuit breaker: if we've hit max attempts, allow exit
if [[ "$stop_hook_active" == "true" ]]; then
# Already in a Stop hook continuation - allow exit to prevent infinite loop
echo "Stop hook continuation detected - allowing exit to prevent loop"
attempts=$((attempts + 1))
echo "$attempts" > "$attempts_file"
if [[ $attempts -ge $MAX_BLOCK_ATTEMPTS ]]; then
# Circuit breaker tripped
echo "⚠ CIRCUIT BREAKER: Max block attempts ($MAX_BLOCK_ATTEMPTS) reached"
echo "Review was not completed. Allowing exit to prevent infinite loop."
echo "Session: $session_id"
# Reset attempts for next time
rm -f "$attempts_file"
exit 0
fi
fi
@ -124,6 +144,7 @@ EOF
cmd_approve() {
local session_id=$(get_session_id "${1:-}")
local state_file=$(get_state_file "$session_id")
local attempts_file="${STATE_DIR}/${session_id}.attempts"
if [[ ! -f "$state_file" ]]; then
echo "No review state for session: $session_id"
@ -140,6 +161,9 @@ cmd_approve() {
}
EOF
# Clear circuit breaker attempts
rm -f "$attempts_file"
echo "✓ Review approved for session: $session_id"
}

View file

@ -290,6 +290,34 @@ output=$(run_gate check reset-session)
exit_code=$?
assert_exit_code "Check blocks after re-enable" "2" "$exit_code"
# --- Circuit Breaker ---
echo ""
echo "## Circuit Breaker (Prevents Infinite Loop)"
# Enable a review
run_gate enable test-circuit > /dev/null
# Simulate multiple stop_hook_active=true continuations
# Default MAX_BLOCK_ATTEMPTS is 3
echo '{"stop_hook_active": true}' | run_gate check test-circuit > /dev/null 2>&1 || true # attempt 1
echo '{"stop_hook_active": true}' | run_gate check test-circuit > /dev/null 2>&1 || true # attempt 2
# Third attempt should trip circuit breaker
output=$(echo '{"stop_hook_active": true}' | run_gate check test-circuit 2>&1)
exit_code=$?
assert_exit_code "Circuit breaker trips after max attempts" "0" "$exit_code"
assert_output_contains "Circuit breaker message shown" "CIRCUIT BREAKER" "$output"
# Attempts file should be cleaned up
if [[ ! -f "$TEST_STATE_DIR/test-circuit.attempts" ]]; then
echo -e "${GREEN}PASS${NC}: Attempts file cleaned after circuit breaker"
((PASSED++))
else
echo -e "${RED}FAIL${NC}: Attempts file should be cleaned after circuit breaker"
((FAILED++))
fi
# --- Clean Command ---
echo ""
echo "## Clean Command"