Peer Relay — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Peer Relay (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A deliberately dumb message relay that lets two separate Claude Code instances — run by two different developers, on two different machines — exchange direct questions and answers without sharing or merging their context.
It is an MCP server that behaves like a tiny mailbox. The only thing that ever crosses between the two instances is the literal text of a question and the literal text of an answer (plus a couple of small, self-written fields). Nothing else.
Relay, not knowledge base. peer-relay is intentionally dumb. No context syncing. No state mirroring. No background memory. No "let me read your repo for you."
Two developers are working on related things. Dev A's Claude Code needs something only Dev B (or Dev B's codebase) knows — "why does the session token expire after 15 minutes?". Today you'd Slack the human, who asks their Claude, who answers, who pastes it back.
peer-relay lets Dev A's Claude ask Dev B's Claude directly and asynchronously, while keeping the two working contexts completely separate. A's repo, files, and conversation never touch B's, and vice-versa. Only the question and the answer move.
This is the whole point, so it's worth being precise.
The relay stores ONLY these fields:
| Field | What it is |
|---|---|
id | An opaque message id. |
room_token | The shared pairing secret (so one store can host isolated pairs). |
asker_id | A short opaque label of who asked (e.g. alice) — for 1:1 routing. |
question | The literal question text. |
context_hint | Optional short free-text the asker writes themselves. |
answer | The literal answer text (once answered). |
sources | Optional array of short citation strings the answerer writes. |
asked_at / answered_at | ISO-8601 timestamps. |
It never stores, inspects, or fuses:
The guarantee is structural, not a pinky-promise in prose:
src/storage/types.ts names every field that can exist. There is no generic metadata, payload, context, or blob field anywhere.src/storage/sqlite.ts has exactly those columns and nothing else — no JSON catch-all column, no key/value side table. The only multi-value field, sources, is constrained to a flat array of strings.context_hint and sources are written by the humans-in-the-loop Claudes themselves, in their own words. The server does not and cannot auto-collect them.To make the relay store anything richer, you would have to change that interface and that schema — a visible, reviewable diff. That's the design.
The biggest risk in letting one Claude answer another is confident-but-unfounded claims crossing the wire and being treated as fact. peer-relay mitigates this at the prompt layer:
answer_question strongly prompts the answering Claude to ground its answer in concrete sources — file paths, file:line refs, commit hashes, PR numbers, decision docs.check_answers reminds the asker to treat answers as claims from another developer, to prefer verifying cited sources, and to weight "from memory" answers accordingly.Sources are encouraged but not required — sometimes the honest answer is "from memory, not verified."
| Tool | Signature | Purpose |
|---|---|---|
ask_peer | (question: string, context_hint?: string) → { question_id } | Enqueue a question for the peer. context_hint is short orienting text you write. |
check_questions | () → [{ question_id, question, context_hint, asked_at }] | Peer polls for questions addressed to them. |
answer_question | (question_id: string, answer: string, sources?: string[]) → { ok } | Post an answer back, ideally with grounding sources. |
check_answers | () → [{ question_id, question, answer, sources, answered_at }] | Asker polls for answers to their questions. |
The flow is poll-based and asynchronous: A ask_peer → B check_questions → B answer_question → A check_answers.
The whole thing is one container plus a shared secret. One of you hosts it; both of you connect.
git clone https://github.com/jmgomezl/peer-relay.git
cd peer-relay
# pick a shared secret — BOTH devs will use this exact value
echo "PEER_RELAY_ROOM_TOKEN=$(openssl rand -hex 32)" > .env
docker compose up -dThat's it. The relay is now on http://localhost:8787/mcp, with a persistent volume for the mailbox and a built-in health check. cat .env to get the token to share with the other dev.
No Docker? Use Node ≥ 20 instead: npm install && npm run build && PEER_RELAY_ROOM_TOKEN=... PEER_RELAY_HOST=0.0.0.0 npm start.The other dev's Claude Code has to be able to hit that URL. Pick whichever matches your situation:
| Situation | What to use |
|---|---|
| Two laptops, different networks (most common) | Expose it with a tunnel — zero server needed:<br>cloudflared tunnel --url http://localhost:8787 or ngrok http 8787<br>Share the https://… URL it prints. This also gives you HTTPS (see the security note below). |
| Same office LAN / Tailscale / VPN | Use the host's LAN/Tailscale IP: http://<host-ip>:8787/mcp. |
| A shared box / VPS / cloud | Run the same docker compose up -d there, open port 8787 (ideally behind an HTTPS reverse proxy), use its address. |
⚠️ Security: the server speaks plain HTTP and the room token is a bearer secret. Do not send it over the open internet unencrypted — put HTTPS in front (a tunnel does this for you, or use a reverse proxy like Caddy/Traefik). On a trusted LAN/VPN, plain HTTP is fine.
Both devs point at the same URL and same room token, but send a different x-peer-id. See examples/dev-a.mcp.json and examples/dev-b.mcp.json.
Dev A's .mcp.json (in their project root):
{
"mcpServers": {
"peer-relay": {
"type": "http",
"url": "https://your-relay-url/mcp",
"headers": {
"x-room-token": "the-shared-secret-from-step-1",
"x-peer-id": "alice"
}
}
}
}Dev B's is identical except "x-peer-id": "bob". Restart Claude Code (or /mcp to reconnect) and the four peer-relay tools appear.
That's the entire pairing: same room token = same mailbox; distinct peer ids = the two sides.
In Dev A's Claude Code:
Ask my peer why the session token expires after 15 minutes — mention it's about the auth refactor in src/auth/session.ts.Claude calls ask_peer. Later, in Dev B's Claude Code, "check questions from my peer" surfaces it; B answers with sources; A's "check for answers" retrieves it.
Heads-up — polling, no push. The relay does not notify either side. Each dev has to ask their Claude to "check for questions/answers." That's intentional for v1; a question waits in the mailbox until the peer polls.
docker compose logs -f # watch it
docker compose down # stop (mailbox volume is kept)
docker compose down -v # stop and wipe the mailboxThe mailbox persists in the peer-relay-data volume across restarts. See .env.example for all env vars.
x-room-token: <token> or Authorization: Bearer <token>. A wrong/missing token gets 401.x-peer-id) says which side you are. In a room, a question asked by alice is visible to everyone who is not alice — for a strict 1:1 pair, that's exactly bob.context_hint by hand.sources is encouraged precisely because answers can be wrong.Storage sits behind the Storage interface. The default implementation is SQLite (src/storage/sqlite.ts) for zero-setup local dev. To move to Postgres later, implement the same interface — no changes to the tools or transport. The narrow interface is also what keeps the trust model enforceable: there's simply no method to store anything beyond the documented fields.
npm run dev # watch-mode server (tsx)
npm test # end-to-end test: A asks, B answers, A reads it back
npm run typecheck # tsc --noEmitThe e2e test (test/e2e.test.ts) drives a full ask→answer→read cycle through the queue against an in-memory SQLite store, plus room isolation and double-answer rejection.
MIT — see LICENSE.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.