[BUG] ralph-loop: the expected completion promise is not whitespace-normalized, so a promise containing a double space can never match
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 (2.1.220)
Plugin
ralph-loop 1.0.0 (claude-plugins-official marketplace)
Summary
A completion promise containing a double space (or leading/trailing whitespace) can never match, so the loop cannot be stopped even when the model truthfully emits the exact phrase it was given.
hooks/stop-hook.sh normalizes only the observed side:
PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; s/^\s+|\s+$//g; s/\s+/ /g')
while the expected side is read from the state file with only surrounding quotes stripped:
COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/')
and the two are compared literally. setup-ralph-loop.sh stores --completion-promise verbatim (COMPLETION_PROMISE="$2"), so 'ALL DONE' is expected as ALL DONE but can only ever be observed as ALL DONE.
Because the promise is the intended way to end the loop, and the loop has no manual stop in claude -p, an unbounded loop configured this way can only be ended by killing the process.
Reproduction
PLUGIN=~/.claude/plugins/marketplaces/claude-plugins-official/plugins/ralph-loop
cd "$(mktemp -d)"; mkdir .claude
printf -- '---\niteration: 1\nsession_id:\nmax_iterations: 20\ncompletion_promise: "ALL DONE"\n---\n\ndo the task\n' > .claude/ralph-loop.local.md
printf '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"<promise>ALL DONE</promise>"}]}}\n' > t.jsonl
echo "{\"session_id\":\"s1\",\"transcript_path\":\"$PWD/t.jsonl\"}" | bash "$PLUGIN/hooks/stop-hook.sh" >/dev/null
grep '^iteration' .claude/ralph-loop.local.md 2>/dev/null || echo "STATE FILE DELETED"
Observed: iteration: 2 — the promise was honoured exactly as configured and the loop kept going.
Control — the same run with a single space in both places prints STATE FILE DELETED, so the harness above discriminates.
Suggested fix
Normalize the expected side the same way the observed side is normalized, immediately before the comparison:
COMPLETION_PROMISE_NORM=$(printf '%s' "$COMPLETION_PROMISE" | perl -0777 -pe 's/^\s+|\s+$//g; s/\s+/ /g' 2>/dev/null || printf '%s' "$COMPLETION_PROMISE")
if [[ -n "$PROMISE_TEXT" ]] && [[ "$PROMISE_TEXT" = "$COMPLETION_PROMISE_NORM" ]]; then
(Keeping the original $COMPLETION_PROMISE for the user-facing messages, so what is echoed still matches what the user typed.)
Verified in isolation, on a copy of the shipped script with only this hunk changed, using the reproduction above: upstream leaves the state file at iteration: 2, the patched copy removes it and ends the loop.
One incidental note on the comparison line: the existing comment says
# Use = for literal string comparison (not pattern matching)
# == in [[ ]] does glob pattern matching which breaks with *, ?, [ characters
The reasoning is inaccurate — in bash [[ a = b ]] and [[ a == b ]] behave identically, and what actually disables glob matching here is that the right-hand operand is quoted. The code is safe as written (verified: a promise of DONE* does not match an observed DONEXYZ), but the comment would mislead anyone who later removes the quotes.
Related
Currently unreachable in normal use because the loop never runs at all — see #81825.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