ralph-loop: two copies of setup-ralph-loop.sh at v1.0.0; the unpatched one silently creates unstoppable loops

Status Open
Maintainer reply None cached
Activity 0 comments · opened Aug 19, 2026

Summary

The ralph-loop plugin ships two different versions of scripts/setup-ralph-loop.sh, both declaring version: 1.0.0. One parses --max-iterations / --completion-promise correctly; the other strands them in the prompt body, producing a loop with no exit condition of any kind.

I hit this in a real session: the loop ran to iteration 13 and could not be stopped by any output.

Impact

When the unpatched script runs, the state file lands as:

max_iterations: 0          # user passed 3
completion_promise: null   # user passed SWA-SUPPLY-MVP-ARCH-DONE

Both stop conditions in hooks/stop-hook.sh are then dead:

if [[ $MAX_ITERATIONS -gt 0 ]] && [[ $ITERATION -ge $MAX_ITERATIONS ]]   # 0 → never true
if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]] # null → check skipped

0 is the hook's own encoding for infinite, and a null promise means the <promise> comparison never runs — so even a correctly tagged <promise> cannot exit the loop.

What makes it hard to notice: the prompt is fed back verbatim each iteration, so the user's own --max-iterations 3 is echoed every time, reading exactly as though it had been applied. The setup banner also prints Max iterations: unlimited, but that scrolls past at iteration 1 and is never repeated.

The only recovery is /cancel-ralph (or rm .claude/ralph-loop.local.md).

Root cause

setup-ralph-loop.sh parses $ARGUMENTS with a while/case loop, so --max-iterations only matches when it arrives as its own argv element. When the invocation arrives as a single quoted string, it falls through to the *) catch-all and is stored as prompt text.

The patched copy adds a recovery branch for exactly this, and its own comment describes the failure precisely:

they are stored as prompt text while max_iterations stays 0 and completion_promise stays null. The loop then runs unbounded AND echoes the user's own "--max-iterations 3" back at them every iteration, which reads exactly as if it had been applied. Silent, and unstoppable by design.

The bug is that the fix did not reach both copies, and the version number did not change, so nothing detects the skew or triggers an update.

| Path | Lines | Recovery logic |
|---|---|---|
| plugins/cache/claude-plugins-official/ralph-loop/1.0.0/scripts/setup-ralph-loop.sh | 249 | present |
| plugins/marketplaces/claude-plugins-official/plugins/ralph-loop/scripts/setup-ralph-loop.sh | 204 | absent |

Every other file in the two trees is byte-identical — plugin.json, hooks/hooks.json, hooks/stop-hook.sh, all three commands, LICENSE, README. Only the setup script diverges.

Reproduction

mkdir -p /tmp/ralph-repro && cd /tmp/ralph-repro
bash <marketplace>/scripts/setup-ralph-loop.sh \
  "Do the thing --max-iterations 3 --completion-promise DONE" >/dev/null 2>&1
cat .claude/ralph-loop.local.md

Observed (204-line copy):

max_iterations: 0
completion_promise: null
---
Do the thing --max-iterations 3 --completion-promise DONE

Expected, and what the 249-line copy produces — for both this form and separate-argv form:

max_iterations: 3
completion_promise: "DONE"

Suggested fixes

  1. Ship the 249-line script to every distribution path, and bump the version so skew is detectable. Two artifacts at the same version with different behaviour is the underlying defect.
  2. Treat max_iterations: 0 + completion_promise: null as a hard error at setup time, not as "run forever". A loop with no exit condition is almost never intended, and today it's reachable by a plain typo in quoting.
  3. Re-print the bound each iteration in systemMessage (e.g. iteration 4/∞ — NO EXIT CONDITION SET). The current message says To stop: output <promise>…</promise> even when completion_promise is null and that promise can never be honoured — actively misleading.
  4. Consider making /cancel-ralph discoverable from the loop itself; the setup banner says "This loop cannot be stopped manually!", which is false — the plugin ships a cancel command.

Environment

  • Claude Code on Windows (Git Bash)
  • ralph-loop v1.0.0 from claude-plugins-official
  • Marketplace tree delivered via GCS (.gcs-sha), not a git clone — so it can't be patched by PR downstream

View original on GitHub ↗