Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

> A **Claude Code plugin** + an **MCP server** for Codex. With Bun, Claude Code, and Codex installed, one `npm install` wires two agents into one shared, auditable ledger.

![ContextRelay running Claude Code and Codex as one pair, with a live shared ledger](docs/assets/contextrelay-pair.gif)

*Scripted demo using real `ctxrelay` output format — paths, IDs, and timestamps are sanitized.*

ContextRelay wires Claude Code and Codex into one repository through a loopback
daemon, so they exchange live messages, hand off work, and debate decisions —
with every message, handoff, note, and decision written to a shared, auditable
Expand Down Expand Up @@ -82,6 +86,10 @@ The terminal-native management tab sits alongside those dashboard views:
A typical session: Claude implements, hands the risky part to Codex for an
independent review, and every move lands in the shared ledger.

![A Claude-to-Codex review handoff recorded in the ContextRelay ledger](docs/assets/contextrelay-handoff.gif)

*Scripted from the real `ctxrelay ledger` format; the scenario is representative.*

```text
Claude → /contextrelay:handoff
reason: finished the token-refresh change; want a second opinion before merge
Expand Down
63 changes: 63 additions & 0 deletions docs/assets/contextrelay-handoff.cast
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{"version":2,"width":90,"height":26,"timestamp":1718000000,"title":"ContextRelay — a representative Claude → Codex handoff","env":{"SHELL":"/bin/zsh","TERM":"xterm-256color"}}
[0,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "]
[1.278,"o","\u001b[2m# Claude finished a token-refresh change — wants a second pair of eyes.\u001b[0m"]
[1.778,"o","\r\n"]
[2.878,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "]
[2.924,"o","c"]
[2.962,"o","t"]
[3.017,"o","x"]
[3.08,"o","r"]
[3.118,"o","e"]
[3.162,"o","l"]
[3.198,"o","a"]
[3.247,"o","y"]
[3.295,"o"," "]
[3.346,"o","l"]
[3.396,"o","e"]
[3.44,"o","d"]
[3.502,"o","g"]
[3.548,"o","e"]
[3.586,"o","r"]
[3.628,"o"," "]
[3.671,"o","l"]
[3.731,"o","i"]
[3.772,"o","s"]
[3.827,"o","t"]
[4.077,"o","\r\n"]
[4.527,"o","2026-06-18T10:22:14.512Z \u001b[33mhandoff\u001b[0m claude->codex 7c1a9e02\r\n review src/auth/refresh.ts for races + token-leak paths before merge\r\n2026-06-18T10:23:48.901Z \u001b[36mmessage\u001b[0m codex->claude b2e6f4a1\r\n My independent view: refresh() is unguarded — two concurrent 401s double-refresh\r\n2026-06-18T10:24:31.770Z \u001b[36mmessage\u001b[0m codex->claude c4f0a7d5\r\n ...and the old token lingers in memory after rotation. Neither path is tested.\r\n"]
[6.527,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "]
[6.591,"o","c"]
[6.649,"o","t"]
[6.708,"o","x"]
[6.77,"o","r"]
[6.832,"o","e"]
[6.883,"o","l"]
[6.925,"o","a"]
[6.97,"o","y"]
[7.034,"o"," "]
[7.089,"o","l"]
[7.149,"o","e"]
[7.207,"o","d"]
[7.256,"o","g"]
[7.296,"o","e"]
[7.353,"o","r"]
[7.39,"o"," "]
[7.435,"o","s"]
[7.495,"o","h"]
[7.559,"o","o"]
[7.619,"o","w"]
[7.656,"o"," "]
[7.703,"o","7"]
[7.746,"o","c"]
[7.8,"o","1"]
[7.836,"o","a"]
[7.9,"o","9"]
[7.945,"o","e"]
[7.985,"o","0"]
[8.038,"o","2"]
[8.288,"o","\r\n"]
[8.738,"o","7c1a9e02 (handoff)\r\nsession: session_8f3c2d1b\r\ntime: 2026-06-18T10:22:14.512Z\r\nsource: claude -> codex\r\n\r\nreason: finished the token-refresh change; want a second opinion before merge\r\nask: review src/auth/refresh.ts for races and token-leak paths\r\nfiles: src/auth/refresh.ts, src/auth/refresh.test.ts\r\n"]
[10.738,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "]
[11.908,"o","\u001b[2m# Independent cross-vendor review — durable, replayable evidence.\u001b[0m"]
[12.408,"o","\r\n"]
[16.208,"o",""]
Binary file added docs/assets/contextrelay-handoff.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions docs/assets/contextrelay-pair.cast
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{"version":2,"width":90,"height":26,"timestamp":1718000000,"title":"ContextRelay — two agents, one ledger","env":{"SHELL":"/bin/zsh","TERM":"xterm-256color"}}
[0,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "]
[0.882,"o","\u001b[2m# Run Claude Code and Codex as one auditable pair\u001b[0m"]
[1.382,"o","\r\n"]
[2.382,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "]
[2.428,"o","c"]
[2.466,"o","t"]
[2.521,"o","x"]
[2.584,"o","r"]
[2.622,"o","e"]
[2.666,"o","l"]
[2.702,"o","a"]
[2.751,"o","y"]
[2.799,"o"," "]
[2.85,"o","-"]
[2.9,"o","-"]
[2.944,"o","v"]
[3.006,"o","e"]
[3.052,"o","r"]
[3.09,"o","s"]
[3.132,"o","i"]
[3.175,"o","o"]
[3.235,"o","n"]
[3.485,"o","\r\n"]
[3.935,"o","contextrelay v3.4.0\r\n"]
[5.035,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "]
[5.076,"o","c"]
[5.131,"o","t"]
[5.195,"o","x"]
[5.253,"o","r"]
[5.312,"o","e"]
[5.374,"o","l"]
[5.436,"o","a"]
[5.487,"o","y"]
[5.529,"o"," "]
[5.574,"o","s"]
[5.638,"o","t"]
[5.693,"o","a"]
[5.753,"o","t"]
[5.811,"o","u"]
[5.86,"o","s"]
[6.11,"o","\r\n"]
[6.56,"o","{\r\n \"instanceId\": \"ctx_9a2f10\",\r\n \"bridgeReady\": true,\r\n \"claudeState\": \"idle\",\r\n \"codexState\": \"idle\",\r\n \"claudeAttachmentStatus\": \"live_attached\",\r\n \"claudeConnected\": true,\r\n \"tuiConnected\": true,\r\n \"deliveryMode\": \"push\",\r\n \"queuedMessageCount\": 0,\r\n \"ledgerEntries\": 12,\r\n \"latestActiveHandoff\": null\r\n}\r\n"]
[8.36,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "]
[9.602,"o","\u001b[2m# Both agents attached — every message + handoff lands in the ledger.\u001b[0m"]
[10.102,"o","\r\n"]
[13.702,"o",""]
Binary file added docs/assets/contextrelay-pair.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions scripts/gen-demo-casts.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Asciicast v2 generator for ContextRelay demos.
// Commands and the output *format* mirror real `ctxrelay` output, but values are
// trimmed and sanitized (representative paths, IDs, and timestamps) — these are
// scripted demos, not a live capture. Deterministic: same input -> identical
// .cast (seeded jitter), so the committed casts regenerate byte-for-byte.
import { writeFileSync } from "node:fs";

// Seeded PRNG (mulberry32) so typing jitter is reproducible — not Math.random().
let _seed = 0x9e3779b9;
function rng() {
_seed = (_seed + 0x6d2b79f5) | 0;
let t = Math.imul(_seed ^ (_seed >>> 15), 1 | _seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}

const GREEN = "\x1b[1;32m";
const DIM = "\x1b[2m";
const CYAN = "\x1b[36m";
const YEL = "\x1b[33m";
const RST = "\x1b[0m";
const PROMPT = `${GREEN}~/code/acme-api${RST} ${CYAN}$${RST} `;

function build(steps, { width = 90, height = 26, title }) {
_seed = 0x9e3779b9; // reset per cast so each regenerates identically
let t = 0.0;
const ev = [];
const out = (s) => ev.push([round(t), "o", s]);
const wait = (s) => { t += s; };
const nl = "\r\n";

const type = (cmd) => {
out(PROMPT);
for (const ch of cmd) { wait(0.035 + rng() * 0.03); out(ch); }
wait(0.25);
out(nl);
};
const comment = (text) => {
out(PROMPT);
const s = `${DIM}# ${text}${RST}`;
for (const ch of `# ${text}`) { wait(0.018); }

