[BUG] ralph-loop Stop hook blocks unrelated parallel sessions — session_id guard is ineffective
Stop hook session isolation for ralph-loop plugin
Title
[BUG] ralph-loop Stop hook blocks unrelated parallel sessions — session_id guard is ineffective
Labels
bug, area:hooks, area:plugins
Body
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
The ralph-loop plugin's Stop hook fires in every Claude Code session in the same project directory. When one session has an active Ralph loop, the hook blocks exit in all sessions — including unrelated ones that never started a loop.
The hook at plugins/ralph-loop/hooks/stop-hook.sh (lines 31-35) has a session isolation guard:
STATE_SESSION=$(echo "$FRONTMATTER" | grep '^session_id:' | sed 's/session_id: *//' || true)
HOOK_SESSION=$(echo "$HOOK_INPUT" | jq -r '.session_id // ""')
if [[ -n "$STATE_SESSION" ]] && [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
exit 0
fi
However, this guard is ineffective because the setup script (scripts/setup-ralph-loop.sh line 144) writes:
session_id: ${CLAUDE_CODE_SESSION_ID:-}
The $CLAUDE_CODE_SESSION_ID environment variable is not reliably set (see #24371, #20132, #25642). When it's empty, STATE_SESSION is empty, the -n "$STATE_SESSION" check fails, and the guard is silently skipped — allowing the hook to block every session in the project.
Root Cause
Two independent issues compound:
CLAUDE_CODE_SESSION_IDis not exposed as an environment variable — The setup script uses this env var to stamp the state file, but Claude Code does not reliably export it. This means the state file often hassession_id:with an empty value.
- Empty session_id bypasses the guard — The guard at line 33 checks
[[ -n "$STATE_SESSION" ]]first. When the session_id was never written (empty string), this check fails and the entire guard is skipped, falling through to block the stop.
Impact
- Cross-session prompt injection: As documented in #29477, one session's ralph-loop prompt leaks into another session as a
<system-reminder>, causing the receiving session to work on the wrong task - Wasted tokens: Parallel sessions are blocked from exiting and re-fed someone else's prompt
- Silent failure: No warning when session isolation fails — the user only notices when the wrong session starts working on a different task
Prior Issues (all auto-closed, unfixed)
- #15885 — Multiple ralph state files for parallel use (auto-closed as stale)
- #29477 — Stop hook output leaking between sessions (auto-closed as duplicate of #15047)
- #15047 — ralph-wiggum stop hook triggered in separate session (closed as stale)
- #29494 — feat: expose session_id in Stop hook input JSON (closed as duplicate)
Proposed Fix
The Stop hook input JSON does include session_id as a common field (per hook docs). The fix should:
- In the setup script: Accept
--session-id <id>as an explicit argument, with a fallback chain:--session-idflag →$CLAUDE_SESSION_ID→$CLAUDE_CODE_SESSION_ID→ transcript path UUID extraction
- In the command template: Pass the session ID from the agent context (where Claude Code does know its session ID) into the setup script via
--session-id
- In the stop hook: The existing guard logic is correct when session_id is populated. No changes needed there.
This is a plugin-side fix that works regardless of whether CLAUDE_CODE_SESSION_ID is ever properly exported.
Steps to Reproduce
- Enable ralph-loop plugin globally
- Open two terminal windows in the same project directory
- In Session A:
/ralph-loop "Task A" --max-iterations 5 - In Session B: Do unrelated work, then try to exit
- Session B is blocked by the Stop hook and fed Session A's prompt
Expected Behavior
Session B should exit normally. The Stop hook should detect that the active Ralph loop belongs to Session A and skip.
Environment
- Claude Code (latest)
- Any platform (Linux, macOS, Windows)
- ralph-loop plugin enabled globally or per-project
Additional Context
A community workaround exists (https://github.com/teknologist/claude-ralph-wiggum-pro) that uses session-scoped state files. The simpler fix is to properly populate the session_id field in the existing state file using the hook input JSON rather than unreliable env vars.
This issue has 9 comments on GitHub. Read the full discussion on GitHub ↗