Every failure this product has had leaves a trace somewhere:
config.json, status.json, the hook entries in your
agent's settings, the watcher's pid lock, the offline outbox. The doctor reads
all of them, checks the known traps by name, and prints one line per
subsystem.
If something is not working, run this before anything else. It is faster than reading a troubleshooting page, and if you end up emailing us, its output is the one thing that usually turns three exchanges into one.
Running it#
$ runi doctor
$ runi doctor --offline # skip the live workspace probe
It is installed beside the daemon as ~/.relay/relay-doctor. Run it
from inside the repository you care about, because one of its checks is about
the current directory.
It exits 1 if anything failed and 0 otherwise, so CI
and scripts can gate on it. Everything except the final probe is a local read.
--offline makes it entirely local.
Reading the output#
| Badge | Means |
|---|---|
| ok | Checked, and correct. |
| warn | Degraded, or explaining an expected silence. Does not affect the exit code. |
| FAIL | Broken. The line says how to fix it, and the exit code is 1. |
The checks, in the order they print#
config#
Whether ~/.relay/config.json exists and carries the four fields that make a daemon: the URL, the anon key, a refresh token and a workspace id.
- FAIL missing or unreadable. This machine has never been connected.
- FAIL missing <fields>. A partial install. Reconnect from the app.
- ok names the workspace and the profile, so you can confirm you are looking at the right one.
auth#
Whether a cached access token has life left in it.
A warn here is not a problem. It means the next hook will refresh: one extra round trip, then cached again. Tokens rotate on every use and are re-persisted, so a fresh hook with time left on the cache performs zero auth round trips on your agent's critical path.
hooks#
Four checks, in increasing order of subtlety:
- Are there any Runi entries? warn if not.
- Do the scripts they name exist? FAIL if not. This is the deleted-wrapper trap.
- Are the critical events there?
SessionStartandUserPromptSubmitare FAIL; the rest are warn. - Has each registered event ever fired? warn naming any that never has.
The last one exists because registered and running are different states. A hook can be written correctly into a settings file the CLI does not read, and that is invisible from the settings alone.
publish#
The last publication attempt, from status.json.
| Error | Means | Do |
|---|---|---|
auth-expired | The refresh token no longer works. | Sign in again from the app. |
access-denied | Read-only in the workspace, or removed from it. | Ask an owner or admin for member access. |
offline-queued | No network. Writes are queued locally. | Nothing. They replay when the network returns. |
warn authenticated but nothing published yet is the healthy state of a machine that has been connected and has not yet run a session in a tracked repository.
briefs#
Whether model-written task briefs are landing. When they are unavailable, cards keep their local brief, written from your prompt on your machine, and the doctor says why. Otherwise you are left wondering whether titles were always this blunt.
context#
Prints only when the last briefing was partial, naming the collections that were unreachable. A healthy fetch is silent. The packet told the agent the same thing, so this is that fact repeated for you.
receipts#
Whether delivery receipts are being filed. Without them, sessions cannot show whose briefing they reached. A workspace on an older schema answers the receipt call with a 404; the daemon notes that and stops retrying for an hour instead of treating it as a refused publication.
generation#
The gotcha that costs the most hours after the deleted wrapper: a fix on disk that no running process is executing. Hooks re-read on every run. MCP servers do not. This line names any server still on the previous daemon, with its pid and its agent, so you know which sessions to restart.
ignore#
Prints when Git could not answer an ignore check. Paths Git cannot vouch for are withheld, so nothing unsafe is published. Everything else keeps publishing normally, which is why this is a warning and not a failure.
A path outside the work tree is the usual trigger. If you see it constantly,
check that git is on the PATH your agent runs with.
adapter:<name>#
One line per adapter that has been seen, with its status, the format version it is reading, and when it was last seen. An unfamiliar format degrades visibly here instead of breaking your agent session.
codex#
The rollout watcher: whether the process is alive, checked against its pid lock and not merely against a status field, how many live sessions it sees, and when it last checked. Without it, Codex Desktop and VS Code sessions are not observed at all.
openclaw#
Silent unless OpenClaw is installed. When it is, it resolves each agent's workspace and says whether that workspace is a Git repository and whether the repository is tracked. The check exists because OpenClaw's out-of-the-box silence needs explaining, not debugging. See OpenClaw.
outbox#
Pending writes, their age, how many belong to a previous login and will never send, and any dead letters. Dead letters are a FAIL because they need a decision from you. See Queued and rejected writes.
this repo#
What the current directory resolves to, and whether the workspace tracks it. The answer comes from the config list and the offline cache, which is the same answer the network would give without the round trip. The doctor has to work offline.
sessions#
How many recent sessions this machine holds locally, and how many are live.
online#
The one network call: a single row read against the workspace, with an eight
second timeout. It proves reachable and authorised, which is a stronger
claim than a ping. Skipped by --offline.
Dead-letter commands#
$ runi doctor --retry-dead <id>
$ runi doctor --discard-dead <id>
$ runi doctor --retry-dead all
$ runi doctor --discard-dead all --before 2026-09-01
$ runi doctor --retry-dead all --since 2026-09-05
all in place of an id takes every dead letter.
--since and --before take an ISO date and narrow
all by when the write was refused. Both bounds exist for real
situations: hundreds of rows refused while an account was a viewer should not
be retried one command at a time, and a week of stale sessions should not be
replayed into the workspace just because the newest few should be.
A healthy run#
Runi doctor
ok config workspace 8f2c… as Ravjeet (~/.relay/config.json)
ok auth cached access token valid for another 52 min
ok hooks all 6 Claude Code events registered and their scripts exist
ok publish last event published 3 min ago
ok receipts briefing receipts filed, last 3 min ago
ok adapter:claude ok (v3), last seen 3 min ago
ok outbox empty — nothing waiting to replay
ok this repo relay is tracked — sessions here publish to the workspace
ok sessions 12 recent on this machine, 1 live
ok online workspace reachable and authorized (1 task row visible)
Nothing broken. If the app still looks empty, work in a tracked repo and
give it one prompt.