Skip to content
RALPH LOOP

How to Run a Ralph Loop With opencode

A green CRT terminal showing the open-source opencode CLI running in a looping Ralph loop inside a Docker sandbox

To run a Ralph loop opencode setup, install Ralph into your project, log in to opencode once inside its sandbox, then run ./ralph.sh --agent opencode. Ralph drives the open-source opencode CLI through its run subcommand, 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.

The reason this pairing is worth its own page: opencode is the open-source, provider-agnostic agent in the lineup, and Ralph is a hackable open Bash script. An open CLI on top of an open loop means you own both ends. You pick the model. You pick the provider. Nothing about the loop is locked to a vendor. This post is the end-to-end recipe, from an empty repo to a reviewable diff.

If the loop itself is new to you, start with what the Ralph technique is. If you want to compare terminal agents before you commit, the field guide is agentic coding CLIs.

opencode is a terminal-first coding agent with no editor attached. You give it a prompt, it reads files, edits them, runs shell commands, prints a result, and exits. That clean exit is the hook Ralph needs. Each iteration becomes a discrete unit of work: spawn opencode, let it do one task, read what it emitted, then spawn it again.

Two properties make opencode a natural fit for a hackable loop.

  • It is open source. You can read exactly what the agent does, fork it, and audit its behavior. That matches the spirit of ralph.sh, which is a script you are meant to open and edit, not a black box.
  • It is provider-agnostic. opencode does not force one model vendor on you. You configure the model and provider opencode supports, and the loop runs identically regardless of which one you chose.

Ralph then supplies the three things a single opencode run cannot give you on its own:

  • A loop, so the agent gets another pass after every commit instead of stopping.
  • Fresh context per iteration, so a long run never rots into a confused, drifting session.
  • A Docker sandbox boundary, so the agent can edit files and run commands in bypass-permissions mode without that blast radius reaching your real machine.

The state lives on disk (the task list, the logs, the git history), not in a chat transcript. That is what lets a stateless opencode invocation reorient on every pass and still make forward progress across dozens of iterations. The Ralph technique itself was popularized by Geoffrey Huntley, and his original writeup on Ralph as a Bash loop is the shortest path to the core idea.

Run the installer in your project root. It drops 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, then writes a default implementation-mode setup. Pick opencode when prompted, or set it per run with --agent opencode later. 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 Sandbox first. See the Docker Sandboxes docs for the official setup flow. The loop drives the standalone sbx CLI under the hood.

The loop executes a task list. It does not invent one. Before you run anything, turn your requirements into a PRD and a set of task specs with the prd-creator skill.

Open any agent in plan mode and prompt it with your requirements:

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 the loop succeeds.

Step 3: Log in to opencode inside the sandbox

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

opencode runs inside an isolated sandbox, not on your host, so it needs credentials in that environment. Authenticate once with the login action:

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

Ralph prints the login command for every supported agent, highlights the opencode 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-opencode-<project>-<hash8> opencode .

Inside the sandbox, authenticate opencode once. Run opencode auth login and follow the prompt to connect your model provider. That is the exact command Ralph points you to if it detects an auth failure at the start of a run, so it never thrashes on a box that can never make progress. The credential persists in that named sandbox, so later runs attach to the same box already logged in.

If your provider prefers a key over an interactive login, Docker Sandboxes forward environment variables at the moment a sandbox is created. Export the provider key opencode expects in your shell, then create the sandbox with ./ralph.sh --login --agent opencode so it picks the value up. Configure the model and provider opencode supports through its own config; Ralph does not hardcode any provider identifiers, which is the whole point of the bring-your-own-provider design.

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-opencode-<project>-<hash8>

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

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

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

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

That expands to an sbx ports call against the opencode sandbox, for example sbx ports ralph-opencode-<project>-<hash8> --publish 3000:3000. Per-agent names keep state separate: your opencode sandbox and your Claude sandbox never share credentials or installed tools, so you can run both against the same repo without one clobbering the other.

With a task list in place and opencode authenticated, start the loop. The default agent is Claude, so name opencode explicitly:

Terminal window
# 10 iterations (the default)
./ralph.sh --agent opencode
# 50 iterations for a longer unattended run
./ralph.sh --agent opencode -n 50
# exactly one iteration, good for a smoke test
./ralph.sh --agent opencode --once

The short form -a works too: ./ralph.sh -a opencode -n 5. There is also ./ralph.sh --agent opencode --max-iterations 5 if you prefer the long flag. The iteration count is a safety budget, not a target. The loop stops the moment the work is done.

Under the hood, Ralph builds an opencode command and runs it inside the sandbox. This is the detail that sets opencode apart from the other agents: Ralph drives it through the run subcommand, not a -p print flag. For ./ralph.sh --agent opencode, the expansion is:

Terminal window
sbx run --name ralph-opencode-<project>-<hash8> opencode . -- run "$PROMPT_CONTENT"

