Unattended long-horizon execution for Claude Code. Relay lets an agent work until it's nearly out of usage budget, checkpoint its own state, sleep until the window resets, and resume itself — until the job is actually done.
The name is the idea: work is handed from one session to the next like a baton, and nothing gets dropped at the exchange.
Agentic coding sessions are long-horizon. Subscription quotas are windowed. Those two facts collide on any project bigger than an afternoon: the agent hits its ceiling mid-task, the session dies, and the only fix is a human sitting at a terminal waiting to restart it — holding the project's state in their head.
Relay closes that gap. It's a supervisor loop that treats "out of budget" as a normal, recoverable state rather than a failure.
Relay spawns Claude Code in bounded chunks via its headless mode (claude -p --resume), with hard caps on turns and spend so no single chunk can run away unattended. Between chunks it decides what to do next based on how the last one ended.
It's a crash-only design: all state lives in files, never in process memory. Kill it at any moment — Ctrl-C, a closed laptop, a reboot — and you lose the current chunk and nothing else.
Claude Code doesn't expose a live "% of quota used" figure to scripts (only to the interactive status line), so Relay uses two layers with deliberately different reliability profiles.
1. Proactive — best effort. A PreToolUse hook shells out to ccusage (a community tool that reads Claude Code's own local session logs), compares burn against a ceiling you calibrate, and returns a JSON deny once you cross threshold. The agent therefore stops between tool calls and writes a clean handoff note, rather than getting severed mid-edit. Writes to PROGRESS.md stay exempt — that exemption is what makes the graceful stop possible at all. Needs Node and a calibration step; no-ops silently without them.
2. Reactive — always on, zero config. The orchestrator pattern-matches Claude Code's own rate-limit message, parses the stated reset time, and sleeps exactly that long. This works even if you skip layer 1 entirely, and it's the guaranteed backstop.
| File | Role |
|---|---|
PROGRESS.md |
A machine-read STATUS: sentinel drives loop termination; the prose below it is the agent's note to its future self — and your at-a-glance dashboard. |
.claude/.relay-session-id |
The resume token. Claude Code's native --resume restores full conversation and file context, so a restart continues rather than starts over. |
.relay.log |
Every chunk's raw JSON, timestamped. What ran, what it cost, and exactly where a run went sideways. |
PROGRESS.md is belt-and-braces on top of --resume — it also survives context compaction on very long runs, and it's readable by you without trawling a transcript.
Requirements: the Claude Code CLI (logged in, claude on your PATH) and jq. Node/npx optionally, for the proactive layer. On macOS also brew install coreutils — reset-time parsing needs GNU date.
git clone https://github.com/<you>/relay.git
cd relay
$EDITOR TASK.md # write the real brief
./scripts/relay.shEverything hinges on TASK.md having a checkable definition of done ("all tests in /tests pass", "npm run lint is clean") rather than a vague goal. Nobody is watching each turn to catch a premature STATUS: DONE, so the agent needs a criterion it can verify against on its own.
./tests/test-relay.shRuns the real orchestrator against a fake claude binary that simulates hitting a rate limit, resuming, tripping the guard, and finishing — plus unit tests for the hook and message matching. No network, no API calls, no tokens spent.
Don't overwrite TASK.md in place — PROGRESS.md, the log, and the session ID are all run state, and reusing a directory invites a new project to resume a stale session. Keep this repo as the template and scaffold a fresh directory per project:
./scripts/new-project.sh ../my-next-project
cd ../my-next-project
$EDITOR TASK.md
./scripts/relay.shThat copies the engine and blank templates — never a log or session ID.
RELAY=/path/to/relay
cp -r "$RELAY/.claude" .
cp "$RELAY/CLAUDE.md" .
cp "$RELAY/PROGRESS.template.md" ./PROGRESS.md
mkdir -p scripts && cp "$RELAY/scripts/"*.sh scripts/
chmod +x scripts/*.sh .claude/hooks/*.shWatch for collisions — if the target repo already has its own CLAUDE.md, back it up first.
tail -f .relay.log # orchestrator decisions + raw chunk output
cat PROGRESS.md # human-readable checkpointOn macOS, wrap the run in caffeinate -dis so the machine doesn't sleep mid-job, and use screen/tmux if you want to close the terminal. Locking the screen is fine; closing the lid usually isn't.
Set as environment variables before running:
| Variable | Default | Meaning |
|---|---|---|
MODEL |
sonnet |
Model alias or pinned ID passed to --model. |
PERMISSION_MODE |
acceptEdits |
Autonomy level — see below. |
ALLOWED_TOOLS |
Bash,Read,Edit,Write,Glob,Grep |
Tools usable without prompting. |
MAX_TURNS_PER_CHUNK |
40 |
Hard turn ceiling per invocation. |
MAX_BUDGET_PER_CHUNK_USD |
8 |
Hard spend ceiling per invocation. |
RELAY_TOKEN_LIMIT |
(unset) | Your calibrated 5-hour token ceiling. Unset disables the proactive guard. |
RELAY_USAGE_THRESHOLD |
95 |
Percent of that limit at which the guard pauses. |
POLL_RETRY_SECS |
600 |
Re-check interval after a guard pause. |
MAX_CONSECUTIVE_ERRORS |
5 |
Strikes before stopping for a human. |
RESET_BUFFER_SECS |
120 |
Cushion added past a reported reset time. |
Example:
RELAY_TOKEN_LIMIT=450000 MODEL=claude-opus-4-8 PERMISSION_MODE=bypassPermissions \
caffeinate -dis ./scripts/relay.shCalibrating the proactive guard: run an ordinary Claude Code session until it tells you you've hit the limit, then immediately run ./scripts/check-usage.sh. The token count it reports is roughly your real ceiling — use that (or a touch under) as RELAY_TOKEN_LIMIT. Skip this and the proactive layer simply stays off.
acceptEdits(default) — edits and shell commands without prompting, but only via the tools inALLOWED_TOOLS.dontAsk— denies anything not explicitly allow-listed. More locked down, more setup.bypassPermissions— full autonomy, no prompts. Equivalent to--dangerously-skip-permissions. Use only in a directory, VM, or container you're comfortable giving an agent unrestricted shell access to.
Whichever you pick: commit and push before starting. An overnight run should be a diff you review in the morning. A local commit protects your working tree; a pushed commit also protects you from anything happening to .git itself.
relay/
├── README.md
├── LICENSE
├── CLAUDE.md # protocol the agent follows: how to checkpoint, how to pause
├── TASK.md # your brief (starts as the template)
├── PROGRESS.md # checkpoint file; STATUS: line drives the loop
├── TASK.template.md # sources for new-project.sh
├── PROGRESS.template.md
├── docs/
│ ├── relay-architecture.png
│ └── relay-architecture.svg
├── .claude/
│ ├── settings.json # registers the hook
│ └── hooks/usage-guard.sh # the proactive guard
├── scripts/
│ ├── relay.sh # the orchestrator you run
│ ├── check-usage.sh # calibration helper
│ └── new-project.sh # scaffold a fresh project
└── tests/
└── test-relay.sh # full control-flow tests, no tokens spent
- The proactive percentage is an estimate, measured against a number you supply — per-plan token ceilings aren't published. Recalibrate if it drifts.
- ccusage is third-party. Its JSON field names have shifted between versions. The hook queries defensively for both known shapes and disables itself if ccusage fails, falling back to the reactive net — it won't block your run either way.
- Reset-time parsing needs GNU
date. If parsing fails, it falls back to a conservative fixed sleep. It errs toward waiting too long rather than waking too early, and never hangs indefinitely. - Rate-limit detection is string matching, so a new message variant can slip past it. This has happened once already (see below); the fix is a one-line regex addition and a test case.
- CLI flags track Claude Code's docs. If something breaks after an update, check the CLI reference and hooks docs.
- Not a substitute for review. Relay makes an agent run longer unattended; it doesn't make its output correct. Read the diff.
Two bugs worth recording, since both are instructive.
stdout and stderr must be captured separately. Merging them lets log noise corrupt the session_id JSON parse — which silently breaks resume, turning "continues where it left off" into "starts over": precisely the failure the tool exists to prevent. Caught by running the loop against a fake claude binary, which is now tests/test-relay.sh.
The error taxonomy was incomplete. The first real production run died after five retries against You've hit your org's monthly usage limit — a message variant the matcher didn't recognise, so it fell through to generic error handling instead of sleeping. The recovery logic was right; the classifier wasn't. Fixed, with a regression test built from the actual logged failure.
MIT — see LICENSE.