ralph-harness

agent
Security Audit
Fail
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Fail
  • process.env — Environment variable access in hooks/approve-plan.ts
  • process.env — Environment variable access in hooks/steer.ts
  • exec() — Shell command execution in src/cli/main.ts
  • spawnSync — Synchronous process spawning in src/cli/main.ts
  • process.env — Environment variable access in src/cli/main.ts
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Runs Claude Code in a loop for days. Every iteration is a fresh claude -p, and a commit ships only if your own checks pass.

README.md

ralph: a Ralph loop for Claude Code that runs for days. Every iteration is a fresh claude -p, and git, not the model, decides what shipped.

npm version Homebrew: vkuprin/tap/ralph test runs on bun zero runtime dependencies license

Why · Install · Quick start · How it works · Real runs · Commands · Options · Safety

A Ralph loop for Claude Code that runs for days. Every iteration is a fresh
claude -p that reads the job from PROMPT.md and its own notes from
PROGRESS.md, does some work and commits. Then git and your own commands decide
whether that commit ships. What the model says about its work never counts.

Needs bun, git and the claude CLI. macOS, Linux and Windows.
No npm dependencies.

Why not a bash loop

The original Ralph is one line of bash,
while :; do cat PROMPT.md | claude -p; done, and it works. What goes wrong
when it runs for a day, and what ralph does instead:

  • A bad commit lands like a good one. Git, your VERIFY_CMD, FROZEN and a
    reviewer that can't write decide what ships. A commit that fails is reset and
    kept under refs/ralph/<name>/. The agent's account of its own work never
    counts.
  • It works in your checkout and pushes where it likes. Each loop has its own
    worktree and branch, only the harness pushes, and the whole run is one pull
    request, not one per iteration.
  • The notes file grows until it fills the prompt. PROGRESS.md is trimmed to
    its last entries and capped in bytes before it goes in.
  • A usage limit turns it into a tight loop of failed runs. The limit is
    waited out and doesn't count toward MAX_ITER. A rate-limited reviewer is
    retried only up to a ceiling, because the commit it holds is ungated.
  • The laptop sleeps, and a timeout kills a healthy agent on wake. Time the
    machine spent asleep doesn't count against a timeout.
  • Ctrl-C leaves its test servers running. ralph stop stops them too.

Claude Code's /loop and the ralph-loop plugin repeat inside one session, so
the context window fills. Every ralph iteration is a new claude -p process.

How it works

One iteration: read PROMPT.md and PROGRESS.md, run a fresh claude -p that commits in its own worktree, then the harness gates the commit with FROZEN, VERIFY_CMD and a read-only reviewer. A kept commit is pushed to the pull request, a failed one is reset and saved in refs/ralph/. Only files carry over to the next iteration.

  • Every iteration is a new claude -p reading PROMPT.md and PROGRESS.md. It
    commits and rewrites PROGRESS.md; nothing else carries over.
  • The agent never pushes. The harness checks each new commit (FROZEN,
    VERIFY_CMD, reviewer), resets the ones that fail, and pushes the rest.
  • An iteration that ships nothing makes the loop wait longer, not stop, unless
    QUIET_STOP says so.
  • A usage limit is waited out and doesn't count toward MAX_ITER.
  • ralph stop stops the loop right away, together with any tests or servers
    the agent started. When the loop itself was killed outright (kill -9, a
    crash), it stops the agent the loop left running (not on Windows yet).

Real runs

On October 1 and 2 a loop worked on this repository for 17 hours, with the whole
suite (bun run check) as its VERIFY_CMD: 40 iterations, 38 kept, 1 reset when
the suite went red, 1 with nothing left to do. The run is one pull request,
#12. 46 of its 50 commits are
the loop's; the other 4 are mine, fixing Windows CI afterwards.

$ ralph status self
self                 stopped
  repo        ~/Desktop/Coding/ralph-harness
  worktree    ~/Desktop/Coding/ralph-harness-ralph-self (ralph/self)
  iterations  40 run, 38 shipped a commit
  verdicts    38 keep, 1 quiet, 1 revert:verify
  cost        $140.30 API-equivalent (a lower bound), 1772859 tokens, $3.69 per kept commit
  last        [2026-10-02 15:20:39] ralph finished after 40 iterations

An earlier loop on September 21 ran for 18 hours: 24 iterations, 22 commits
kept, 2 reset. VERIFY_CMD reset one that broke the tests. The reviewer reset
the other: it added a rule to AGENTS.md saying every printed hint is now
quoted, while the same unquoted hint was still live in the script. The next
iteration shipped the whole fix
(c423e1e).

