[BUG] ralph-loop: --max-iterations 08 silently discards the cap and makes the loop unbounded (leading zero parsed as octal)
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
--max-iterations 08 silently turns a bounded loop into an unbounded one.
scripts/setup-ralph-loop.sh validates the value as a decimal string:
if ! [[ "$2" =~ ^[0-9]+$ ]]; then ... exit 1; fi
MAX_ITERATIONS="$2"
08 passes and is stored verbatim. hooks/stop-hook.sh then hands that string to bash arithmetic, where a leading zero means octal:
if [[ $MAX_ITERATIONS -gt 0 ]] && [[ $ITERATION -ge $MAX_ITERATIONS ]]; then
[[ 08 -gt 0 ]] errors (value too great for base) and returns 1, the && short-circuits, and the whole max-iterations check is skipped. The script's only other reading of "not greater than zero" is unlimited, so the cap is discarded. set -euo pipefail does not help: an if condition is exempt from set -e, and the hook goes on to return decision: block and exit 0.
Three variants:
| input | intended | actual |
|---|---|---|
| 08, 09 | stop at 8 / 9 | never stops (stderr on a Stop hook that exits 0, so the user sees nothing) |
| 010 | stop at 10 | stops at 8 — a silent off-by-N, no diagnostic at all |
| 9223372036854775808 | stop at that value | wraps to a negative number → never stops, and with zero stderr |
This matters more than an ordinary off-by-N because the loop has no manual stop: /cancel-ralph requires a turn, and in claude -p there is no way to invoke it, so an unbounded headless run can only be ended by killing the process.
Reproduction
PLUGIN=~/.claude/plugins/marketplaces/claude-plugins-official/plugins/ralph-loop
cd "$(mktemp -d)"
env -u CLAUDE_CODE_SESSION_ID bash "$PLUGIN/scripts/setup-ralph-loop.sh" do the task --max-iterations 08
grep '^max_iterations:' .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 12); 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:
setup-ralph-loop.sh: line 154: [[: 08: value too great for base (error token is "08")
Max iterations: unlimited
max_iterations: 08
iteration: 13 <- 12 rounds, still running
Note that setup's own summary line prints Max iterations: unlimited, because line 154 contains the identical -gt 0 expression — so the misreport and the bug share a root cause.
Control — the same script with --max-iterations 8 prints STATE FILE DELETED after the same 12 rounds (cap enforced, stops at round 8), which shows the harness above discriminates.
Suggested fix
Force base 10 where the value is parsed, and reject values that overflow rather than storing a number that means something else. In setup-ralph-loop.sh, after the regex check:
MAX_ITERATIONS=$((10#$2))
if [[ $MAX_ITERATIONS -lt 0 ]]; then
echo "❌ Error: --max-iterations is out of range: $2" >&2
exit 1
fi
And defensively in stop-hook.sh, after its own regex validation, so a hand-edited or pre-existing state file cannot reintroduce it:
ITERATION=$((10#$ITERATION))
MAX_ITERATIONS=$((10#$MAX_ITERATIONS))
if [[ $ITERATION -lt 0 ]] || [[ $MAX_ITERATIONS -lt 0 ]]; then
# fail safe (stop) rather than fail open (run forever)
rm "$RALPH_STATE_FILE"; exit 0
fi
With both applied: 08 stops at round 8, 010 stops at round 10, and the overflow value is rejected with exit code 1 before a state file is created.
Related
Currently unreachable in normal use because the loop never runs at all — see #81825.
3 Comments
I’ve implemented and tested a fix for this issue here: sylvesterkaczmarek/claude-code@fcd416b.
The setup script now canonicalizes --max-iterations as decimal before any Bash arithmetic, so values such as 08 and 010 are stored and interpreted as 8 and 10 rather than octal.
The Stop hook also normalizes persisted iteration values defensively, so existing or manually edited state files cannot reintroduce the problem. Values outside Bash’s supported signed integer range are rejected instead of silently wrapping.
Regression checks cover 0, 00, 08, 010, leading-zero decimal values, the maximum supported integer, overflow values, and cap enforcement at the normalized iteration count.
Reproduced on Linux with Claude Code 2.1.233 and ralph-loop 1.0.0 from the official marketplace, using the reproduction from the report.
--max-iterations 08: setup accepts the value, its summary reports max iterations as unlimited, and after 12 stop-hook rounds the loop is still running at iteration 13 — the cap is silently discarded. The only diagnostic is avalue too great for baseline on stderr, which is not surfaced to the user during a session.--max-iterations 010: the loop stops after round 8, not 10 — the leading zero is read as octal, with no diagnostic at all.--max-iterations 8: the cap is enforced and the loop stops at round 8, confirming the harness discriminates.The mechanism matches the report: the value is validated as a decimal string but later evaluated by shell arithmetic, where a leading zero means octal; for
08/09the comparison errors and the entire max-iterations check is skipped, which the script treats as "unlimited". The suggested base-10 normalization at parse time plus a fail-safe range check in the stop hook resolves all variants.This is a bug in the ralph-loop plugin's scripts rather than in the Claude Code CLI itself; flagging it for the plugin maintainers. It deserves attention because a headless (
-p) run has no way to cancel the loop other than killing the process.🤖 Generated with Claude Code
For your information: the label reproduced on this issue does not prevent it from becoming stale and eventually auto-closed. See https://github.com/anthropics/claude-code/issues/87647 for details, and consider upvoting that for visibility.