@titochan/claw-link
v0.3.2
Published
P2P encrypted communication between AI agents via WebRTC DataChannel
Downloads
1,329
Maintainers
Readme
claw-link
P2P encrypted communication between Claw, Agent, and so on instances via WebRTC.
Agent Capability Levels
Not all agents are equal. claw-link provides three integration modes matched to what the agent can actually do:
┌──────────────────────────────────────────────────────────────┐
│ L1 Serial Agent L2 Streaming Agent L3 In-Process│
│ (run cmd → read) (background proc) (Node.js) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ curl / HTTP │ │ stdin/stdout │ │ require() │ │
│ └──────┬──────┘ └──────┬───────┘ └─────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Bridge │ │ ClawAgent │ │ Transport │ │
│ │ (HTTP+Queue) │ │ (JSON lines) │ │ (EventEmit│ │
│ └──────┬──────┘ └──────┬───────┘ └─────┬─────┘ │
│ └──────────────────────┴────────────────────┘ │
│ │ │
│ WebRTC DataChannel (encrypted P2P) │
└──────────────────────────────────────────────────────────────┘Which level is my agent?
| Capability | L1 | L2 | L3 |
|------------|----|----|-----|
| Run a shell command and read output | yes | yes | yes |
| Run a background process | no | yes | yes |
| Pipe stdin/stdout in real-time | no | yes | yes |
| require() Node.js modules | no | no | yes |
| Examples | Simple ReAct agent, tool-call-only agent, most MCP clients | Claude Code, Cursor, Cline, aider | Custom Node.js agent, OpenClaw runtime |
| Use mode | claw-link bridge | claw-link connect --json | require('claw-link') |
L1: Bridge Mode (serial agents)
Problem: L1 agent runs claw-link connect, the process never exits, agent freezes.
Solution: The bridge runs in the background. Agent talks to it via one-shot HTTP calls. Messages queue up and wait. Hooks wake the agent when something arrives.
Setup
# Start bridge in background (once)
claw-link bridge --port 7654 --name MyClaw --perm helper \
--on-message 'echo "CLAW_MSG:{from}:{type}" >> /tmp/claw-notify' &Full workflow — CLI commands (recommended)
Built-in CLI commands talk to the bridge directly — no curl, no JSON body construction:
# 1. Connect to a room (auto-generated ID if omitted)
claw-link bridge connect
# → {"roomId":"a1b2c3d4","inbox":"...","invite":"..."}
# Or with a known room ID:
claw-link bridge connect my-room
# → {"roomId":"my-room","inbox":"...","invite":"..."}
# 2. Share roomId SECURELY with the other agent (private channel only!)
# The Room ID IS the auth token — anyone who has it can join.
# 3. Other agent connects on their bridge (same command):
claw-link bridge connect a1b2c3d4
# 4. Send a message
claw-link bridge send "Hello from MyClaw"
# → {"ok":true,"id":"msg123","roomId":"a1b2c3d4"}
# Send other message types:
claw-link bridge send -t task --desc "review app.js" --data '{"file":"app.js"}'
claw-link bridge send -t query "what framework are you using?"
# 5. Poll for reply (instant or long-poll)
claw-link bridge recv --wait 10
# → [{"id":"...","type":"result","payload":{...},"from":"PeerClaw",...}]
# 6. Check connection status anytime
claw-link bridge status
# → {"connected":true,"roomId":"a1b2c3d4","peer":"PeerClaw","permission":"helper",...}
# 7. List all rooms
claw-link bridge rooms
# 8. Done — disconnect
claw-link bridge close a1b2c3d4All commands support --port <port> (default: 7654) and --room <roomId> where applicable.
Full workflow — curl (alternative)
If you prefer raw HTTP calls or your environment doesn't have claw-link installed:
curl -s -X POST http://127.0.0.1:7654/connect
curl -s -X POST http://127.0.0.1:7654/connect -d '{"roomId":"my-room"}'
curl -s -X POST http://127.0.0.1:7654/send \
-d '{"type":"task","description":"review app.js","data":{"file":"app.js","content":"..."}}'
curl -s 'http://127.0.0.1:7654/recv?wait=10'
curl -s http://127.0.0.1:7654/status
curl -s -X POST http://127.0.0.1:7654/closeMessage Monitoring (IMPORTANT — read this carefully)
If you are an AI agent with a built-in hook/event system (e.g. Claude Code hooks, Cursor rules, Cline auto-run, or any file-watch / cron / scheduled-task mechanism), you MUST set up monitoring for incoming claw-link messages. Otherwise you will miss messages from your peer while you are busy doing other work.
The bridge persists all messages to ~/.claw-link/<roomId>/inbox.jsonl — nothing is ever lost. But you need a way to know when new messages arrive so you can respond.
Option 1: Claude Code — hooks + CLAUDE.md
Step 1. Start bridge with notification file:
claw-link bridge --on-message 'echo "{from}:{type}:{id}" >> /tmp/claw_notify'Step 2. Add hook in .claude/settings.json — fires after every tool call, checks for new messages:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash|Read|Edit|Write",
"hooks": [
{
"type": "command",
"command": "if [ -s /tmp/claw_notify ]; then echo '[ClawLink] New messages waiting — run: claw-link bridge recv'; fi"
}
]
}
]
}
}Step 3. Add to your project's CLAUDE.md:
## ClawLink
You have access to claw-link for P2P communication with other agents.
- Check for messages: `claw-link bridge recv --wait 5`
- Send a message: `claw-link bridge send "your message"`
- Check status: `claw-link bridge status`
- Full usage: `claw-link guide`
When you see "[ClawLink] New messages waiting" in hook output, immediately
run `claw-link bridge recv` to read and respond. After responding, clear
the notification: `> /tmp/claw_notify`Option 2: Cursor / Windsurf — rules file
Add to .cursor/rules/clawlink.mdc (Cursor) or .windsurfrules (Windsurf):
---
description: ClawLink P2P agent communication
globs: *
alwaysApply: true
---
## ClawLink Integration
A claw-link bridge is running on port 7654. You can communicate with
other AI agents through it.
Commands (run in terminal):
- `claw-link bridge recv --wait 5` — check for new messages
- `claw-link bridge send "message"` — send a chat message
- `claw-link bridge status` — check connection status
- `claw-link guide` — full command reference
When completing a task or waiting for input, check for claw-link messages.
Respond promptly to incoming messages.Option 3: Cline — auto-approve rules
Add to .clinerules:
# ClawLink bridge is running on port 7654.
# Check messages: claw-link bridge recv --wait 5
# Send messages: claw-link bridge send "your message"
# Full guide: claw-link guide
#
# After finishing a task, check for incoming claw-link messages.Option 4: Any agent — poll loop (fallback)
If your agent has no hook/rules system, use a background poll:
# Start bridge with flag-file hook:
claw-link bridge --on-message 'echo "{from}:{type}:{id}" >> /tmp/claw_notify'
# Check for messages anytime:
claw-link bridge recv
# Or long-poll (blocks until message or timeout):
claw-link bridge recv --wait 30
# Full inbox history:
claw-link bridge recv --allMessages persist to ~/.claw-link/<roomId>/inbox.jsonl — nothing is ever lost, even if you check hours later.
Notification Adapters (recommended)
Configure once in .clawlinkrc — all events (connect, message, disconnect) are delivered automatically with full payload:
{
"notify": { "type": "webhook", "url": "http://localhost:8080/clawlink" }
}| Type | Config | How it works |
|------|--------|-------------|
| webhook | {url, headers?} | HTTP POST full JSON payload to URL |
| file | {dir} | Write one .json file per event to dir |
| shell | {command} | Template {from}, {content} etc in shell cmd |
| stdout | — | JSON lines to stdout |
Webhook payload example:
{"event":"message","ts":1234567890,"roomId":"my-room","from":"PeerClaw","type":"chat","content":"hello"}Hooks (legacy)
Shell hooks still work via CLI flags — useful for simple setups:
| Flag | Fires when | Placeholders |
|------|-----------|-------------|
| --on-connect | Peer joins | {peer}, {permission}, {roomId} |
| --on-message | Message arrives | {from}, {type}, {id}, {roomId}, {content}, {description}, {question} |
| --on-disconnect | Peer leaves | {reason}, {roomId} |
Auto-Reconnect
When a peer disconnects, the bridge automatically reconnects to the same room:
- Backoff: 5s → 10s → 20s → 30s (cap), exponential
- Retries: unlimited until
/closeor TG/kill - Message safety: with ACK enabled, unconfirmed messages are replayed after reconnect
- Room ID: stays the same — inbox path never drifts
To stop reconnection: curl -s -X POST http://127.0.0.1:7654/close -d '{"roomId":"..."}'
ACK & Offline Retry
The bridge tracks outbound message delivery:
- Every sent message enters a pending queue (persisted to
pending.jsonl) - The receiving bridge sends back an ACK automatically
- On ACK receipt, the message leaves the pending queue
- On reconnect, all pending messages are replayed
- The receiver deduplicates by message ID — no double delivery
Check pending count: curl -s http://127.0.0.1:7654/status → {"pending": 0, ...}
Bridge CLI Reference
| Command | Equivalent HTTP | Description |
|---------|----------------|-------------|
| bridge connect [room-id] | POST /connect | Connect to a room |
| bridge send [message] | POST /send | Send message (default: chat) |
| bridge send -t task --desc "..." | POST /send | Send task |
| bridge send -t query "..." | POST /send | Send query |
| bridge recv [--wait N] | GET /recv?wait=N | Receive messages |
| bridge recv --all | GET /recv?all=1 | Read full inbox |
| bridge recv --limit N | GET /recv?limit=N | Backpressure: max N msgs |
| bridge status [--room X] | GET /status?room=X | Room status |
| bridge rooms | GET /rooms | List all rooms |
| bridge tasks [--state X] | GET /tasks?state=X | Track delegated tasks |
| bridge perm <level> | POST /perm | Change permission dynamically |
| bridge close [room-id] | POST /close | Close room |
| bridge stop [pid] | — | Kill bridge process |
HTTP API Reference
| Method | Path | Body | Returns |
|--------|------|------|---------|
| POST | /connect | {roomId?} | {roomId, inbox, invite} |
| GET | /status | — | {connected, roomId, peer, permission, inbox} |
| POST | /send | {type, ...} | {ok, id} |
| GET | /recv | — | [messages] |
| GET | /recv?wait=N | — | [messages] (long-poll, max 120s) |
| GET | /rooms | — | [{roomId, connected, peer, ...}] |
| GET | /tasks | — | [{id, description, state, sentAt, ...}] |
| POST | /perm | {roomId?, level} | {ok, permission} |
| POST | /close | {roomId?} | {ok} |
| GET | /health | — | {status} |
L2: JSON Lines Mode (streaming agents)
Agent runs a background process, reads stdout line by line, writes to stdin.
Connect (both peers use the same command)
# First peer — omit room-id to auto-generate:
claw-link connect --name MyClaw --perm helper --json
# Second peer — provide the room-id:
claw-link connect a1b2c3d4 --name PeerClaw --perm helper --jsonstdout events (read these)
{"event":"room","roomId":"a1b2c3d4"}
{"event":"role","role":"offerer"}
{"event":"connected","peer":"PeerClaw","permission":"helper"}
{"event":"message","id":"xx","type":"chat","payload":{"content":"hello"},"from":"PeerClaw","ts":1234567890}
{"event":"disconnected","reason":"peer-left"}
{"event":"error","message":"..."}stdin messages (write these)
{"type":"chat","content":"hello"}
{"type":"task","description":"review this file","data":{"file":"app.js","content":"..."}}
{"type":"result","data":{"status":"done","issues":[]},"replyTo":"msg-id"}
{"type":"file","name":"output.json","content":"{...}"}
{"type":"query","question":"what framework are you using?"}
{"type":"ack","replyTo":"msg-id"}L3: Node API (in-process agents)
const { ClawTransport, protocol } = require('claw-link');
// Create room
const t = new ClawTransport({ name: 'MyClaw', permission: 'helper' });
t.on('room', (roomId) => { /* share roomId with peer */ });
t.on('connected', (peer, perm) => {
t.send(protocol.task('review this code', { file: 'app.js' }, 'MyClaw'));
});
t.on('message', (msg) => {
if (msg.type === 'result') console.log('Got result:', msg.payload.data);
});
t.connect();
// Join room (peer side)
const peer = new ClawTransport({ name: 'Peer', room: 'a1b2c3d4' });
peer.on('message', (msg) => {
peer.send(protocol.result({ status: 'done' }, 'Peer', msg.id));
});
peer.connect();Events
| Event | Args | When |
|-------|------|------|
| room | roomId | Room assigned |
| role | role | "offerer" or "answerer" |
| connected | peerName, permission | P2P ready |
| message | msg | Incoming message |
| disconnected | reason | Connection lost |
| error | err | Error occurred |
Message Types (all modes)
| type | required fields | use |
|------|----------------|-----|
| chat | content | Plain text message |
| task | description, data | Delegate a task |
| result | data, replyTo | Return task result |
| file | name, content | Share file content |
| query | question | Ask a question |
| ack | replyTo | Confirm receipt |
Permission Levels
Set with --perm. Both sides negotiate — the more restrictive wins.
| Level | Allows | Use when |
|-------|--------|----------|
| intimate | Everything: chat, task, file, config | Agents you fully control |
| helper | Chat + task + file (private data auto-filtered) | Collaboration |
| chat | Chat only | Untrusted peers |
Security: Room ID = Auth Token
The Room ID is a 128-bit cryptographically random string. It serves as both the room address and the authentication token — there is no separate password or key. Knowing the Room ID is the only thing needed to join a room.
Rules:
- Never post a Room ID in public channels, issue trackers, or logs
- Never commit a Room ID to version control
- Only share Room IDs through secure private channels (encrypted DM, face-to-face, etc.)
- If a Room ID is compromised, close the room and create a new one
- Custom room IDs (e.g.
--room my-room) are short and guessable — use only for local testing
The signaling server enforces: rate limiting, IP cooldown, room capacity (2 peers max), message type whitelist, and payload validation. But none of that matters if the Room ID leaks.
Connection Flow
Peer A Signal Server Peer B
│ │ │
│── connect (no room) ─────→│ │
│←── ready {roomId} ────────│ │
│ │ │
│ (A shares roomId with B out-of-band) │
│ │ │
│ │←── connect /roomId ────│
│←── peer-joined ───────────│── ready {roomId} ─────→│
│── offer ─────────────────→│───────────────────────→│
│←── answer ────────────────│←───────────────────────│
│←→─ ICE candidates ───────→│←─────────────────────→│
│ │ │
│═══════════ P2P DataChannel (DTLS encrypted) ═══════│
│── handshake ──────────────────────────────────────→│
│←── handshake-ack ─────────────────────────────────│
│ │
│ (permission negotiated independently by both) │
│═══ chat / task / file / query / result ═══════════│Configuration (.clawlinkrc)
Place a .clawlinkrc file (JSON) in your project directory or home directory. CLI args override rc values.
{
"name": "MyClaw",
"permission": "helper",
"port": 7654,
"signalingUrl": "wss://ginfo.cc/signal/",
"dataDir": "~/.claw-link",
"defaultRoom": "my-room",
"aliases": {
"stable": "my-stable-room-id",
"dev": "my-dev-room-id"
},
"tgToken": "123456:ABC-DEF...",
"tgChatId": "987654321",
"notify": {
"type": "webhook",
"url": "http://localhost:8080/clawlink"
},
"hooks": {
"onConnect": "echo connected",
"onMessage": "echo {from}:{content}",
"onDisconnect": "echo disconnected"
}
}Room aliases: use short names in place of room IDs everywhere — CLI, HTTP API, even curl:
claw-link connect stable # resolves to "my-stable-room-id"
curl -X POST .../connect -d '{"roomId":"dev"}' # resolves to "my-dev-room-id"Priority: CLI flags > environment variables > .clawlinkrc > defaults
Human CLI
git clone https://github.com/TITOCHAN2023/ClawLink.git
cd ClawLink && npm install
claw-link connect --name ClawA # Create room (interactive)
claw-link connect <room-id> --name ClawB # Join room (interactive)
claw-link server --port 8765 # Local signaling server
claw-link ping wss://ginfo.cc/signal/ # Test connectivityTelegram Bot Notifications
Bind a Telegram bot to monitor all rooms and remotely kill them. Agents don't need to know about this — it's a user-level feature.
Setup via CLI flags:
claw-link bridge --tg-token "123456:ABC-DEF..." --tg-chat "987654321"Or set once via environment variables (recommended):
export CLAWLINK_TG_TOKEN="123456:ABC-DEF..."
export CLAWLINK_TG_CHAT="987654321"After this, every claw-link bridge will auto-bind TG with no extra flags.
What you see in Telegram:
🏠 Room created: a1b2c3d4
🔗 [a1b2c3d4] PeerClaw joined (helper)
💬 [a1b2c3d4] PeerClaw: Hello!
📋 [a1b2c3d4] PeerClaw task: Review app.js
❌ [a1b2c3d4] Peer left
🔄 [a1b2c3d4] Reconnecting (attempt 1)...TG commands:
| Command | Effect |
|---------|--------|
| /kill <roomId> | Close the room, stop auto-reconnect |
| /set <roomId> <level> | Change permission (intimate/helper/chat) |
Architecture
src/
bridge.js ClawBridge — HTTP API + message queue + hooks (L1)
agent.js ClawAgent — JSON lines stdin/stdout (L2)
transport.js ClawTransport — P2P EventEmitter core (L3)
protocol.js Message envelope + type constructors
client.js Interactive terminal UI (humans)
cli.js CLI entry (connect/bridge/server/ping)
server.js Built-in JS signaling server (local dev)
session.js Session state machine
permissions.js Permission negotiation + privacy filterDefault signaling server: wss://ginfo.cc/signal/
Other Docs
IF YOU ARE Hermes Agent
howToHook/hermes_agent.md
License
MIT
