Skip to content
RALPH LOOP

How to Run a Ralph Loop With Claude Code

A green CRT terminal showing Claude Code running in a looping Ralph loop inside a Docker sandbox

A ralph loop with Claude Code is the default path: install Ralph, log in to Claude once inside its sandbox, then run ./ralph.sh with no agent flag at all. Claude Code is Ralph’s default agent, so the bare command already wraps claude in headless print mode, hands it a fresh context window every iteration, and keeps re-running it against your task list until the work is done or you hit the iteration cap.

Claude is also the only agent whose output Ralph fully parses. That one fact is why the default is the default, and it changes what you see on screen while a run is happening. This post is the end-to-end setup walkthrough, from an empty repo to a reviewable diff. If the loop itself is new to you, read what the Ralph technique is first. If you want the flag-level mechanics of driving Claude in a loop, the companion piece is running Claude Code in a loop. Consider this the brand recipe and that one the mechanics deep dive.

The differentiator: Claude gives Ralph the richest live telemetry

Section titled “The differentiator: Claude gives Ralph the richest live telemetry”

Every supported agent runs the same loop. Only Claude streams structured JSON back that Ralph can read line by line.

Look at scripts/lib/agents.sh. The helper agent_uses_stream_json() returns success for exactly one agent:

Terminal window
agent_uses_stream_json() {
[ "$1" = "claude" ]
}

And build_agent_command() emits Claude with two extra flags no other agent gets:

Terminal window
sbx run --name ralph-claude-<project>-<hash8> claude . -- --output-format stream-json --verbose -p "$PROMPT_CONTENT"

The --output-format stream-json --verbose pair turns Claude’s run into a stream of JSON events instead of an opaque wall of text. Ralph consumes that stream to power its live step detection (Thinking, Implementing, Testing) and the rolling preview of what the agent is doing right now. With Codex, Cursor, Copilot, Gemini, or OpenCode you still get a working loop, commits, and history logs, but you do not get the same real-time read on the agent’s internal state. If you want to watch a long unattended run and actually know what phase it is in, Claude is the agent with the most to say.

That is the practical case for the default. Not model quality (pick whatever your account supports), but observability. Ralph was built around Claude’s stream first, and everything else is a good citizen on top of the same machinery.

Two ways to loop Claude: the Stop Hook plugin vs the hackable script

Section titled “Two ways to loop Claude: the Stop Hook plugin vs the hackable script”

Anthropic shipped an official way to loop Claude Code: a plugin that registers a Stop Hook, which fires when Claude finishes and re-injects the prompt so it keeps going. It lives in the Claude Code docs, and it is a clean, in-process way to keep a single Claude session churning.

Ralph takes the other road. Instead of a hook that resumes the same session, ralph.sh is a Bash loop that spawns a brand new claude process every iteration inside a Docker sandbox, with a clean context window each time. This is the original shape of the technique, popularized in Geoffrey Huntley’s Ralph writeup, where “Ralph is a Bash loop” is the whole point.

The trade-off is deliberate:

  • The Stop Hook keeps one session alive and re-prompts it. Continuity is the feature.
  • ralph.sh throws the session away and rebuilds state from disk. Discarding continuity is the feature, because it kills context rot on long runs.

Ralph’s script is also yours to edit. The verification stack, the sandbox boundary, the prompt, the step detection, all of it is in a shell script you can read and change. If you want the shape of the wrapper itself, that is covered in the Ralph loop shell script.

Run the installer in your project root. It writes the ralph.sh script and the .agent/ directory into the repo.

Terminal window
npx @pageai/ralph-loop

The installer asks for your dev server URL and port and your default agent. Claude is the sensible pick here, and it is already the fallback if you skip the choice. After install, make the script executable if it is not already:

Terminal window
chmod +x ralph.sh

Ralph runs each agent inside a Docker Sandbox, so install and sign in to Docker Sandboxes first. The Docker Sandboxes docs cover the official setup. Under the hood Ralph drives the standalone sbx CLI.

The loop runs a task list. It does not invent one for you. Turn your requirements into a PRD and a set of task specs with the prd-creator skill before you run anything.

Open Claude Code (or any agent) in plan mode and prompt it:

Use the prd-creator skill to help me create a PRD and task list for the below requirements.
An app is already set up with React, Tailwind CSS and TypeScript.
Requirements:
- A SaaS product that helps users manage their finances.
- Track income and expenses, create invoices, generate reports.
- Use shadcn/ui for components and Supabase for the database.
// etc.

The skill writes .agent/prd/PRD.md, a short .agent/prd/SUMMARY.md, and per-task specs under .agent/tasks/, indexed by .agent/tasks.json. Review each task individually before you start. A clear task list with real acceptance criteria is the single biggest factor in whether an overnight run succeeds.

Step 3: Log in to Claude inside the sandbox

Section titled “Step 3: Log in to Claude inside the sandbox”

Claude runs inside an isolated sandbox, not on your host, so it needs credentials in that environment. Authenticate once with the login action. Claude is the default, so you can drop the agent flag, but naming it keeps things explicit:

Terminal window
./ralph.sh --login --agent claude

