Merge pull request #170 from douw3rd/fix/background-stdin-hang

fix: redirect stdin from /dev/null for all claude execution paths
This commit is contained in:
Frank Bria 2026-02-09 12:29:59 -07:00 committed by GitHub
commit 605e50f725
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 72 additions and 2 deletions

View file

@ -1147,9 +1147,11 @@ execute_claude_code() {
# Use stdbuf to disable buffering for real-time output
# Use portable_timeout for consistent timeout protection (Issue: missing timeout)
# Capture all pipeline exit codes for proper error handling
# stdin must be redirected from /dev/null because newer Claude CLI versions
# read from stdin even in -p (print) mode, causing the process to hang
set -o pipefail
portable_timeout ${timeout_seconds}s stdbuf -oL "${LIVE_CMD_ARGS[@]}" \
2>&1 | stdbuf -oL tee "$output_file" | stdbuf -oL jq --unbuffered -j "$jq_filter" 2>/dev/null | tee "$LIVE_LOG_FILE"
< /dev/null 2>&1 | stdbuf -oL tee "$output_file" | stdbuf -oL jq --unbuffered -j "$jq_filter" 2>/dev/null | tee "$LIVE_LOG_FILE"
# Capture exit codes from pipeline
local -a pipe_status=("${PIPESTATUS[@]}")
@ -1206,7 +1208,10 @@ execute_claude_code() {
if [[ "$use_modern_cli" == "true" ]]; then
# Modern execution with command array (shell-injection safe)
# Execute array directly without bash -c to prevent shell metacharacter interpretation
if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" > "$output_file" 2>&1 &
# stdin must be redirected from /dev/null because newer Claude CLI versions
# read from stdin even in -p (print) mode, causing SIGTTIN suspension
# when the process is backgrounded
if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" < /dev/null > "$output_file" 2>&1 &
then
: # Continue to wait loop
else

View file

@ -642,6 +642,71 @@ EOF
[[ "$found_prompt" == "true" ]]
}
# =============================================================================
# BACKGROUND EXECUTION STDIN REDIRECT TESTS
# Newer Claude CLI reads stdin even in -p mode, causing SIGTTIN suspension
# when the process is backgrounded. Verify /dev/null redirect is present.
# =============================================================================
@test "modern CLI background execution redirects stdin from /dev/null" {
# Verify the implementation in ralph_loop.sh redirects stdin from /dev/null
# to prevent SIGTTIN suspension when claude is backgrounded.
# Without this, newer Claude CLI versions hang indefinitely.
run grep 'portable_timeout.*CLAUDE_CMD_ARGS.*< /dev/null.*&' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
assert_success
[[ "$output" == *'< /dev/null'* ]]
}
@test "live mode execution redirects stdin from /dev/null" {
# Verify the live (streaming) mode also redirects stdin from /dev/null.
# This path is used by ralph --monitor (which adds --live).
# The live mode splits across two lines (line continuation with \),
# so we check the continuation line that has < /dev/null.
local script="${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
# The live mode has LIVE_CMD_ARGS on one line and < /dev/null on the next
run grep '< /dev/null 2>&1 |' "$script"
assert_success
[[ "$output" == *'< /dev/null'* ]]
}
@test "all claude execution paths redirect stdin" {
# Verify that ALL portable_timeout invocations of claude redirect stdin,
# to prevent regressions. There are 3 paths: modern background, live, legacy.
# Legacy uses < "$PROMPT_FILE", the other two must use < /dev/null.
# We check that no portable_timeout line invoking claude lacks a stdin redirect
# (either on the same line or a continuation line).
local script="${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
# All 3 portable_timeout lines that invoke claude should have < somewhere nearby
# Modern background: has < /dev/null on same line
run grep 'portable_timeout.*CLAUDE_CMD_ARGS.*< /dev/null' "$script"
assert_success
# Live mode: has < /dev/null on continuation line
run grep '< /dev/null 2>&1 |' "$script"
assert_success
# Legacy mode: has < "$PROMPT_FILE" on same line
run grep 'portable_timeout.*CLAUDE_CODE_CMD.*< ' "$script"
assert_success
}
@test "modern CLI background execution has comment explaining stdin redirect" {
# Verify the fix is documented with context about why /dev/null is needed
run grep -c 'stdin must be redirected' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
assert_success
# Should appear in both background and live mode sections
[[ "$output" == "2" ]]
}
# =============================================================================
# .RALPHRC CONFIGURATION LOADING TESTS
# Tests for the environment variable precedence fix