The Ralph Loop you already know — with the footguns filed down.
A hardened fork of Anthropic's
ralph-loopplugin for Claude Code. Same while-true agent technique, same slash command surface, same stop hook — but portable across operating systems, safe from runaway iteration cost, and with regex-based completion signals when a literal phrase isn't flexible enough.
The original ralph-loop plugin from Anthropic is elegant — a Stop hook
that returns {"decision":"block", "reason":"<same prompt>"} in a while-true
loop, so the agent iterates on the same task until a <promise>…</promise>
tag matches or a hard iteration cap is reached.
In day-to-day use it has three rough edges:
- The args file lives in
/tmp. That's fine on macOS and Linux but breaks on Windows, and two concurrent Claude Code sessions in the same project can race for the same path. - No iteration cap by default. If you forget
--max-iterationsand your--completion-promisenever fires, Ralph runs forever — and every iteration is a full Claude turn on your bill. - Completion matching is a literal string compare.
TASK COMPLETEandtask completedon't match.ALL_DONE_v2can't be caught byALL_DONE_.*.
claudecode-ralph-loop-plus fixes those three without changing what Ralph
fundamentally is.
| Concern | Upstream ralph-loop |
ralph-loop-plus |
|---|---|---|
| Args file location | /tmp/ralph-loop-args.txt |
.claude/.ralph-args.txt (project-scoped) |
| Windows portability | ❌ (no /tmp) |
✅ |
| Default when no stopping condition | Runs forever | Auto-cap at 50 iterations |
| Regex completion matching | ❌ | ✅ --completion-regex <PATTERN> (ERE) |
| Case-insensitive promises | ❌ | ✅ --promise-case-insensitive |
| Non-destructive inspector command | ❌ | ✅ /ralph-status |
| Structured logging | ❌ | ✅ Optional, via RALPH_LOG_FILE (JSONL) |
| Flag parsing | sed -E regex (fragile) |
awk match() literal (O(n), inherited fix) |
| Shell-injection in slash command | Historically vulnerable | Write-tool-first contract (inherited fix) |
The dual-step Write → Bash(--prompt-file) initialization is inherited
from the upstream fix that ships in Anthropic's plugin cache. See
CHANGELOG.md for the full diff.
git clone https://github.com/daniloaguiarbr/claudecode-ralph-loop-plus.git \
~/.claude/plugins/local/claudecode-ralph-loop-plusThen enable it in your ~/.claude/settings.json:
{
"enabledPlugins": {
"claudecode-ralph-loop-plus@local": true
}
}Restart Claude Code. /ralph-loop, /ralph-status, /cancel-ralph, and
/help should all appear.
/ralph-loop Refactor src/cache.ts so every public method has a doctest. Output <promise>DOCTESTS COMPLETE</promise> when every public method has one. --completion-promise 'DOCTESTS COMPLETE' --max-iterations 15
/ralph-status
Either let the promise match, let the iteration cap expire, or:
/cancel-ralph
/ralph-loop [PROMPT...] [OPTIONS]
OPTIONS:
--max-iterations <n> Hard cap. 0 = unlimited.
--completion-promise '<text>' Literal phrase inside <promise>…</promise>.
--completion-regex '<pattern>' ERE regex matched against the text inside
<promise>…</promise>. grep -E semantics.
--promise-case-insensitive Compare promises case-insensitively.
--unlimited Explicitly disable the safety auto-cap.
-h, --help Show usage.
If you set none of --max-iterations, --completion-promise, or
--completion-regex, the loop is auto-capped at 50 iterations. That's
deliberately low — it's a guardrail, not a ceiling. Override it:
export RALPH_DEFAULT_MAX_ITERATIONS=200 # Raise the default
/ralph-loop Do the thing --unlimited # Disable on this one loopSet RALPH_LOG_FILE before starting the loop:
export RALPH_LOG_FILE=.claude/ralph-loop.log.jsonlEvery loop_started and iteration_continue event is appended as one
JSON object per line. Good for postmortems on long-running loops, or for
counting how many iterations a task really took.
┌──────────────┐ writes ┌─────────────────────────────┐
│ /ralph-loop │ ───────────────▶ │ .claude/.ralph-args.txt │
│ (slash cmd) │ │ (raw, byte-for-byte prompt) │
└──────────────┘ └──────────┬──────────────────┘
│ │
│ invokes (via Bash tool) │ reads
▼ ▼
┌──────────────────────────┐ ┌─────────────────────────────┐
│ scripts/ │ ────────▶│ .claude/ralph-loop.local.md │
│ setup-ralph-loop.sh │ writes │ (YAML frontmatter + prompt) │
└──────────────────────────┘ └──────────┬──────────────────┘
│ read by stop hook
▼
┌──────────────────────────────────────────┐
│ hooks/stop-hook.sh │
│ — parses frontmatter │
│ — checks iteration, max, promise/regex │
│ — returns {"decision":"block", │
│ "reason":<same prompt>} to continue │
│ — or exit 0 to allow stop │
└──────────────────────────────────────────┘
The key architectural idea is the same as upstream: the prompt never
crosses a shell boundary. The slash command uses the Write tool (no
shell interpretation) to drop the raw prompt on disk; the setup script
reads it back with cat and parses flags with awk match() substring
literals (not regex, not eval).
Ralph exits the loop when the text inside the first <promise>…</promise>
tag in the agent's most recent text block satisfies one of these:
Literal mode (default when --completion-promise is set):
--completion-promise 'DOCTESTS COMPLETE'
Matches <promise>DOCTESTS COMPLETE</promise> exactly. Whitespace inside
is normalized first (leading/trailing stripped, internal collapsed to one
space). Add --promise-case-insensitive to relax case matching.
Regex mode (when --completion-regex is set):
--completion-regex 'FEATURE_[A-Z_]+_SHIPPED'
Matches <promise>FEATURE_AUTH_SHIPPED</promise> or
<promise>FEATURE_SEARCH_SHIPPED</promise>. Uses grep -E semantics.
If both --completion-promise and --completion-regex are set, literal
match is tried first, then regex.
This is a derivative work of Anthropic's ralph-loop plugin, licensed
under Apache 2.0 (inherited). The NOTICE file credits the original
authors. The underlying technique is by Geoffrey Huntley
(ghuntley.com/ralph). Ralph Wiggum is a
Simpsons character, chosen by Huntley for his deterministically bad
persistence — which turns out to be a great model for iterative AI
workflows.
If you want the original plugin instead, it ships in
anthropics/claude-plugins-official
under plugins/ralph-loop/.
claudecode-ralph-loop-plus/
├── .claude-plugin/
│ └── plugin.json # manifest
├── commands/
│ ├── ralph-loop.md # /ralph-loop (Write + --prompt-file)
│ ├── ralph-status.md # /ralph-status (new in Plus)
│ ├── cancel-ralph.md # /cancel-ralph
│ └── help.md # /help
├── hooks/
│ ├── hooks.json # registers Stop hook
│ └── stop-hook.sh # block + feed prompt back
├── scripts/
│ └── setup-ralph-loop.sh # flag parsing + state file creation
├── tests/
│ └── smoke.sh # bash -n + JSON validation + ShellCheck
├── .github/workflows/
│ └── validate.yml # CI on push/PR
├── LICENSE # Apache 2.0 (inherited)
├── NOTICE # attribution
├── README.md # this file
├── CHANGELOG.md # Keep a Changelog
├── CONTRIBUTING.md
├── SECURITY.md
└── CODE_OF_CONDUCT.md
Slash commands don't appear after install. Restart Claude Code after
editing ~/.claude/settings.json. Verify plugin.json is inside
.claude-plugin/ (not at the repo root).
The loop never stops. You probably passed no stopping condition AND
used --unlimited. Either remove --unlimited, add --max-iterations,
or set a --completion-promise / --completion-regex that actually fires.
Windows line endings corrupt the state file. Add
.claude/ralph-loop.local.md text eol=lf to your .gitattributes, or
run dos2unix on it if you've edited it by hand.
I want to reset state. Delete .claude/ralph-loop.local.md and
.claude/.ralph-args.txt. Both are gitignored by default via this repo's
.gitignore — copy those lines into your project's .gitignore too.
See CONTRIBUTING.md. TL;DR: open an issue first for
anything non-trivial, run tests/smoke.sh before PR, keep the Ralph
philosophy intact (simple, few moving parts, no magic).
See SECURITY.md. The primary attack surface is shell
metacharacter injection via the user prompt; the Write-first contract
eliminates it in normal use. If you find a way past that, email
rather than filing a public issue.
Apache 2.0 — same as upstream. Attribution preserved in NOTICE.
Fork endurecido do plugin ralph-loop da Anthropic para Claude Code.
Mesma técnica de loop agêntico (Ralph Wiggum), mesmo Stop hook, mesmos
slash commands — porém:
- Arquivo de argumentos em
.claude/.ralph-args.txtem vez de/tmp→ funciona no Windows, sem colisão entre sessões concorrentes. - Cap automático de 50 iterações quando você não define condição de
parada → protege sua conta de loop infinito. Use
--unlimitedpara desativar. - Matching de completion por regex via
--completion-regex. - Matching case-insensitive via
--promise-case-insensitive. - Comando
/ralph-statuspara inspecionar o loop sem mexer no estado. - Logging estruturado opcional em JSON Lines via
RALPH_LOG_FILE.
Uso e instalação idênticos ao original (veja "Quick start" acima).
Licença Apache 2.0 herdada do upstream; atribuição em NOTICE.