Check failure

Code scanning / CodeQL

Unused loop iteration variable Error

For loop variable ch is not used in the loop body.

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable ch.
out(s);
wait(0.5);
out(nl);
};
const print = (block) => { wait(0.45); out(block.replace(/\n/g, nl) + nl); };

const round = (x) => Math.round(x * 1000) / 1000;

for (const step of steps) {
if (step.comment) { comment(step.comment); wait(step.pause ?? 0.9); }
else if (step.cmd) {
type(step.cmd);
if (step.out != null) print(step.out);
wait(step.pause ?? 1.4);
} else if (step.blank) { out(nl); wait(step.pause ?? 0.4); }
}
// tail hold so a looped GIF rests on the final frame
wait(2.2); out("");

const header = { version: 2, width, height, timestamp: 1718000000, title,
env: { SHELL: "/bin/zsh", TERM: "xterm-256color" } };
return [JSON.stringify(header), ...ev.map((e) => JSON.stringify(e))].join("\n") + "\n";
}

// ---- Cast 1: the pair is live, recording to one ledger -------------------
const pair = build([
{ comment: "Run Claude Code and Codex as one auditable pair", pause: 1.0 },
{ cmd: "ctxrelay --version", out: "contextrelay v3.4.0", pause: 1.1 },
{ cmd: "ctxrelay status", out:
`{
"instanceId": "ctx_9a2f10",
"bridgeReady": true,
"claudeState": "idle",
"codexState": "idle",
"claudeAttachmentStatus": "live_attached",
"claudeConnected": true,
"tuiConnected": true,
"deliveryMode": "push",
"queuedMessageCount": 0,
"ledgerEntries": 12,
"latestActiveHandoff": null
}`, pause: 1.8 },
{ comment: "Both agents attached — every message + handoff lands in the ledger.", pause: 1.4 },
], { title: "ContextRelay — two agents, one ledger" });

