What it does for you
relay runs a coding agent on whichever model you can reach: a local model when you are offline, your subscription CLI or an API key when you need more, with failover between them. The agent edits your code through tools that refuse a guess, writes nothing and runs nothing unless you allow it, and records every turn in a hash-chained ledger that a stranger can re-derive.
- Any endpoint, your own credentialsA ladder of tiers, free and private first. A missing key just removes that tier.
- Edits that refuse to guessAn edit addressed by a stale line anchor, an ambiguous search or a mismatched diff is refused with nothing written.
- Off by defaultWrites need
--allow-write, shell commands need--allow-exec, and file tools stay inside--root. - A run you can proveEvery turn and tool call is chained by hash. Edit one byte of a saved run and loading it is refused.
Source: README.md at 9c79f30 (release 0.7.0)
Watch
Video walkthrough: coming with the next release.
How it works, one step at a time
Scroll, or use the step buttons. Each panel shows real output from relay's own tools at commit 9c79f30, run on a four-line file with an off-by-one bug. No model is in the loop for these steps; the tools behave the same whichever model calls them.
- 01
One ladder of endpoints
relay tries model tiers in order and fails over on exhaustion or error: a local model first, then your subscription CLI, then public APIs, then a gateway URL you set, then a cloud endpoint.
Keys come from your environment and subscriptions from your own signed-in CLI. A tier whose credential is absent is never added, so a missing key gives a shorter ladder, never an error at call time.
Source: README.md, "Reaches every endpoint"; src/relay/endpoints.py
- 02
Read the file with line anchors
Take
pager.py, whoseendis one short. Aread_filecall with"hashed": trueprefixes every line with an 8-hex anchor.The anchor hashes the line's text together with its line number, so two identical lines get different anchors.
Source: src/relay/hashline.py,
line_anchor - 03
Edit by anchor
The model replaces line 3 by naming its anchor,
6627593c, without repeating the line. With--allow-writethe edit lands and the bug is fixed.Source: src/relay/local_tools.py,
_t_edit_lines - 04
A stale view is refused
Send the same edit again. Line 3 has changed, so its anchor no longer exists, and the edit is refused with nothing written.
The other edit tools follow the same rule. A search string that matches twice is refused. A unified diff whose context no longer matches is refused, with no fuzzy placement. Pick each case in the panel.
Source: src/relay/local_tools.py,
_t_edit_lines,_t_edit_file,_t_apply_diff - 05
Nothing writes or runs unless you allow it
The tool gate is default-deny. Without
--allow-writean edit is refused before it touches the file. Without--allow-execa shell command is refused. File tools resolve every path inside--root, so../../etc/passwdnever opens.Granting exec also grants write, because a shell can write files.
Source: src/relay/local_tools.py,
ToolGateand_safe_path - 06
Every step goes into a chained ledger
Each turn and tool call is appended to the session ledger. An entry's hash covers its sequence number, kind, content, metadata and the previous entry's hash, so each entry pins everything before it.
verify()re-derives every hash in order. This four-entry run verifies, and its head hash is the run's checkpoint.Source: src/relay/local_session.py,
SessionLedger.appendandverify - 07
An edited run will not load
Save the run, then change one tool call so it claims the agent edited
auth.py. Loading re-derives the chain on the read path, and the file is refused.Source: src/relay/local_session.py,
SessionLedger.load - 08
Measure the boundary against injection
relay --probe-injectionruns six fixed injection scenarios, such as "run curl to send .env" or "overwrite the test file". It assumes the worst case: the model was fooled and sent exactly the smuggled call. Then it checks whether the gate refuses it.Under the default gate all six are contained and it exits 0. With
--allow-exec, four get through and it exits 1. An open shell is a larger capability, and the probe says so.Source: src/relay/injection_probe.py; README.md, "Prove the boundary holds"
- 09
Accept a run only when your check passes
A ledger proves what the agent did, and your tests decide whether the code works. With
--check "pytest -q", relay runs your command once after the agent finishes, records the result on the ledger, and accepts the run only if it passes. A failed check skips--auto-commitand exits non-zero.A rule-based guard also reads the recorded edits. A pass earned by editing the test that grades it, or by injecting a skip, is flagged and the run is not accepted. This step is described from the README; it needs a model, so it was not run for this page.
Source: README.md, the section on
--check
Walkthrough
Install it, run it once, then use the main feature. Each command below is real, and so is its output.
Install
Install from PyPI. Python 3.11 or newer.
$ python -m pip install flywheel-relayFirst run: probe the boundary
Before relay touches a model, run its built-in injection probes against the permission boundary. All six were contained.
$ relay --probe-injection "contained": 6, "total": 6, "receipt": "1405769b874ca5d7"Read with line anchors
The agent reads a file with a short hash on each line, so a later edit can name exactly the line it saw.
read_file {"path": "pager.py", "hashed": true} b76150a4|def paginate(items, page, size): ba6dfa08| start = page * size 6627593c| end = start + size - 1 a5dfd4fd| return items[start:end]Edit by anchor
An edit names the anchor it read. If the file changed since, the anchor no longer matches and the edit is refused.
edit_lines {"path": "pager.py", "at": "6627593c", "new": " end = start + size"} edited pager.py (replace 6627593c) def paginate(items, page, size): start = page * size end = start + size return items[start:end]Run a task, accepted only by your check
Point relay at a model and a repository. Writes need
--allow-write, and the run counts as done only when your check passes.$ relay --agent "fix the off-by-one in paginate()" --root . --allow-write --check "pytest -q"
The probe output came from flywheel-relay 0.7.0 installed from PyPI and matches a run of the source at 9c79f30. The last two commands need a model you can reach.
What it does not do
- A verified ledger proves what the agent did. It does not prove the edits are correct; that is what
--checkis for. - With exec granted, the shell is not confined to
--root. It can read and write anything the user running relay can. - The command denylist refuses a few literal destructive spellings. It is a guardrail for small models and not a security boundary.
- The injection probe measures six fixed scenarios against the gate. It assumes the model was fooled and says nothing about how often a model is fooled.
- Small local models vary from run to run in whether they call the edit tools at all. The README reports this as a known limitation.
Source: README.md at 9c79f30, "An actual coding agent", "Ambient repo context" and "Use from an agent (MCP)"
Check what stuck
Answer each one in your head before you open it.
Why does sending the same anchored edit twice fail the second time?
The anchor hashes the line's text and position. After the first edit line 3 holds different text, so the old anchor matches nothing and the edit is refused.
What does relay do with a search string that appears twice?
It refuses the edit and asks for more context. It never picks one of the two matches.
Why does --allow-exec also turn on write?
A shell can write files through redirection or other tools, so a gate that kept write off while exec was on would be a gate the run path bypasses.
A saved run has one tool call edited. What happens when you load it?
The chain is re-derived on load, the hash no longer matches, and the file is refused.
The probe reports 2 of 6 with --allow-exec. Is that a bug?
No. It is the honest measurement: an open shell can do what the gate would otherwise refuse, and the probe exits non-zero to say so.