Across the eight loops I've run since September 22, on this repository and on
private ones (as of October 6): 106 iterations, 95 commits kept, 7 reset by
VERIFY_CMD, 3 with nothing to do, 1 stopped mid-run and set aside.

The dollar amounts are what claude -p reports each run would have cost at API
prices, the reviewer's run included. On a subscription they are not a bill, and
they are a lower bound: a run killed before it answers reports nothing. Across
those eight loops: $572, about $6 per kept commit.

Install

Homebrew

brew install vkuprin/tap/ralph

npm. ralph runs on
bun, so bun has to be on PATH either way:

bun add -g @vkuprin/ralph-harness        # or: npm i -g @vkuprin/ralph-harness

From source

git clone https://github.com/vkuprin/ralph-harness && cd ralph-harness
ln -s "$PWD/bin/ralph" ~/.local/bin/ralph

Optional: the ralph-new skill in any Claude session (ralph setup doesn't
need it). As a Claude Code plugin, where it's /ralph-harness:ralph-new:

/plugin marketplace add vkuprin/ralph-harness
/plugin install ralph-harness@ralph-harness

Or link it from the install for /ralph-new:

ln -s "$(brew --prefix)/opt/ralph/libexec/skills/ralph-new" ~/.claude/skills/ralph-new                   # Homebrew
ln -s ~/.bun/install/global/node_modules/@vkuprin/ralph-harness/skills/ralph-new ~/.claude/skills/ralph-new # bun
ln -s "$(npm root -g)/@vkuprin/ralph-harness/skills/ralph-new" ~/.claude/skills/ralph-new                 # npm
ln -s "$PWD/skills/ralph-new" ~/.claude/skills/ralph-new                                                  # source

Windows

ralph runs natively on Windows 10 and 11. It needs three things:

  • Git for Windows. It provides git, and
    its Git Bash runs your VERIFY_CMD, HEALTH_CMD, NOTIFY_CMD and the other
    *_CMD settings. Claude Code uses the same bash for its own shell commands.
  • bun: powershell -c "irm bun.sh/install.ps1 | iex".
  • Claude Code's native build (claude.exe), from the
    install page. The claude.cmd that
    npm i -g @anthropic-ai/claude-code installs is a batch file, and the harness
    won't start one: cmd.exe would read the agent's arguments. A loop refuses to
    start if that's the claude on PATH.

Then install from npm and use it from PowerShell, cmd or Git Bash:

bun add -g @vkuprin/ralph-harness        # or: npm i -g @vkuprin/ralph-harness
ralph setup