The run subcommand is opencode’s non-interactive path: it takes a prompt, does the work with full tool access including file writes and shell, and exits. That is exactly what a loop needs, an agent that can do real work without a person approving each step.

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 opencode, inserted right after run. So this:

Terminal window
./ralph.sh --agent opencode -- <EXTRA>

expands to:

Terminal window
sbx run --name ralph-opencode-<project>-<hash8> opencode . -- run <EXTRA> "$PROMPT_CONTENT"

Use that slot to select the model and provider opencode supports. Because opencode is provider-agnostic, the exact identifiers depend on which provider you configured, so check opencode’s own help for the flags it accepts rather than guessing. The rule to remember: everything left of -- configures Ralph (agent, iteration count, login), and everything right of -- configures opencode.

Every pass is mechanical and identical. The agent 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 opencode run with a clean context window. The agent 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. Continuity is a liability here, not a feature: a clean context on every pass is what avoids the context rot that sinks long single-session runs.

flowchart TD
    Start(["./ralph.sh --agent opencode -n 50"]) --> Pick["Pick top task from .agent/tasks.json"]
    Pick --> Spawn["sbx run opencode . -- run (fresh context)"]
    Spawn --> Work["opencode reads state, edits files, runs commands"]
    Work --> 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/, the per-iteration logs in .agent/history/, 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, which gives opencode a short project overview on each iteration.

One rule keeps the whole thing reliable: one task per invocation. opencode 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. opencode emits a semantic promise tag in its final message and Ralph reads it:

  • <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 --agent opencode -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. If opencode is not authenticated when the loop starts, Ralph detects the auth failure, stops, and tells you to run opencode auth login inside the sandbox (via ./ralph.sh --login --agent opencode), so a broken box never burns iterations.

Three things keep a long opencode run under control.

If you notice the agent 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 opencode box:

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

From there you can re-run a failing test by hand, confirm auth by running opencode directly, or read .agent/logs/LOG.md and the per-iteration logs in .agent/history/. 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-opencode-<project>-<hash8> registry.npmjs.org

That is a feature, not a bug. The agent installs what the task needs without a path to exfiltrate your source. To reattach to a running box later, sbx run ralph-opencode-<project>-<hash8> drops you back in without recreating it.

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. This is the whole premise of running an AI coding agent overnight.

Ralph is agent agnostic. The supported agents are claude (default), codex, copilot, cursor, gemini, and opencode, and switching is a single flag. opencode earns its place when you want an open-source agent and the freedom to route through whatever model provider you already use. The loop machinery is identical across agents, so the choice comes down to which CLI holds up over a multi-hour run.

The verification stack the loop assumes is the same regardless of agent: Playwright for end-to-end tests, Vitest for unit tests, TypeScript for types, ESLint for linting, and Prettier for formatting. The repo mantra is blunt about it: if you didn’t test it, it doesn’t work. That comparison across CLIs lives in the best agentic CLI for long-running tasks. If you want to strip the loop down to its essentials, read the Ralph loop shell script, and if you would rather drive a proprietary CLI, the companion recipe is running a Ralph loop with the Cursor CLI.

A real opencode loop, start to finish, 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 opencode
# 3. smoke test a single pass
./ralph.sh --agent opencode --once
# 4. run the loop for real
./ralph.sh --agent opencode -n 50

Define your tasks in .agent/tasks.json, write a clear .agent/PROMPT.md, configure the model and provider opencode supports, start the loop, and read the commits in the morning.

Frequently asked questions

How do I run a Ralph loop with opencode?

Install Ralph with npx @pageai/ralph-loop, create a task list, log in with ./ralph.sh --login --agent opencode, then run ./ralph.sh --agent opencode. Ralph drives the open-source opencode CLI through its run subcommand 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.

Does ralph.sh support opencode?

Yes. opencode is one of the supported agents, alongside claude, codex, copilot, cursor, and gemini. Select it with --agent opencode or the short form -a opencode. Ralph builds an opencode run command and executes it inside a deterministic sandbox named ralph-opencode-<project>-<hash8>.

How does Ralph invoke opencode under the hood?

Ralph uses opencode run rather than a print flag. For ./ralph.sh --agent opencode the expansion is sbx run --name ralph-opencode-<project>-<hash8> opencode . -- run "$PROMPT_CONTENT". Anything you pass after Ralph's own -- separator is inserted right after run, so you can select the model and provider opencode supports.

How do I log in to opencode for the loop?

Run ./ralph.sh --login --agent opencode. It drops you into the sandbox shell where you run opencode auth login and connect your model provider. The credential persists in that named sandbox, so future runs attach to the same box already authenticated. You can also forward a provider key as an environment variable at sandbox creation time.

Can I choose my own model and provider with opencode?

Yes. opencode is provider-agnostic, so you configure the model and provider it supports through its own config or by passing flags after Ralph's -- separator. Ralph does not hardcode any provider identifiers, which keeps the loop portable across whatever backend you already pay for.