// ---- Cast 2: a representative handoff, in real ctxrelay ledger format -----
const handoff = build([
{ comment: "Claude finished a token-refresh change — wants a second pair of eyes.", pause: 1.1 },
{ cmd: "ctxrelay ledger list", out:
`2026-06-18T10:22:14.512Z ${YEL}handoff${RST} claude->codex 7c1a9e02
review src/auth/refresh.ts for races + token-leak paths before merge
2026-06-18T10:23:48.901Z ${CYAN}message${RST} codex->claude b2e6f4a1
My independent view: refresh() is unguarded — two concurrent 401s double-refresh
2026-06-18T10:24:31.770Z ${CYAN}message${RST} codex->claude c4f0a7d5
...and the old token lingers in memory after rotation. Neither path is tested.`, pause: 2.0 },
{ cmd: "ctxrelay ledger show 7c1a9e02", out:
`7c1a9e02 (handoff)
session: session_8f3c2d1b
time: 2026-06-18T10:22:14.512Z
source: claude -> codex

reason: finished the token-refresh change; want a second opinion before merge
ask: review src/auth/refresh.ts for races and token-leak paths
files: src/auth/refresh.ts, src/auth/refresh.test.ts`, pause: 2.0 },
{ comment: "Independent cross-vendor review — durable, replayable evidence.", pause: 1.6 },
], { title: "ContextRelay — a representative Claude → Codex handoff" });

const dir = process.argv[2] || ".";
writeFileSync(`${dir}/contextrelay-pair.cast`, pair);
writeFileSync(`${dir}/contextrelay-handoff.cast`, handoff);
console.log("wrote contextrelay-pair.cast and contextrelay-handoff.cast to", dir);
10 changes: 10 additions & 0 deletions website/docs/introduction/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ ContextRelay is a local, provider-neutral coordination control-plane. It wires C

Reach for ContextRelay when **one agent should implement while the other reviews**, when **a risky decision needs a second opinion**, or when you want an **auditable record of what the agents did before you ship**.

![ContextRelay running Claude Code and Codex as one pair, with a live shared ledger](/img/contextrelay-pair.gif)

*Scripted demo using real `ctxrelay` output format — paths, IDs, and timestamps are sanitized.*

:::info Current version
These docs describe **ContextRelay 3.4.0**, the current release on [npm](https://www.npmjs.com/package/@proofofwork-agency/contextrelay). After upgrading the package, run [`ctxrelay upgrade`](../getting-started/upgrading-contextrelay.md) to reconcile your local setup.
:::
Expand Down Expand Up @@ -49,6 +53,12 @@ ctxrelay claude # start Claude Code wired into the daemon
ctxrelay codex # start the Codex TUI wired into the daemon
```

Once they're paired, a handoff looks like this — Claude asks Codex for an independent review, and every message lands in the shared ledger:

![A Claude-to-Codex review handoff recorded in the ContextRelay ledger](/img/contextrelay-handoff.gif)

*Scripted from the real `ctxrelay ledger` format; the scenario is representative.*

:::tip Three names, one CLI
`contextrelay`, `ctxrelay`, and `context-relay` are the **same binary** - use whichever you prefer. This site uses `ctxrelay` for brevity.
:::
Expand Down
Binary file added website/static/img/contextrelay-handoff.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/static/img/contextrelay-pair.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading