Skip to content
RALPH LOOP

How to Run a Ralph Loop With the GitHub Copilot CLI

A green CRT terminal showing the GitHub Copilot CLI running in a looping Ralph loop inside a Docker sandbox

A Ralph loop with Copilot runs the GitHub Copilot CLI on your task list in a fresh context every iteration, and the one detail that makes it different from every other agent is auth: Copilot signs in through the GitHub CLI (gh auth login), not an agent-specific login. Install Ralph into your repo, authenticate with gh inside the sandbox, then run ./ralph.sh --agent copilot. Ralph re-runs Copilot against .agent/tasks.json until the work is done or you hit the iteration cap.

This is the full 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 a wider look at terminal agents before you commit, the field guide is agentic coding CLIs. This post is the canonical Ralph plus Copilot page: everything below is the end-to-end setup.

Copilot is the GitHub-native agent. Its CLI runs from the terminal, reads a prompt, edits files, runs shell commands, prints a final message, and exits. That clean exit is the hook a loop needs. Each iteration is a discrete unit of work: spawn the agent, let it do one task, read what it emitted, repeat.

The reason to pick Copilot over another CLI is almost never the model. It is the account. If your team already standardized on GitHub, your identity, your seats, your billing, and your permissions already live there. Copilot authenticates through gh, so the loop rides on the same GitHub login your engineers use every day. There is no separate vendor account to provision, no extra key to rotate, no new invoice to reconcile. For a shop that runs everything through GitHub, that is one less system to babysit.

Ralph supplies the three things a single Copilot CLI call cannot:

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

State lives on disk (the task list, the logs, the git history), not in a chat transcript. That is what lets a stateless Copilot invocation reorient on every pass and still make forward progress across dozens of iterations. The original argument for why a plain Bash loop beats a clever agent runtime is in Geoffrey Huntley’s writeup on Ralph.

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 copilot when prompted, or set it per run with --agent copilot 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 Sandboxes 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 Copilot (or 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. Vague tasks produce vague diffs no matter which agent runs them.

Step 3: Authenticate Copilot through the GitHub CLI

Section titled “Step 3: Authenticate Copilot through the GitHub CLI”

This is the step that sets Copilot apart. The agent runs inside an isolated sandbox, not on your host, so it needs credentials in that environment. Kick off the login action:

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

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

Inside the sandbox, authenticate with the GitHub CLI. Copilot does not have its own login flow. It leans on gh:

Terminal window
gh auth login

Follow the device-code prompt, pick GitHub.com, and pick HTTPS. Once gh reports you as logged in, Copilot inside that sandbox can act as you. This is not incidental. Ralph’s own auth-failure detection for Copilot watches for the string gh auth login in the agent output, so if the loop starts against an unauthenticated box, it stops and tells you to run exactly that. It never thrashes on a sandbox that can never make progress.

The credential persists in that named sandbox, so later runs attach to the same box already authenticated. That persistence is the whole point of a deterministic sandbox name. Each agent gets its own name, built from the agent slug, the project directory, and an 8-character hash of the absolute path:

ralph-copilot-<project>-<hash8>

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

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

If interactive login through gh ever refuses to stick (Docker Sandboxes persist OAuth tokens on a best-effort basis), there is a token-based fallback. Docker Sandboxes forward environment variables from your shell into the box at create time, not after. So you export the credential in your host shell, delete the stale sandbox, and let Ralph recreate it so the new box picks the value up. Keep the specifics to whatever GitHub token your setup expects and do not paste secrets into the repo. The mechanics of that fallback are the same for every agent; only the credential differs.

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

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

That expands to an sbx ports call against the Copilot sandbox, for example sbx ports ralph-copilot-<project>-<hash8> --publish 3000:3000. Per-agent names keep state separate: your Copilot 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 Copilot authenticated, start the loop. The default agent is Claude, so name Copilot explicitly:

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

The short form -a works too: ./ralph.sh -a copilot -n 5. The --max-iterations 5 long form is equivalent to -n 5. Whatever number you pick is a safety budget, not a target. The loop stops the moment the work is done.

Under the hood, Ralph builds a Copilot CLI command and runs it inside the sandbox. For ./ralph.sh --agent copilot, the expansion is:

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

The -p flag hands Copilot the Ralph prompt for this iteration. That prompt is the whole contract: it tells the agent to read the current state from disk, pick one task, do it, prove it works, and commit.

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 to Copilot. For Copilot specifically, Ralph inserts those extra args right before -p, not after it. So this:

Terminal window
./ralph.sh --agent copilot -- --model <your-model>

expands to:

Terminal window
sbx run --name ralph-copilot-<project>-<hash8> copilot . -- --model <your-model> -p "$PROMPT_CONTENT"

Notice the ordering: --model <your-model> lands before -p "$PROMPT_CONTENT". Do not guess at model identifiers. Pass one your Copilot plan actually supports, and check the Copilot CLI’s own help inside the sandbox if you are unsure which strings it accepts. The rule to remember: everything left of -- configures Ralph (agent, iteration count, login), and everything right of -- configures Copilot.

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 Copilot process 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, does one task, and exits. Continuity is a liability in a long run, not a feature, so the loop deliberately starts cold every time.

flowchart TD
    Start(["./ralph.sh --agent copilot -n 50"]) --> Pick["Pick top task from .agent/tasks.json"]
    Pick --> Spawn["sbx run copilot . -- -p (fresh context)"]
    Spawn --> Work["Copilot 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, and the git log, not in a chat transcript. The prompt that drives every pass lives in .agent/PROMPT.md, and its default mode is implementation. You can swap it for a refactor, review, or test mode by editing that one file.

One rule keeps the whole thing reliable: one task per invocation. Copilot 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. Copilot 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 copilot -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. Exit codes 2 and 3 mean Copilot wants a human: either a blocker it cannot clear or a decision only you can make. Wiring those into a CI job or a wrapper script lets you fire off an overnight run and get paged only when the agent genuinely needs you.

Three things keep a long Copilot 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 Copilot box:

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

From there you can re-run a failing test by hand, confirm your GitHub session with gh auth status, 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-copilot-<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. Reattaching later is safe too: run sbx run ralph-copilot-<project>-<hash8> (no agent slug, no path) to drop back into the same box.

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. The loop machinery is identical across all of them: same task list, same promise tags, same exit codes, same sandbox boundary. The only things that change per agent are the exact CLI invocation and the login flow.

Copilot is the strong pick when your organization already runs on GitHub and you want the agent to inherit that identity instead of standing up a new one. The GitHub-native auth is the differentiator: gh auth login is a command your engineers already know, your GitHub seats already cover the access, and there is no separate account to manage. If you are choosing between backends on the basis of which CLI holds up over a multi-hour run, the head-to-head is in the best agentic CLI for long-running tasks.

Prefer a different backend? The same end-to-end recipe applies. The setup guide for running a Ralph loop with the Cursor CLI covers the Cursor path, and if you want to understand the ralph.sh script itself before you trust it with your repo, read the Ralph loop shell script.

A real Copilot 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 run gh auth login inside it)
./ralph.sh --login --agent copilot
# 3. smoke test a single pass
./ralph.sh --agent copilot --once
# 4. run the loop for real, optionally with a model your plan supports
./ralph.sh --agent copilot -n 50 -- --model <your-model>

Define your tasks in .agent/tasks.json, write a clear .agent/PROMPT.md, sign in with gh, start the loop, and read the commits in the morning.

Frequently asked questions

How do I run a Ralph loop with Copilot?

Install Ralph with npx @pageai/ralph-loop, create a task list, then authenticate with ./ralph.sh --login --agent copilot and run gh auth login inside the sandbox shell. Start the loop with ./ralph.sh --agent copilot. Ralph runs the GitHub Copilot CLI 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.

How does Copilot authenticate inside the sandbox?

Through the GitHub CLI. Copilot has no agent-specific login, so you run gh auth login inside the sandbox and Copilot acts as your GitHub identity. Ralph creates the box with ./ralph.sh --login --agent copilot and drops you into a shell to sign in. The credential persists in the named sandbox, so future runs attach to the same authenticated box.

Does ralph.sh support the GitHub Copilot CLI?

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

How do I pass a model to Copilot through Ralph?

Put it after the -- separator. Anything to the right of -- is forwarded to Copilot, and for Copilot those args are inserted right before -p. So ./ralph.sh --agent copilot -- --model your-model expands to sbx run ... copilot . -- --model your-model -p with the Ralph prompt. Pass a model your Copilot plan supports rather than guessing at an identifier.

Why choose Copilot over another agent for the loop?

Pick Copilot when your team already standardized on GitHub. Auth rides on the same gh login your engineers use, your GitHub seats cover access, and there is no separate vendor account or key to manage. The loop mechanics are identical across agents, so the choice comes down to which account and CLI fit your setup best.