What works differently on Windows:

  • *_CMD settings run in Git Bash. ralph finds it next to git.exe. To use
    another bash, set RALPH_BASH (or Claude Code's CLAUDE_CODE_GIT_BASH_PATH)
    to its full path. The bash on a Windows PATH is often WSL's, and ralph
    doesn't use that one.
  • Windows has no process groups and no TERM signal. ralph stop asks the loop to
    stop through a ralph.stop file in its directory; the loop then kills the
    agent's whole process tree (taskkill /T), logs where it stopped, and exits.
    A loop that doesn't answer within 15s is killed together with its tree.
  • ralph start takes the loop out of the job object of the shell that ran it,
    so a loop started from an agent's shell tool outlives that shell. When the job
    forbids that, ralph start starts the loop outside it through WMI, with no
    window. ralph start --in-job keeps the loop in the job, so it ends with it.
  • ralph tail follows the log itself, and ralph edit opens notepad when
    EDITOR isn't set.
  • Loop names and repo paths can't contain characters Windows forbids in file
    names (< > : " | ? *).

WSL works too. There ralph is simply the Linux version, and the loop and the
repository live in the Linux filesystem.

To upgrade, run brew upgrade ralph, bun add -g @vkuprin/ralph-harness@latest
or git pull, then restart any running loops (ralph stop <name>, ralph start <name>).
A running loop keeps the files it started with, and Homebrew deletes the old
version's files. What changed is in CHANGELOG.md and on the
releases page.

Quick start

cd ~/code/my-app
ralph setup                  # Claude asks how the loop should run, then creates it

Or by hand:

ralph new audit ~/code/my-app --set 'VERIFY_CMD=npm test'
ralph edit audit             # write the job into PROMPT.md
ralph start audit
ralph status

Commands

Command What it does
ralph short guide and your loops
ralph setup open Claude Code here with the setup skill; it asks questions and scaffolds the loop
ralph new same as ralph setup
ralph new <name> <repo> [--set KEY=VALUE]... scaffold a loop, with settings written into its config.json
ralph edit <name> open PROMPT.md in $EDITOR
ralph start <name> run it in the background
ralph stop <name> stop the loop and everything the agent started
ralph status [name] running or not (and anything a killed loop left running), iterations, verdicts, HEAD
ralph review <name> [n] what it shipped, what it threw away, what waits to merge
ralph results <name> [n] last n verdicts as a table
ralph log <name> [n] last n log lines
ralph tail <name> follow the log
ralph steer <name> "text" redirect it, starting with the iteration in flight if one is running
ralph steer --hard <name> "text" add a pass/fail test every result must meet (see below)
ralph migrate <name> convert an old bash-harness config.sh to config.json
ralph --version the installed version

A loop lives in ~/.claude/ralph/<name>/ ($RALPH_HOME): config.json,
PROMPT.md (the job), PROGRESS.md (its memory), ralph.log, results.tsv.

Hard constraints. Steering ranks; ## Hard constraints in PROMPT.md
filters. Each item is a numbered pass/fail test ("2. Never recommends X, which
was tried and rejected"). The reviewer is handed them verbatim and rejects a
commit whose result recommends, ranks or lists anything that fails one, near
misses and fallbacks included, and "nothing passes" counts as a complete answer.
Such a reject is recorded as revert:constraint. The agent cannot loosen them:
an iteration that edits the section gets it put back, and the log says so.
Change the list between iterations, or with ralph steer --hard, which also
holds when it lands mid-iteration.

Options

Set them in the loop's config.json (JSON with comments), or when creating it with
ralph new … --set KEY=VALUE. Read once at start: restart to apply. An unknown key
or a wrong type refuses the start. The defaults below are what ralph new writes; a
key left out of the file entirely is off for WORKTREE, PUSH, PR_DRAFT, REVIEW,
LIMIT_RESET and CHURN_AT.

Loop
Setting Default What it does
REPO your repo the checkout to work on, absolute path. Required
MODEL "opus" model for the agent and the reviewer
PLAN_FIRST false start each iteration in Claude's plan mode; the harness approves the plan and the same run carries it out
MAX_ITER 500 hard ceiling on iterations; the loop ends right after the last one, with no pause and no wait for ACTIVE_HOURS
QUIET_STOP 0 stop after this many iterations in a row ship nothing; 0 never
QUIET_SLEEP 1200 seconds to wait after an iteration that shipped nothing
STEP_SLEEP 30 seconds between iterations that shipped
ITER_TIMEOUT 7200 seconds one agent run may take
DONE_CMD "" your "job is done" check, before every iteration; exit 0 stops the loop. Not asked while HEALTH_CMD or CI fails
DONE_TIMEOUT 300 seconds DONE_CMD may take
ACTIVE_HOURS "" local hours iterations may start in, like "22-08"; empty is any time
ADD_DIRS [] extra directories the agent may read
DENY [] tool patterns the agent may never use, like "Bash(ssh *)"
LIVE_STEER true let ralph steer reach the iteration in flight
ESCALATE_AFTER 3 failures in a row before the prompt says pivot
CLOSING names PROGRESS.md last line of every prompt
Gates
Setting Default What it does
WORKTREE true work in a harness-owned worktree on ralph/<name>; every gate needs it
WORKTREE_DIR next to the repo where the worktree goes
SETUP_CMD "" run once in a new worktree, like npm ci
VERIFY_CMD "" your check after every commit; failing resets the commit
VERIFY_TIMEOUT 1800 seconds VERIFY_CMD may take
FROZEN [] paths a commit may not touch, relative to the top of the repository, case and all
REVIEW true a read-only Claude reviewer judges each commit
REVIEW_MODEL MODEL the reviewer's model
REVIEW_LIMIT_TRIES 12 times a rate-limited reviewer is retried; 0 forever
HEALTH_CMD "" your check of the running system, before every iteration; while it fails, fixing it leads the prompt
HEALTH_TIMEOUT 300 seconds HEALTH_CMD may take
CHURN_AT 4 flag files changed by this many of the last CHURN_WINDOW kept iterations; 0 off
CHURN_WINDOW 8 kept iterations CHURN_AT counts over
CHURN_IGNORE [] paths left out of that count
Push and pull requests
Setting Default What it does
BRANCH "main" the base branch
PUSH "pr" "pr" pushes ralph/<name> and keeps one pull request open; true pushes kept commits straight to BRANCH; false stays local
PUSH_CONFIRM "" required with PUSH true: the name of BRANCH again. Whatever deploys BRANCH deploys every kept commit, so the loop refuses to start without it
PR_DRAFT true with "pr", open the pull request as a draft and mark it ready when the loop ends by itself; with one loop per stage, nobody merges a stage half done
PR_MERGE false with "pr", merge the pull request when the loop ends by itself and every check passes
PR_MERGE_METHOD "merge" "merge", "squash" or "rebase"
PR_MERGE_WAIT 3600 seconds to wait for checks still running
PR_MERGE_POLL 30 seconds between two looks at the checks
CI_FEEDBACK true with "pr", read the checks on the pushed head before every iteration; a failure (check, step, log tail) leads the next prompt and keeps DONE_CMD from being asked
PR_FIX_ITERS 5 with PR_MERGE, iterations the loop may run after its checks fail at the end, to fix them; 0 blocks the merge at once
NEXT_LOOP "" with PR_MERGE, a loop to start once the pull request merges, with this loop's ## Carry forward section copied into its PROGRESS.md. {n+1} in it counts from this loop's name (polish-{n+1} after polish-3 is polish-4), and a next loop that is not there yet is made then, from this loop's config.json and PROMPT.md, so the chain goes on
NEXT_FROM "" the loop a next loop that is not there yet is made from, instead of this one
LAND_OK_CMD "" your check that BRANCH may move now, like "no data load running in production"; while it fails, a push (true) or a merge (PR_MERGE) waits, asking every ACTIVE_POLL seconds
LAND_OK_TIMEOUT 300 seconds LAND_OK_CMD may take
Limits and errors
Setting Default What it does
RATE_LIMIT_SLEEP 1800 wait before retrying after a limit
LIMIT_RESET true wait until the reset time the limit message names instead
RATE_LIMIT_EXTRA_RE more text that counts as a limit (regex, case-insensitive)
RATE_LIMIT_RE built in replaces the built-in limit pattern entirely
ERROR_SLEEP 300 wait after a failure, doubling up to an hour
ERROR_STOP 0 stop after this many failures in a row; 0 never
ACTIVE_POLL 300 seconds between clock checks while waiting
POLL_GAP_MAX 60 a longer gap between polls is a suspend and doesn't count against a timeout
Notifications
Setting Default What it does
NOTIFY_CMD "" shell command run on an event; the event is in RALPH_EVENT, RALPH_LOOP, RALPH_DIR, RALPH_ITER, RALPH_MESSAGE
NOTIFY_TIMEOUT 30 seconds it may take; its exit status is ignored. At the timeout everything it started is killed with it, nohup and & included, so start a next stage with NEXT_LOOP, not from the notifier

Events: stopped, died, refused, stuck, limit, limit-clear, decision, health,
health-clear, churn, pr, pr-blocked, pr-ready, land-held, ci-failed, merged,
merge-blocked, next, next-failed. died is a signal ralph stop did not send (a reboot, a
closed session); a loop killed outright says nothing.
template/config.json has a macOS notification and a Telegram example.

Memory and logs
Setting Default What it does
PROGRESS_KEEP 8 Log entries kept in PROGRESS.md; older ones go to PROGRESS-archive.md; 0 keeps all
PROGRESS_MAX_BYTES 120000 most of PROGRESS.md put into one prompt; 0 all
LOG_MAX_BYTES 10000000 rotate ralph.log past this size; 0 never
LOG_KEEP 3 rotated logs kept
REF_KEEP 20 thrown-away commits kept under refs/ralph/<name>/, of each kind; 0 keeps all

Safety

  • The agent runs with --dangerously-skip-permissions. Run it where it can't
    reach production credentials, or in a container or VM.
  • With WORKTREE on it never touches your own checkout, and only the harness
    pushes. DENY prevents accidents; it won't stop an agent determined to get
    round it.
  • Anything the agent reads (HEALTH_CMD output, web pages) can carry
    instructions. Say in PROMPT.md what it must never write to.

Tests

bun run check                                  # typecheck, then every test
RALPH_REAL_CLAUDE=1 bun test tests/contract    # against the real claude CLI, a few cents

On Windows, run the suite from Git Bash: the tests use sh, sleep and the
other tools it puts on PATH. The stand-ins for claude and gh are compiled
to .exe on the fly, and no other claude on PATH is ever reached.

A change users would notice comes with a changeset (bunx changeset). Merging
the release PR it produces publishes the new version.

Credits

Geoffrey Huntley for the technique;
karpathy/autoresearch,
Continuous Claude,
ralph-tui,
tcc-autoresearch and
Anthropic's Effective harnesses for long-running agents
for ideas; anthropics/cwc-long-running-agents
for the steering hook, which hooks/steer.ts is adapted from (Apache-2.0).

License

MIT, except hooks/steer.ts (Apache-2.0). See LICENSE and
NOTICE.

ralph logo

Reviews (0)

No results found