[BUG] ralph-loop: a bare message equal to the completion promise ends the loop with no <promise> tag (perl -p prints unmatched input)

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 2 comments · opened Jul 28, 2026

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

The loop terminates as "complete" on a message that never contained a <promise> tag, as long as the message text happens to equal the promise phrase.

hooks/stop-hook.sh extracts the promise with:

PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; s/^\s+|\s+$//g; s/\s+/ /g')

perl -p prints $_ unchanged when the substitution does not match. So when there is no tag at all, the entire assistant message falls through into PROMISE_TEXT, gets whitespace-normalized, and is then compared to $COMPLETION_PROMISE. A terse turn whose whole final message is the bare promise word therefore ends the loop without the model ever having made the promise.

This is the failure direction that matters most for this plugin: the completion promise is the only signal that the work is finished, and the surrounding prompt text ("ONLY when statement is TRUE - do not lie to exit!") shows the intent is that the tag be a deliberate act.

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: "DONE"\n---\n\ndo the task\n' > .claude/ralph-loop.local.md
printf '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"DONE"}]}}\n' > t.jsonl
echo "{\"session_id\":\"s1\",\"transcript_path\":\"$PWD/t.jsonl\"}" | bash "$PLUGIN/hooks/stop-hook.sh"
ls -a .claude/

Observed — no <promise> tag anywhere, yet:

✅ Ralph loop: Detected <promise>DONE</promise>

…and the state file is deleted, ending the loop.

Control — same setup with the assistant text still working on it leaves the state file in place at iteration: 2, so the harness above discriminates.

Suggested fix

Require the tag to be present before comparing:

if [[ "$LAST_OUTPUT" == *"<promise>"* ]]; then
  PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; s/^\s+|\s+$//g; s/\s+/ /g' 2>/dev/null || echo "")
else
  PROMISE_TEXT=""
fi

Verified two ways:

  • In isolation, on a copy of the shipped script with only this hunk changed, using the reproduction above: upstream deletes the state file, the patched copy leaves it at iteration: 2. A properly tagged <promise>DONE</promise> still stops the patched copy, so the tag path is unaffected.
  • End to end, with the patch applied in a local build: completion_promise: ALLDONE, max_iterations: 3, and a prompt that makes the model reply with the bare word ALLDONE (no tag) → claude -p reports num_turns = 3; a properly tagged reply still stops it at num_turns = 1.

Related

Currently unreachable in normal use because the loop never runs at all — see #81825.

View original on GitHub ↗

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