Run your test suite on dedicated machines instead of your laptop, split across as many hosts as you have. rr syncs your working tree to one or more remote machines over rsync and SSH, runs a command there, and streams the output back.
> /plugin marketplace add rileyhilliard/rr> /plugin install rr@rr
Repo: rileyhilliard/rr
What's inside
rr syncs your working tree to one or more remote machines over rsync and SSH, runs a command there, and streams the output back. It picks whichever host is free, locks it so two runs don't collide, and can split one test suite into shards that run on several hosts at once.
rr run "make test" # sync, run on the first free host, stream output back
rr test # a named task; parallel tasks fan out across every host
rr test-api -- tests/auth -x # forward args to scope a task to one path
It's a single Go binary with a YAML config. Each remote needs SSH access and rsync. There's no agent or daemon to install on it.
Coding agents that work test-first run the test suite constantly: after every change, before every commit, and once more to confirm. On a real project each of those runs takes minutes and pins every core on the machine. Run two agents in separate worktrees, or let one agent fan out subagents, and the runs start fighting over CPU, ports, and test databases on the same laptop. Tests fail because runs interfere with each other, and the machine you're working on becomes unusable while they do.
rr moves those runs onto machines whose only job is running tests. Your laptop edits code and the runners run it. A few spare mini PCs or an old workstation in the closet is enough.
Splitting the suite is where the time goes down. Break a suite into shards (by package, by directory, backend vs frontend) and rr runs each shard on a different host at the same time. The speedup is close to linear: two hosts finish in roughly half the time, three in roughly a third. The floor is your slowest shard, so it pays to keep shards similar in size.
OpenData is the heaviest user of rr today. Its suite has around 14,000 tests across a Python data pipeline, a FastAPI backend, and TypeScript frontend and MCP packages, and one command runs all of it:
# Simplified from OpenData's .rr.yaml
local_fallback: never # a run that silently lands on the laptop must never count as a pass
tasks:
test:
description: Run all tests in parallel
setup: (cd opendata && uv sync --quiet) & (cd backend && uv sync --quiet) & bun install --frozen-lockfile & wait
parallel:
- test-opendata
- test-backend-api
- test-backend-services
- test-backend-infra
- test-frontend-fast
- test-mcp-fast
fail_fast: false
test-opendata:
description: Run OpenData tests (extra args forward to pytest)
run: cd opendata && uv run pytest {args} -n 4 --no-cov -q --tb=short
Agents working on OpenData iterate with scoped runs like rr test-opendata -- tests/test_services/test_foo.py -x, which run only the tests touching their change, and treat the full rr test as the gate before a commit. None of it runs on the laptop.
I built rr for two reasons. The first was my laptop: the fan spun up and the battery drained every time I ran tests, while a few much faster machines sat idle in the corner.
The bigger one was multi-agent coding. When several agents work on the same project at once, each in its own worktree, each one runs the test suite whenever it wants to check its work. They don't know about each other. Run locally, their suites start at the same moment on the same machine, compete for CPU, and collide on shared ports, test databases, and caches, so tests fail for reasons that have nothing to do with the code. rr puts those runs in a queue. Each run takes a lock on a free runner, and the next one goes to another host or waits its turn, so every agent gets a clean run of its suite and none of them step on each other.
| Tool | Where it falls short for this |
|---|---|
rsync && ssh | Works for one host. No locking, no failover, no splitting across hosts |
| CI (GitHub Actions) | Needs a push per iteration and queues for minutes. Too slow for an inner loop |
| Ansible | Inventory files and playbooks to run one command |
| DevPod / Tilt | Container-first. More than you need to sync and run |
| VS Code Remote | Tied to the IDE. Doesn't help a CLI agent |
Most of rr's defaults assume the caller is an agent or a script, and a person watching is the less common case.
Output is structured by default. rr writes JSON phase events (connect, sync, exec) to stderr and ends with a single result event. Your command's stdout and stderr pass through untouched, so test output reads the same as it would locally. Add --pretty (or -p) when a human is watching and you want spinners and colors.
$ rr run "go test ./..."
{"type":"phase","phase":"connect","status":"complete","host":"m4-mini","duration_s":0.21,...}
{"type":"phase","phase":"sync","status":"complete","host":"m4-mini","duration_s":0.84,...}
ok github.com/you/project/internal/api 3.112s
{"type":"result","status":"success","host":"m4-mini","duration_s":4.9,"exit_code":0,"details":{...}}
The other defaults each solve a problem that came up with real agents:
| Behavior | Why it matters for an agent |
|---|---|
| Error codes | Failures carry a code (LOCK_HELD, SSH_TIMEOUT, DEPENDENCY_MISSING, HOST_NOT_FOUND, CONFIG_NOT_FOUND, ...) and a suggested fix, so the agent can branch on the code without parsing prose. Codes are set where the error happens, not guessed from the message. |
| Parsed test results | For pytest, Jest, and Go test, the result carries pass/fail counts (details.summary) and each failing test with its file:line and message (details.failures), so the agent doesn't scroll back through the output. |
| Zero-test detection | A run that collected no tests keeps its exit code but sets details.no_tests, so a bad path filter doesn't look like a passing suite. |
| Loud local fallback | If local_fallback is on and rr ends up running locally because hosts were unreachable or all locked, it emits a warning event and sets details.fallback; for locked hosts it lists who holds each lock. A deliberate local run (--local, or local mode) isn't a fallback, and its connect event says so in details.reason. Set local_fallback: false if a local run should never count. |
| Path rewriting and hints | Absolute local paths in a command (/Users/you/project/tests/...) are rewritten to the remote copy. A failure that looks like a local-path mistake gets a hint explaining the mapping. |
| Scoped runs | Extra arguments forward into the task (rr test-api -- tests/auth -x), or into an {args} placeholder, so an agent can run one file without a new task definition. |
| Worktree isolation | Each git worktree syncs to its own remote directory, so parallel agents on separate branches never overwrite each other's tree. rr prune removes directories for deleted worktrees. |
| Locking and queueing | One run per host at a time. Extra runs move to the next free host or wait in line rather than competing for the same CPU and ports. |
If you use Claude Code, the rr plugin teaches Claude how to set up and use rr:
/plugin marketplace add https://github.com/rileyhilliard/rr
/plugin install rr@rr
Then run /rr:setup from your project root. It writes the configs, checks SSH connectivity, runs a test command remotely, and makes sure the tools your project needs exist on each host. See docs/claude-code.md for details.
# 1. Install
brew install rileyhilliard/tap/rr # or see Install below
# 2. Set up in your project
cd your-project
rr init # creates .rr.yaml
# 3. Run something (--pretty for human-readable output)
rr --pretty run "make test"
Homebrew (macOS/Linux)
brew install rileyhilliard/tap/rr
Install script
curl -sSL https://raw.githubusercontent.com/rileyhilliard/rr/main/scripts/install.sh | bash
Go install
go install github.com/rileyhilliard/rr/cmd/rr@latest
Manual download
Grab the binary for your platform from releases. rr runs on macOS, Linux, and Windows under WSL.
You need ssh and rsync on your machine, and passwordless SSH access to each remote. If ssh user@yourhost logs in without asking for a password, you're set. If it doesn't, follow the SSH setup guide, or run rr setup <host> to configure keys and test the connection.
rr init # creates .rr.yaml with interactive prompts
rr doctor # checks SSH, rsync, config, and each host
If rr init doesn't pick up your SSH config, you can add hosts by hand. See the configuration docs.
rr run "make test" # sync files, then run the command
rr exec "git status" # run without syncing (faster for quick checks)
rr sync # sync only
rr pull coverage.xml # copy a file back from the remote
Define named tasks in .rr.yaml:
tasks:
test:
run: pytest -n auto
build:
run: make build
Then run them by name:
rr test # same as: rr run "pytest -n auto"
rr test tests/test_api.py # extra args are appended: pytest -n auto tests/test_api.py
rr test -- -k login -x # use -- before flags so rr doesn't parse them itself
rr tasks # list available tasks
If the arguments need to go somewhere other than the end of the command, put {args} where they belong: run: pytest {args} --tb=short.
FAQ
rr is a Claude Code plugin with 1 hand-picked skill for development work, indexed on Flowy. Install it with the command on its page. It includes rr. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.
Is this plugin yours?
Claim it with GitHubSubmit a pluginPromote it