[BUG] ralph-loop: iteration counter freezes when the state file reads "iteration:N" (writer stricter than reader), so max_iterations is never reached

Status Open
Reported on v2.1.220
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Jul 28, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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

hooks/stop-hook.sh reads the iteration counter with a pattern that tolerates zero spaces, but writes it back with one that requires exactly one space. A state file containing iteration:1 is therefore readable but not writable: the counter never advances, max_iterations is never reached, and the loop runs forever.

Reader (accepts iteration:1):

ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')

Writer (requires iteration: , one space):

sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$RALPH_STATE_FILE" > "$TEMP_FILE"

The mismatch is silent — the hook still returns decision: block and exits 0, so from the outside the loop simply never ends.

setup-ralph-loop.sh always writes the space, so this is reachable via a hand-edited, copied, or externally generated state file rather than through the happy path. It is cheap to make robust, and the failure mode (an unstoppable loop) is expensive: /cancel-ralph needs a turn and cannot be invoked at all in claude -p.

Reproduction

PLUGIN=~/.claude/plugins/marketplaces/claude-plugins-official/plugins/ralph-loop
cd "$(mktemp -d)"; mkdir .claude
printf -- '---\niteration:1\nsession_id:\nmax_iterations: 3\ncompletion_promise: null\n---\n\ndo the task\n' > .claude/ralph-loop.local.md
printf '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"still working"}]}}\n' > t.jsonl
for i in $(seq 1 8); do
  [ -f .claude/ralph-loop.local.md ] || break
  echo "{\"session_id\":\"s1\",\"transcript_path\":\"$PWD/t.jsonl\"}" | bash "$PLUGIN/hooks/stop-hook.sh" >/dev/null 2>&1
done
grep '^iteration' .claude/ralph-loop.local.md 2>/dev/null || echo "STATE FILE DELETED"

Observed after 8 rounds with max_iterations: 3: iteration:1 — frozen.

Control — identical run with iteration: 1 (one space) prints STATE FILE DELETED, so the harness above discriminates.

Suggested fix

Make the writer as lenient as the reader:

sed "s/^iteration:.*/iteration: $NEXT_ITERATION/" "$RALPH_STATE_FILE" > "$TEMP_FILE"

Verified: with that one-character change the same 8-round run reaches the cap and removes the state file.

Related

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

View original on GitHub ↗

3 Comments

yxl-lab · 18 days ago

I reproduced this against current main. With max_iterations: 3, the Stop hook reaches the limit for iteration: 1 and iteration: 1, but the reader-accepted iteration:1 form remains frozen after eight rounds because the writer only replaces ^iteration: .*. The controls assert the logged Max iterations (3) reached deletion reason, so this is not corrupt-state cleanup.

I have an uncommitted one-line patch that applies the lenient writer match only through the first iteration: line, so it accepts zero-space input while still writing canonical iteration: N output. Regression fixtures confirm all three reader-accepted literal-space forms reach the limit, zero-space input is written back canonically, and prompt body text beginning with iteration: is not rewritten. bash -n, git diff --check, and the exact one-file scope all pass.

I found no PR for #81829 or the same iteration-writeback mismatch. PRs #66573 and #78371 touch the same script in other blocks, while #80495 does not modify the Stop hook; none changes the iteration writeback hunk. Is this one-line scope welcome as a PR?

bcherny collaborator · 5 days ago

Reproduced on Linux with Claude Code 2.1.233 and ralph-loop 1.0.0 installed from the claude-plugins-official marketplace.

Running your harness verbatim: with iteration:1 (no space) in the state file, 8 rounds of the stop hook leave the counter frozen at iteration:1 despite max_iterations: 3 — the loop would never terminate. The one-space control run deletes the state file after reaching the cap, exactly as you described. I also confirmed the failure is silent: in the frozen state the hook still exits 0 and returns decision: block with a message claiming the next iteration number, while the file never changes.

The analysis matches what ships in the plugin: the counter is read tolerantly (zero or more spaces after the colon) but written back with a pattern requiring exactly one space, so a iteration:N state file is readable but never updatable. Since the plugin's own setup always writes the space, this only bites hand-edited, copied, or externally generated state files — but the cost when it does is an unstoppable loop, and the suggested one-character writer fix is the right shape.

🤖 Generated with Claude Code

yxl-lab · 4 days ago

Thanks for confirming the reproduction.

I prepared the tested one-line fix here:
https://github.com/yxl-lab/claude-code/commit/e5481e4c159e9272f8afa405cae8b88adba6d13c

GitHub currently says that opening new pull requests is restricted to repository collaborators, so I cannot open the prepared Draft PR from my fork. The branch comparison is available here:
https://github.com/anthropics/claude-code/compare/main...yxl-lab:claude-code:fix/81829-ralph-iteration-spacing

Please feel free to cherry-pick the commit, or let me know when external PR creation is available.