[BUG] ralph-loop: iteration counter freezes when the state file reads "iteration:N" (writer stricter than reader), so max_iterations is never reached
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.
3 Comments
I reproduced this against current
main. Withmax_iterations: 3, the Stop hook reaches the limit foriteration: 1anditeration: 1, but the reader-acceptediteration:1form remains frozen after eight rounds because the writer only replaces^iteration: .*. The controls assert the loggedMax iterations (3) reacheddeletion 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 canonicaliteration: Noutput. 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 withiteration: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?
Reproduced on Linux with Claude Code 2.1.233 and
ralph-loop1.0.0 installed from theclaude-plugins-officialmarketplace.Running your harness verbatim: with
iteration:1(no space) in the state file, 8 rounds of the stop hook leave the counter frozen atiteration:1despitemax_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 returnsdecision: blockwith 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:Nstate 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
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.