fix(ralph-loop): session isolation bypass when CLAUDE_CODE_SESSION_ID is empty#1318
fix(ralph-loop): session isolation bypass when CLAUDE_CODE_SESSION_ID is empty#1318pure-maple wants to merge 1 commit intoanthropics:mainfrom
Conversation
…positive Session isolation bug: - CLAUDE_CODE_SESSION_ID is empty in Bash tool execution context - setup-ralph-loop.sh writes empty session_id to state file - stop-hook.sh treats empty session_id as "legacy" and skips isolation - Result: Ralph Loop in one session intercepts ALL sessions in the same project Fix: - setup: fallback to uuidgen when CLAUDE_CODE_SESSION_ID is empty - hook: treat empty STATE_SESSION as invalid, exit 0 (don't intercept) Completion promise false positive: - Perl -pe substitution preserves full input when no <promise> tag exists - If raw output happens to equal the promise text, loop exits prematurely Fix: - Use -ne with explicit match check, only print extracted tag content
|
Thanks for your interest! This repo only accepts contributions from Anthropic team members. If you'd like to submit a plugin to the marketplace, please submit your plugin here. |
There was a problem hiding this comment.
Pull request overview
Fixes session isolation and completion-promise detection in the Ralph Loop plugin to prevent cross-session interception and premature loop termination when <promise> tags are absent.
Changes:
- Ensure
session_idin Ralph Loop state is never empty by generating a fallback ID during setup. - Tighten Stop-hook session isolation logic to avoid intercepting when isolation can’t be enforced.
- Update
<promise>extraction to return empty output when no<promise>tag exists (prevents false positives).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
plugins/ralph-loop/scripts/setup-ralph-loop.sh |
Writes a non-empty session_id into the Ralph Loop state file using a UUID fallback. |
plugins/ralph-loop/hooks/stop-hook.sh |
Enforces session isolation more explicitly and makes <promise> extraction non-matching-safe. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| active: true | ||
| iteration: 1 | ||
| session_id: ${CLAUDE_CODE_SESSION_ID:-} | ||
| session_id: ${CLAUDE_CODE_SESSION_ID:-$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "fallback-$$-$(date +%s)")} |
There was a problem hiding this comment.
session_id is written from $CLAUDE_CODE_SESSION_ID, but when that variable is empty you now generate a new UUID. The Stop hook later enforces isolation by comparing STATE_SESSION (from this file) to HOOK_INPUT.session_id; if $CLAUDE_CODE_SESSION_ID is not the same identifier as HOOK_INPUT.session_id (or is unset in the Bash tool context as described), the generated UUID will never match any real session and the loop will never intercept even the session that started it. Consider obtaining the same session identifier the Stop hook uses (e.g., pass it into this script explicitly), or change the Stop hook to "claim"/persist the first non-empty HOOK_INPUT.session_id into the state file instead of generating a random value here.
| # Use a dedicated extraction that returns empty string when no <promise> tag exists, | ||
| # preventing false positives when the raw output happens to match the promise text. | ||
| PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -ne 'if (/<promise>(.*?)<\/promise>/s) { my $t = $1; $t =~ s/^\s+|\s+$//g; $t =~ s/\s+/ /g; print $t; }' 2>/dev/null || echo "") |
There was a problem hiding this comment.
Using echo "$LAST_OUTPUT" can mis-handle edge cases where the output begins with -n/-e (option parsing) or contains backslash escapes, which can alter the string being scanned for <promise> and lead to incorrect completion detection. Prefer printf '%s' "$LAST_OUTPUT" (or an equivalent non-echo write) when feeding arbitrary text into Perl.
Summary
CLAUDE_CODE_SESSION_IDis empty in Bash execution context<promise>tag exists in outputProblem
When
CLAUDE_CODE_SESSION_IDis not set (which happens in the Bash tool execution context),setup-ralph-loop.shwrites an emptysession_idto the state file. The stop hook's isolation check ([[ -n "$STATE_SESSION" ]]) treats empty as "legacy" and falls through, causing the Ralph Loop to intercept all sessions in the same project directory — not just the session that started it.In our case, a Ralph Loop started in a learning-system research session leaked into 2 unrelated sessions (work tasks and a migration task), burning hundreds of conversation turns before being noticed.
Additionally, the Perl
-pesubstitution for<promise>tag extraction preserves the full input text when no tag is found. If the raw assistant output happens to equal the completion promise string, the loop exits prematurely (false positive).Fix
setup-ralph-loop.sh (line 144):
uuidgenwhenCLAUDE_CODE_SESSION_IDis empty, ensuringsession_idis always non-emptystop-hook.sh (lines 27-35):
STATE_SESSIONas invalid —exit 0instead of falling throughstop-hook.sh (line 136):
perl -pesubstitution withperl -neextraction that only prints content when<promise>tag actually existsTest plan
CLAUDE_CODE_SESSION_IDunset, verify fallback UUID is written<promise>tags<promise>tags