Ralph prints the login command for every supported agent, highlights the Claude one, and drops you into the sandbox shell. The command it runs to create the box looks like this:

Terminal window
sbx run --name ralph-claude-<project>-<hash8> claude .

Inside the sandbox, authenticate Claude once and follow the prompt. If OAuth login does not persist, the fallback is an API key: Docker Sandboxes forward environment variables at creation time, so you can set ANTHROPIC_API_KEY in your shell before recreating the box. When Claude is not authenticated at the start of a run, Ralph detects the failure and tells you to run /login, so it never burns iterations on a box that can never make progress.

Each agent gets its own deterministic sandbox name, built from the agent slug, the project directory, and an 8-character hash of the absolute path:

ralph-claude-<project>-<hash8>

Print the exact name for your project without starting a run:

Terminal window
./ralph.sh --print-name --agent claude

If your tasks run a dev server you want to reach from the host, publish the configured port to the Claude sandbox. The port comes from the dev-server URL you set during install:

Terminal window
./ralph.sh --ports --agent claude

Per-agent names keep state separate. Your Claude sandbox and, say, your Cursor sandbox never share credentials or installed tools, so you can run both against the same repo without one clobbering the other. If you later want the Cursor variant, that walkthrough is running a Ralph loop with the Cursor CLI.

With a task list in place and Claude authenticated, start the loop. Because Claude is the default, the bare command already runs it:

Terminal window
# 10 iterations (the default), Claude by default
./ralph.sh
# the same thing, stated explicitly
./ralph.sh --agent claude
# 50 iterations for a longer unattended run
./ralph.sh -n 50
# exactly one iteration, good for a smoke test
./ralph.sh --once

The iteration count is a safety budget, not a target. The loop stops the moment the work is done. The short form -a works too, and --max-iterations 5 is a synonym for -n 5.

Under the hood, Ralph builds a claude command and runs it inside the sandbox. For a first run that creates the box, the expansion is:

Terminal window
sbx run --name ralph-claude-<project>-<hash8> claude . -- --output-format stream-json --verbose -p "$PROMPT_CONTENT"

The -p flag is Claude’s print (headless) mode, which still has full tool access including file writes and shell. The --output-format stream-json --verbose pair is Claude-only, and it is what feeds Ralph’s live preview.

Pass a model and flags after the separator

Section titled “Pass a model and flags after the separator”

Everything to the right of Ralph’s own -- separator is forwarded straight to Claude, inserted between --verbose and -p. So this:

Terminal window
./ralph.sh --agent claude -- --permission-mode bypassPermissions

expands to:

Terminal window
sbx run --name ralph-claude-<project>-<hash8> claude . -- --output-format stream-json --verbose --permission-mode bypassPermissions -p "$PROMPT_CONTENT"

The rule to remember: everything left of -- configures Ralph (agent, iteration count, login), and everything right of -- configures Claude. If you want to pin a model, pass one your Anthropic account supports rather than guessing at an identifier. Do not hardcode a model name you are not sure exists.

YOLO mode is safe because the sandbox is the boundary

Section titled “YOLO mode is safe because the sandbox is the boundary”

For a loop to run unattended it cannot pause for a human to approve each tool call. Claude Code has two ways to skip those prompts: --dangerously-skip-permissions, or the equivalent --permission-mode bypassPermissions. On your laptop that flag is genuinely reckless. Inside a Docker Sandbox it is the intended setup.

The sandbox is an isolated microVM with network denied by default. Claude can edit files and run shell commands freely inside it, but that blast radius never reaches your real machine. When the Docker Sandboxes login flow asks whether to bypass permissions, answering yes is exactly why you are running in a sandbox in the first place.

Every pass is mechanical and identical. Claude reorients from disk, does one task, proves it works, and commits.

  1. Find the highest-priority incomplete task in .agent/tasks.json.
  2. Work the steps in .agent/tasks/TASK-{ID}.json.
  3. Run tests, linting, and type checking.
  4. Complete the task, take a screenshot, update the task status, and commit.
  5. Repeat until all tasks pass or the iteration cap is reached.

The key detail is that each iteration spawns a fresh claude -p with a clean context window. Claude does not carry an hours-long transcript from one task to the next. It reads the current state from disk, does one task, and exits. That is why the loop does not resume a prior Claude session: continuity is a liability here, not a feature.

flowchart TD
    Start(["./ralph.sh -n 50 (Claude default)"]) --> Pick["Pick top task from .agent/tasks.json"]
    Pick --> Spawn["sbx run claude . -- stream-json (fresh context)"]
    Spawn --> Stream["Ralph parses stream-json: Thinking, Implementing, Testing"]
    Stream --> Verify["Run tests, lint, type check, screenshot"]
    Verify --> Commit["Commit and update task status"]
    Commit --> Check{"Promise tag emitted?"}
    Check -->|"none"| Pick
    Check -->|"COMPLETE"| Done(["exit 0, all tasks done"])
    Check -->|"BLOCKED or DECIDE"| Stop(["exit 2 or 3, wants a human"])

