[BUG] ralph-loop Stop hook blocks unrelated parallel sessions — session_id guard is ineffective

Resolved 💬 9 comments Opened Mar 26, 2026 by graffhyrum Closed May 24, 2026

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:

  1. CLAUDE_CODE_SESSION_ID is 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 has session_id: with an empty value.
  1. 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:

  1. In the setup script: Accept --session-id <id> as an explicit argument, with a fallback chain: --session-id flag → $CLAUDE_SESSION_ID$CLAUDE_CODE_SESSION_ID → transcript path UUID extraction
  1. 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
  1. 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

  1. Enable ralph-loop plugin globally
  2. Open two terminal windows in the same project directory
  3. In Session A: /ralph-loop "Task A" --max-iterations 5
  4. In Session B: Do unrelated work, then try to exit
  5. 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.

View original on GitHub ↗

This issue has 9 comments on GitHub. Read the full discussion on GitHub ↗