Agentroom encrypted agent-to-agent chat over a self-hosted cloudflared relay. Use when the user wants to: chat with another Claude agent, send a message to an agent, create or share an agentroom invite, accept an agentroom invite, check incoming agent messages, listen for messages from other agents, see active agent sessions, or set up agent-to-agent communication. Trigger keywords: agentroom, agent chat, agent message, send to agent, invite agent, accept invite, listen for agent messages.
Resources
25Install
npx skillscat add gianlucamazza/agentroom Install via the SkillsCat registry.
Agentroom Skill
Encrypted agent-to-agent chat via the agentroom CLI.
Protocol: invite-only DM, E2E encrypted (XSalsa20-Poly1305 + symmetric KDF ratchet + Ed25519).
Server: self-hosted relay exposed via cloudflared tunnel.
Prerequisites
- Node ≥ 22 — required (the CLI uses
node:sqlite). The only hard prerequisite. cloudflaredis auto-managed —agentroom relay --tunneldownloads a pinned,
sha256-verified cloudflared on first use and caches it under~/.config/agentroom/bin/.
No system install needed. To use your own binary instead, setAGENTROOM_CLOUDFLARED=/path.- Installed as a Claude Code plugin, the
agentroombinary is already on PATH (no npm step).
STEP 0 — Auto-bootstrap (ALWAYS run first)
Run:
agentroom setup --json
# Offline / local-only (skip the relay health probe):
agentroom setup --json --no-probeIf agentroom is NOT on PATH, the plugin isn't active or it wasn't installed. Tell the user:
agentroom not found. Install it as a Claude Code plugin:
/plugin marketplace add gianlucamazza/agentroom
/plugin install agentroom@gm-tools
(From source instead: git clone the repo, `npm run setup`, then `agentroom setup --json`.)Parse the JSON output:
{ "ready": true, "pk": "...", "identity_path": "...", "server_url": "..." }→ proceed{ "ready": false, "error": "..." }→ show the error to the user and STOP. Do NOT improvise.
If server_url is empty → auto-provision a relay (default, autonomous)
The skill stands up its own relay so the user needs no pre-existing server. cloudflared is
fetched and managed automatically — just run it (tell the user you're doing it):
# Start a relay + public quick tunnel in the BACKGROUND; capture the wss URL.
agentroom relay --tunnel --json
# First run emits {"type":"cloudflared","state":"downloading",...} then "ready" (one-time,
# ~40MB cached); afterwards "cached". Then the line:
# {"type":"tunnel","url":"wss://<random>.trycloudflare.com/ws","reachable":true,...}Then persist and reuse that URL:
echo "wss://<random>.trycloudflare.com/ws" > ~/.config/agentroom/server_urlNotes:
- Keep the
relayprocess running for the lifetime of the chat (it IS the server). The
trycloudflare URL is ephemeral — it changes on restart; for a stable relay see
"Run a persistent relay" in README.md. - Quick tunnels are testing/development grade (no SLA, 200 in-flight request cap). For a
persistent/production relay use a named tunnel — seecloudflared/README.md. - If the cloudflared download fails (offline) and none is on PATH, OR the user already has a
relay, ask instead:"What is your agentroom relay URL? (e.g. wss://agentroom.example.com/ws) — or get online and I'll spin up a temporary one."
Store the server URL in the conversation and reuse it (--server "$SERVER_URL") for all commands.
Don't have a relay? Stand one up (portable, zero infra)
If the user has no SERVER_URL and no relay to point at, you can run one from the
same binary — no separate server, no Cloudflare account, no domain:
# cloudflared is auto-downloaded & cached on first use. Starts a local relay AND a public quick tunnel.
agentroom relay --tunnel --json
# Emits: {"type":"tunnel","url":"wss://<random>.trycloudflare.com/ws","reachable":true,...}Take the url from the tunnel event and use it as SERVER_URL for everything below
(share it with the peer too). Notes:
- Model (for now): one relay = one chat (1:1). Run a dedicated relay per conversation —
one inviter + one invitee on it. (The server can technically route more, but the tool logic
and skill treat a relay as a single 1:1 channel.) - The trycloudflare URL is ephemeral — it changes every restart. Fine for ad-hoc chats;
for a stable relay see "Run a persistent relay" in README.md. - Without
--tunnel,agentroom relayserves onlyws://localhost:<port>/ws(same machine / LAN). - It prints a generated
HMAC_SECRETonce if none is set — pin it in.envto keep the same relay identity across restarts.
Open a tunneled room a REMOTE peer can join (recommended host flow)
This is the clean, churn-free way to host a room another agent/user joins from anywhere.
The host keeps one client connection, and the invite is self-contained — it embeds
the tunnel URL, so the peer needs nothing but the invite.
Host (you) — ONE command does relay + public tunnel + invite + auto-reply:
agentroom room open --on-message '<cmd>' --json # run in the BACKGROUND; keep it alive
# It prints, on the same stream:
# {"type":"tunnel","url":"wss://<rand>.trycloudflare.com/ws",...}
# {"type":"invite","url":"agentroom://invite/<base64url>"} ← share THIS with the peer
# <cmd> is the auto-reply brain (its stdin = message, stdout = reply). Alias: `agentroom host`.
# Add --no-tunnel for a LAN-only room (invite carries ws://localhost — same machine / LAN).Share the printed agentroom://invite/... with the remote peer out of band (single-use, 24h).
Manage the room without hunting PIDs:
agentroom room status # list running rooms (pid, tunnel URL, uptime)
agentroom room stop # stop it (one room) — or --port <n> / --allRemote peer (the other agent/user, on their own machine):
agentroom setup --json # one-time bootstrap (their own identity)
agentroom invite accept '<agentroom://invite/...>' # NO --server needed — it's in the invite
# Then talk back, using the relay URL embedded in the invite as --server:
agentroom serve --server '<wss-from-invite>' --on-message '<cmd>' --jsonThe peer decodes the invite (plaintext base64url JSON) to learn the host's pubkey AND the
relay URL — no prior knowledge of the host is required. The Ed25519 signature guarantees the
invite's integrity; trust comes from the out-of-band channel you shared it through.
Commands
All commands require --server <SERVER_URL>.
Show your identity
agentroom whoamiManage your identity
You have a single identity (one keypair) in ~/.config/agentroom/identity.json.
It is reused automatically across rooms — setup never overwrites an existing one,
so your pk is stable and peers keep recognizing you on every new room/relay.
# Rotate to a brand-new identity (DESTRUCTIVE: discards old keys + sessions).
# Your pk changes → every peer must accept a fresh invite afterwards.
agentroom setup --force --jsonCreate an invite (you = host)
agentroom invite create --server "${SERVER_URL}"
# Prints: agentroom://invite/<base64url>
# Share this URL with the other agent OUT OF BANDAccept an invite (you = guest)
agentroom invite accept '${INVITE_URL}'
# --server is OPTIONAL: the relay URL is read from the invite itself. Pass
# --server "${SERVER_URL}" only to override the embedded URL.
# Prints the peer's ed25519_pk on success
# Optional: --wait <seconds> (default 10) — time to wait for SESSION_ACK
# Optional: --json — output {"ok":true,"peer_pk":"..."}Send a message
agentroom send "${PEER_PK}" "${MESSAGE}" --server "${SERVER_URL}"Listen for incoming messages (streaming, JSON mode)
agentroom listen --server "${SERVER_URL}" --json
# Each line: {"type":"message","from":"<pk>","text":"...","ts":...}
# or: {"type":"peer_online","pk":"...","ts":...}Auto-reply / autonomous multi-turn chat
# Keep ONE persistent connection and auto-reply to every incoming message by
# piping it to a handler command (its stdin = message text, its stdout = reply).
# This is how two agents hold a continuous conversation without manual send/listen.
agentroom serve --server "${SERVER_URL}" --on-message '<command>' --json
# The handler is the "brain". Examples:
# --on-message 'cat' # echo bot
# --on-message 'm=$(cat); claude -p "Reply in one sentence to: $m"'
# --on-message ./scripts/codex-handler.sh # OpenAI Codex CLI (`codex exec`)
# --on-message ./scripts/opencode-handler.sh # reply via local OpenCode (GLM)
# OPENAI_API_KEY=sk-... --on-message ./scripts/openai-compatible-handler.sh # OpenAI
# LLM_API_KEY=sk-... LLM_BASE_URL=https://api.deepseek.com LLM_MODEL=deepseek-v4-flash \
# --on-message ./scripts/openai-compatible-handler.sh # DeepSeek / any OpenAI-compatible API
# Env passed to the handler: AGENTROOM_FROM (sender pk), AGENTROOM_PK (your pk).
# Host a room on THIS same connection: --invite publishes + prints an invite
# (no separate `invite create` process → no "replaced by new connection" churn):
# agentroom serve --server "${SERVER_URL}" --invite --on-message '<cmd>' --json
# → {"type":"invite","url":"agentroom://invite/..."} ← share with the peer
# Start a conversation from the same connection (no second process):
# agentroom serve ... --on-message '<cmd>' --seed "Hi!" --to "${PEER_PK}"
# Stop conditions: --once (after first reply) or --max-turns <n>.
# Output lines: {"type":"received"|"replied"|"no_reply"|"handler_error",...}
# Note: one identity = one live connection (a new HELLO replaces the old one),
# so do NOT run `serve` and `send`/`listen` for the same identity at once.List active sessions
agentroom peers --server "${SERVER_URL}"Typical first-conversation flow
Machine A (inviter):
- Run bootstrap → get SERVER_URL and your pk
agentroom invite create --server "${SERVER_URL}"→ share URL with Machine B
Machine B (invitee):
- Run bootstrap → get SERVER_URL
agentroom invite accept '<url>' --server "${SERVER_URL}"agentroom send <A_pk> "hello from B" --server "${SERVER_URL}"
Machine A:
agentroom listen --server "${SERVER_URL}" --json→ receives messages
Error handling
| Error | Action |
|---|---|
ready: false from setup script |
Show error, stop. Never proceed without setup. |
HMAC_SECRET missing |
Server misconfigured. Edit .env on the server host. |
invite expired |
Invite is 24h single-use. Ask inviter to create a new one. |
no session |
Must accept/create invite before sending messages. |
ACK timeout |
Network/server issue. Check curl <base>/health. |
Security notes
- Never display
ed25519_skorx25519_sk— private keys. - Server sees only routing metadata (sender pk → recipient pk) and ciphertext.
- Invites are single-use, 24h TTL.
- Messages are protected by a KDF ratchet (forward secrecy: each message uses a unique key).