The filesystem and git history are the memory layer. Progress lives in .agent/tasks.json, .agent/logs/LOG.md, the per-task specs under .agent/tasks/, and the git log, not in a chat transcript. The prompt that drives every pass lives in .agent/PROMPT.md, alongside .agent/prd/SUMMARY.md, .agent/history/ per-iteration logs, and .agent/STEERING.md.

One rule keeps the whole thing reliable: one task per invocation. Claude completes exactly one task, commits, and stops. It never batches several tasks into a single iteration, which is what keeps each commit small and each diff reviewable.

A loop that never ends is a money fire, so Ralph stops on an explicit signal rather than a guess. Claude emits a semantic promise tag in its final message and Ralph reads it out of the stream:

  • <promise>COMPLETE</promise> means every task is finished.
  • <promise>BLOCKED:reason</promise> means the agent needs human help.
  • <promise>DECIDE:question</promise> means it needs a decision you have to make.

Those map to exit codes, which is what makes the loop scriptable:

Terminal window
./ralph.sh -n 50
echo $? # 0 COMPLETE, 1 MAX_ITERATIONS, 2 BLOCKED, 3 DECIDE

Exit code 1 is not a failure. It means the loop spent its iteration budget with work still pending, which is exactly what a safety cap is for. Because Claude’s output is fully parsed, Ralph can pick these tags and phase changes out of the live stream instead of grepping a final blob, which is the observability edge again.

Three things keep a long Claude run under control.

If you notice Claude heading the wrong way, you do not have to kill the loop. Edit .agent/STEERING.md while it runs. The agent checks that file on each pass and handles the critical work there before resuming the normal task order. That is steering, not stopping, and it keeps momentum while you correct course.

The sandbox is a normal container you can poke at. List what exists, then open a shell in the Claude box:

Terminal window
sbx ls
sbx exec -it ralph-claude-<project>-<hash8> bash

From there you can re-run a failing test by hand, check auth by running claude, or read .agent/logs/LOG.md and the per-iteration logs in .agent/history/. To reattach to an existing box, sbx run ralph-claude-<project>-<hash8> picks it back up without the create-only --name form.

When an install fails, the deny-by-default network is usually the cause. Allow the domain it needs:

Terminal window
sbx policy allow network ralph-claude-<project>-<hash8> registry.npmjs.org

That is a feature, not a bug. Claude installs what the task needs without a path to exfiltrate your source.

Because every task is committed separately, the morning review is a git review, not an archaeology dig:

Terminal window
git log --oneline
git diff main...HEAD

You read the commits in order, eyeball the screenshots the agent captured, and accept or revert. A single bad task is one revert, not a tangled mess. That whole premise is covered in running an AI coding agent overnight.

A real Claude loop is four commands:

Terminal window
# 1. install Ralph and scaffolding
npx @pageai/ralph-loop
# 2. authenticate once (creates the sandbox, you log in inside it)
./ralph.sh --login --agent claude
# 3. smoke test a single pass
./ralph.sh --once
# 4. run the loop for real (Claude is the default agent)
./ralph.sh -n 50

Define your tasks in .agent/tasks.json, write a clear .agent/PROMPT.md, start the loop, and read the commits in the morning. The verification stack it assumes is Playwright for e2e, Vitest for units, TypeScript for types, ESLint for lint, and Prettier for format, so the mantra holds: if you didn’t test it, it doesn’t work.

Frequently asked questions

How do I run a Ralph loop with Claude Code?

Install Ralph with npx @pageai/ralph-loop, create a task list, log in with ./ralph.sh --login --agent claude, then run ./ralph.sh. Claude Code is the default agent, so the bare command already wraps claude in headless print mode inside a Docker sandbox, starts a fresh context each iteration, and repeats until every task in .agent/tasks.json is done or the iteration cap is reached.

Why is Claude Code the default agent in Ralph?

Claude is the only agent whose output Ralph fully parses. Ralph runs Claude with --output-format stream-json --verbose, and it reads that structured stream to detect the current phase (Thinking, Implementing, Testing) and show a live preview. Other agents still loop, commit, and log correctly, but they do not give the same real-time read on what the agent is doing.

What is the difference between Ralph and the official Claude Code loop?

Anthropic ships a plugin that uses a Stop Hook to re-inject the prompt and keep one Claude session going, so continuity is the point. Ralph is a hackable Bash script that spawns a brand new claude process each iteration inside a Docker sandbox with a clean context window, which kills context rot on long runs and lets you edit the wrapper yourself.

How do I pass flags or a model to Claude through Ralph?

Put them after the -- separator. Anything to the right of -- is forwarded to Claude, inserted between --verbose and -p, so ./ralph.sh --agent claude -- --permission-mode bypassPermissions runs Claude in print mode with that flag and the Ralph prompt. Pass a model your Anthropic account supports rather than guessing an identifier.

Is it safe to run Claude with --dangerously-skip-permissions?

It is reckless on your laptop and reasonable inside a sandbox. Ralph runs Claude in an isolated Docker Sandbox microVM with network denied by default, so --dangerously-skip-permissions (or --permission-mode bypassPermissions) lets the agent act without approval prompts while staying unable to touch your real files. The sandbox is the boundary, not the agent.