Imported from leox255/loopsy (
AGENTS.md). Install upstream withnpx skills add leox255/loopsy. Copyright stays with the author.
Loopsy - Cross-Machine Communication for AI Coding Agents
What is Loopsy?
Loopsy enables AI coding agent instances (Claude Code, Gemini CLI, Codex CLI, etc.) on different machines to communicate. You can:
- Run commands on remote machines
- Transfer files between machines
- Share context (key-value state) between AI coding agent instances
Setup
- Run
loopsy initon each machine to generate config and API key - Run
loopsy starton each machine to start the daemon - Exchange API keys between machines (add to
~/.loopsy/config.yamlunderauth.allowedKeys) - Peers auto-discover via mDNS, or add manually:
loopsy peers add <ip>
MCP Tools Available
When the Loopsy MCP server is running, you have these tools:
loopsy_list_peers- See all machines on the networkloopsy_execute- Run a command on a remote machineloopsy_transfer_file- Push/pull files between machinesloopsy_list_remote_files- Browse files on a remote machineloopsy_context_set- Store shared state on a peerloopsy_context_get- Retrieve shared state from a peerloopsy_peer_status- Check a peer's healthloopsy_broadcast_context- Set context on ALL peersloopsy_context_list- List context entries with optional prefix filterloopsy_context_delete- Delete a context entry by keyloopsy_send_message- Send a protocol-compliant message to a peer (handles envelope, inbox key, outbox copy, TTL)loopsy_check_inbox- Check your inbox for new messagesloopsy_ack_message- Acknowledge a received message (see Known Issues)loopsy_check_ack- Check if a peer has acknowledged your messages
Messaging Protocol v1
This section defines the standard protocol for AI coding agent instances to communicate reliably across machines. Follow these conventions exactly so that any new machine joining the network can participate immediately.
Key Concepts
- Inbox: Messages addressed to you, stored on YOUR machine by the sender
- Outbox: Local copies of messages you sent, stored on YOUR machine
- ACK: Acknowledgment that a message was read, stored on the SENDER's machine by the receiver
Context Key Patterns
| Pattern | Stored On | Purpose |
|---|---|---|
inbox:<recipient>:<msg_id> |
Recipient's machine | Incoming message for that peer |
outbox:<msg_id> |
Sender's machine | Local record of sent message |
ack:<sender> |
Sender's machine | Last message ID processed from that sender |
Message ID Format
<timestamp>-<sender_hostname>-<4char_hex>
Example: 1771732800000-kai-a3f2
Generate with: Date.now() + '-' + myHostname + '-' + randomHex(4)
Message Envelope
The value stored in each context key is a JSON string with this structure:
{
"from": "kai",
"to": "leo",
"ts": 1771732800000,
"id": "1771732800000-kai-a3f2",
"type": "chat",
"body": "Your message text here"
}
Message types:
chat— General conversationrequest— Asking the peer to do somethingresponse— Reply to a requestack— Acknowledgment (usually sent via the ack key pattern instead)broadcast— Message sent to all peers
How to Send a Message
- Discover peers:
GET /api/v1/peersor useloopsy_list_peers - Get peer address and hostname from the peer list
- Generate a message ID:
<Date.now()>-<my_hostname>-<random_4hex> - Create the JSON envelope with from, to, ts, id, type, body
- PUT to peer's machine:
PUT /api/v1/context/inbox:<peer_hostname>:<msg_id>on the peer's address, with{"value": "<json_envelope>", "ttl": 3600} - Store local outbox copy:
PUT /api/v1/context/outbox:<msg_id>on localhost with the same envelope - Poll for ACK (optional):
GET /api/v1/context/ack:<peer_hostname>on localhost
How to Receive Messages
- List all context:
GET /api/v1/contexton localhost (or use?prefix=inbox:<my_hostname>:if prefix filtering is available) - Filter for keys matching
inbox:<my_hostname>:* - Parse each value as JSON and process in timestamp order
- Send ACK:
PUT /api/v1/context/ack:<my_hostname>on the SENDER's machine with{"value": "<last_msg_id>"} - Delete processed messages:
DELETE /api/v1/context/inbox:<my_hostname>:<msg_id>on localhost
How to Broadcast
To send a message to ALL online peers:
- Discover all peers via
/api/v1/peers - For each online peer, send the message to their inbox as above
- Or use
loopsy_broadcast_contextwith keyinbox:<peer>:<msg_id>(must iterate per peer since inbox keys are unique per recipient)
Polling Conventions
| Scenario | Interval | Timeout |
|---|---|---|
| Active conversation (expecting reply) | 5-10 seconds | 60 seconds |
| Passive monitoring | 30-60 seconds | None |
| Waiting for ACK | 5 seconds | 30 seconds |
TTL Defaults
| Key Type | TTL |
|---|---|
| Inbox messages | 3600s (1 hour) |
| Outbox copies | 3600s (1 hour) |
| ACKs | 7200s (2 hours) |
REST API Quick Reference
All requests require Authorization: Bearer <api_key> header.
PUT /api/v1/context/<key> {"value": "...", "ttl": 3600}
GET /api/v1/context/<key> → returns entry
GET /api/v1/context → returns {entries: [...]}
DELETE /api/v1/context/<key> → deletes entry
Example: Full Send + Receive Cycle
Machine A (kai) sends to Machine B (leo):
# 1. Kai generates message
msg_id = "1771732800000-kai-a3f2"
envelope = {"from":"kai","to":"leo","ts":1771732800000,"id":"1771732800000-kai-a3f2","type":"chat","body":"Hello Leo!"}
# 2. Kai PUTs to Leo's machine
PUT http://<leo_ip>:19532/api/v1/context/inbox:leo:1771732800000-kai-a3f2
Body: {"value": "<envelope_json>", "ttl": 3600}
# 3. Kai stores outbox locally
PUT http://localhost:19532/api/v1/context/outbox:1771732800000-kai-a3f2
Body: {"value": "<envelope_json>", "ttl": 3600}
# 4. Kai polls for ACK
GET http://localhost:19532/api/v1/context/ack:leo
Machine B (leo) receives and acknowledges:
# 1. Leo lists context and finds inbox:leo:1771732800000-kai-a3f2
GET http://localhost:19532/api/v1/context
# 2. Leo sends ACK to Kai's machine
PUT http://<kai_ip>:19532/api/v1/context/ack:leo
Body: {"value": "1771732800000-kai-a3f2", "ttl": 7200}
# 3. Leo deletes processed message locally
DELETE http://localhost:19532/api/v1/context/inbox:leo:1771732800000-kai-a3f2
Important Notes
- No hardcoded hostnames: Always discover peers dynamically via
/api/v1/peersor/api/v1/status - Your hostname: Read from
/api/v1/status→hostnamefield, or from~/.loopsy/config.yaml→server.hostname - Peer auth: Use the peer's API key (from
auth.allowedKeysin your config) in the Authorization header when writing to their machine - Scalability: This protocol works the same whether there are 2 peers or 20
- MCP tools: If MCP tools (
loopsy_context_set, etc.) are working, use them. If they have auth issues, fall back to the REST API directly via curl or a Node.js script
Task Queue Protocol v1
Enables one AI coding agent instance to delegate work to another machine and poll for results. Validated bidirectionally between macOS and Windows.
Context Key Pattern
| Pattern | Stored On | Purpose |
|---|---|---|
task:<id> |
Assignee's machine | Task definition and status |
Task ID Format
Use simple sequential IDs (001, 002, ...) or timestamp-based IDs for uniqueness.
Task Payload
{
"id": "001",
"from": "leo",
"assignee": "kai",
"description": "Gather system metrics and post as context key metrics:leo on leo's machine",
"status": "pending",
"result": null
}
Status lifecycle: pending → running → done | failed
How to Create a Task
- Create the JSON payload with id, from, assignee, description, status="pending", result=null
- POST to assignee's machine:
loopsy_context_setwith keytask:<id>on the assignee's machine - Send a message to the assignee notifying them of the new task
How to Process a Task
- Check for tasks:
loopsy_context_listwith prefixtask:on your machine, orloopsy_context_getfor a specific task - Claim: Update the task's status to
"running"vialoopsy_context_set - Execute: Perform the work described in the task
- Complete: Update status to
"done"and populateresultfield - Notify: Send a message back to the creator confirming completion
How to Poll for Results
- Poll:
loopsy_context_getwith keytask:<id>on the assignee's machine - Check status: If
"done", read theresultfield - Poll interval: Every 5-10 seconds for active tasks
Example: Full Task Cycle
Leo creates task for Kai:
loopsy_context_set on kai's machine:
key: "task:001"
value: {"id":"001","from":"leo","assignee":"kai","description":"List top 5 processes by memory","status":"pending","result":null}
ttl: 3600
Kai claims and completes:
# 1. Kai updates status to running
loopsy_context_set key="task:001" value={...,"status":"running",...}
# 2. Kai does the work, then updates with result
loopsy_context_set key="task:001" value={...,"status":"done","result":"MsMpEng.exe 431MB, node.exe 393MB, ..."}
Leo polls and gets result:
loopsy_context_get on kai's machine key="task:001"
→ status: "done", result: "MsMpEng.exe 431MB, ..."
Distributed Operations
Loopsy enables coordinated work across machines. Here are validated patterns:
Distributed Grep
Search across all machines' codebases and combine results.
- Requester greps locally and stores results as
dgrep-result:<id>:<hostname>on their own machine - Requester asks peer to grep and post results as
dgrep-result:<id>:<peer_hostname>on the requester's machine - Alternative: Use
loopsy_executeto run grep on the peer's machine remotely - Requester reads both result keys and combines into a unified report
Remote Command Execution
Use loopsy_execute to run commands on peer machines. The command runs in the daemon's shell environment:
- macOS/Linux: Standard shell (zsh/bash)
- Windows with Git Bash: Commands use Unix-style paths (
/c/Users/...notC:\Users\...) - Denylist: Commands in
execution.denylistconfig are blocked (rm, shutdown, etc.)
Cross-Platform Notes
Key learnings from macOS ↔ Windows communication:
- Windows paths with Git Bash: The daemon runs in Git Bash, so use
/c/Users/...notC:\Users\...when usingloopsy_execute - File transfer paths: Both source and destination must be within
transfer.allowedPathsin the respective machine's config. Check withloopsy_peer_statusor ask the peer - MCP auth: The MCP server auto-loads the API key from
~/.loopsy/config.yaml— no env vars needed - Peer discovery: Use
loopsy_list_peersthenloopsy_peer_statusto get the peer's configured hostname (may differ from mDNS hostname) - Shell differences: When running commands remotely, be aware of the target machine's shell. Use
pwdto discover the working directory first
Known Issues
loopsy_ack_messageMCP tool returns HTTP 400 in some cases — workaround: useloopsy_context_setdirectly to set theack:<hostname>key on the sender's machine
Multi-Session Support
Run multiple daemon sessions per machine, each acting as an independent peer. This enables a fleet of AI coding agent instances to collaborate — e.g., 3 sessions on macOS + 3 on Windows = 6 peers.
Session Architecture
Each session gets its own isolated data directory under ~/.loopsy/sessions/<name>/ containing:
config.yaml— auto-generated from parent config (unique port, hostname)context.json— isolated context storepeers.json— isolated peer registrylogs/audit.jsonl— isolated audit logdaemon.pid— session process ID
Sessions share the parent machine's auth.apiKey and auth.allowedKeys so all siblings + remote peers can authenticate with each other.
CLI Commands
# Start a single named session
loopsy session start worker-1
# Start a fleet of N sessions (worker-1, worker-2, ..., worker-N)
loopsy session start-fleet --count 3
# List all sessions with status
loopsy session list
# Show detailed status for a session
loopsy session status worker-1
# Stop a single session
loopsy session stop worker-1
# Stop all sessions
loopsy session stop-all
How Sessions Work
- Port allocation: Each session auto-finds a free port starting from 19533 upward
- Hostname: Each session gets
<machine_hostname>-<name>(e.g.,leo-worker-1) - Discovery: Sessions disable mDNS to avoid conflicts. Instead, they use manual peers:
- Main daemon (localhost:19532)
- All running sibling sessions (localhost:195xx)
- Remote peers from parent config
- Cross-registration: When a session starts, it registers itself with the main daemon and all siblings via
POST /peers
MCP Server with Sessions
To connect an MCP server to a specific session, set the LOOPSY_DATA_DIR env var:
LOOPSY_DATA_DIR=~/.loopsy/sessions/worker-1 node packages/mcp-server/dist/index.js
Example: 6-Peer Fleet
# On macOS (leo):
loopsy session start-fleet --count 3
# Creates: leo-worker-1 (19533), leo-worker-2 (19534), leo-worker-3 (19535)
# On Windows (kai):
loopsy session start-fleet --count 3
# Creates: kai-worker-1 (19533), kai-worker-2 (19534), kai-worker-3 (19535)
# All 6 sessions + 2 main daemons = 8 peers communicating
loopsy session list
Daemon Data Directory Flag
The daemon supports --data-dir for custom data directories:
node packages/daemon/dist/main.js --data-dir ~/.loopsy/sessions/worker-1
This reads config from and stores state in the specified directory instead of ~/.loopsy/.
Project Structure
packages/protocol- Shared types, schemas, constantspackages/discovery- mDNS peer discoverypackages/daemon- Fastify HTTP server (the core)packages/mcp-server- MCP server for AI coding agents (Claude Code, Gemini CLI, Codex CLI)packages/cli- CLI management tool
Build
pnpm install
pnpm build
Data Storage
Default location: ~/.loopsy/
| File | Purpose |
|---|---|
config.yaml |
Daemon configuration |
context.json |
Context key-value store |
peers.json |
Peer registry |
logs/audit.jsonl |
Request audit log |
daemon.pid |
Main daemon PID |
sessions/<name>/ |
Per-session data directories (same structure) |
Configuration
Config lives at ~/.loopsy/config.yaml. Key settings:
server.port- Daemon port (default 19532)server.hostname- Custom hostname (default: OS hostname)server.dataDir- Data directory (set automatically by--data-dirflag)auth.apiKey- This machine's API keyauth.allowedKeys- Map of peer name -> API keyexecution.denylist- Commands that cannot be executed remotelytransfer.deniedPaths- Paths that cannot be accessed for file transferdiscovery.enabled- Toggle mDNS discoverydiscovery.manualPeers- Fallback peer list for networks blocking multicast