[BUG] ralph-loop: --max-iterations 08 silently discards the cap and makes the loop unbounded (leading zero parsed as octal)

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

--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.

View original on GitHub ↗

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