opententacle
v0.1.15
Published
P2P coordination layer for AI coding agents
Readme
opententacle
P2P coordination for AI agents. Connects Claude Code (and other runtimes) across devices — agents discover each other, advertise capabilities, and delegate tasks without a central server.
How it works
Each device runs tentacle. It joins a libp2p DHT network and exposes three MCP tools to Claude Code: discover_agents, delegate_task, and check_result.
Three modes:
- Passive (
tentacle mcp) — connects to the network, discovers peers, and can delegate tasks. No config needed. This is what Claude Code invokes via MCP. - Sharing (
tentacle start) — additionally publishes your capabilities to the DHT, accepts incoming tasks, and executes them locally. Requirestentacle setupfirst. - Relay (
tentacle relay) — a headless bootstrap/relay node. No Claude dependency. Participates in DHT and runs a circuit relay v2 server so peers behind NAT can find and reach each other. Intended to run on a public server that everyone adds to theirconfig.peers.
Install
Requirements: Node 24+, Claude Code
npm install -g opententacle
tentacle setuptentacle setup probes your environment, saves ~/.opententacle/config.yaml, and offers to register the MCP server in Claude Code automatically.
tentacle setup is only needed if you want to share your agent with the network — Claude Code works in passive mode without any config.
Usage
Start in passive mode (what Claude Code uses — no config needed):
tentacle mcpStart as a sharing daemon (advertises your capabilities and accepts tasks):
tentacle startRegister as MCP server in Claude Code (~/.claude/claude.json):
{
"mcpServers": {
"tentacle": {
"command": "tentacle",
"args": ["mcp"]
}
}
}Claude Code now has access to three tools:
| Tool | Description |
| ----------------- | ----------------------------------------------------------------------- |
| discover_agents | Find peers by capability, e.g. "tool:puppeteer" or "tag:typescript" |
| delegate_task | Send a task to a specific peer and get back a taskId |
| check_result | Poll for the result of a delegated task |
Connecting peers
Add a bootstrap peer (connects, discovers its peer ID, and saves it to config):
tentacle peer add 1.2.3.4:4001Remove a bootstrap peer:
tentacle peer remove 1.2.3.4:4001Peers added this way are persisted in ~/.opententacle/config.yaml and dialed automatically on next start.
Environment variables
| Variable | Effect |
| --------------- | -------------------------------------------------------------------------------------- |
| NO_BANNER | Set to any value to suppress the startup ASCII art |
| CI | Banner is also suppressed when this is set (most CI environments set it automatically) |
| RELAY_PORT | TCP port for the relay node (default 4001). Relay mode only. |
| ANNOUNCE_ADDR | Public multiaddr to advertise, e.g. /ip4/1.2.3.4/tcp/4001. Relay mode only. Required when the container cannot detect its own public IP (Docker, ECS). |
Config
The configuration file is generated by tentacle reload-config and lives at ~/.opententacle/config.yaml.
Most fields are auto-detected — edit only the ones you want to override.
Advertised capabilities
# Which Claude Code built-in tools to advertise to the network.
# Set a tool to false to hide it from peers (it still works locally).
tools:
bash: true
webFetch: true
webSearch: true
# Which MCP servers to advertise.
mcpServers:
puppeteer: true
# Free-form labels peers can search by (e.g. "tag:typescript").
tags:
- typescript
- data-pipelinesInbound task queue
queue:
capacity: 5 # max tasks accepted concurrently (default 5)Webhook (result delivery)
When a task finishes the result is POSTed back to the caller's webhook URL. For the caller to receive results its own webhook must be reachable from the network.
# Override the auto-detected public URL. Required behind strict NAT
# or when running in Docker without UPnP.
webhook: https://myserver.example.comAuto-detection order: explicit webhook value → UPnP-mapped LAN IP → Cloudflare trace public IP.
Network ports
network:
libp2pPort: 4001 # TCP port for libp2p (default: random)
webhookPort: 8080 # HTTP port for the webhook server (default: random)
# Multiaddrs broadcast in the manifest. Use when the auto-detected address
# is wrong, e.g. behind a reverse proxy or Cloudflare Tunnel.
announceAddrs:
- /ip4/203.0.113.10/tcp/4001Bootstrap peers
Peers added via tentacle peer add are stored here. You can also edit the list directly.
peers:
- /ip4/1.2.3.4/tcp/4001/p2p/12D3Koo...Execution
Controls how the claude subprocess is invoked when this node executes an incoming task.
execution:
timeout: 300000 # subprocess timeout in ms (default 300 000 = 5 min)
adapters:
claude-code:
# Pass --dangerously-skip-permissions to claude (use with care).
dangerouslySkipPermissions: false
# Restrict which tools claude may use for incoming tasks.
allowedTools: [] # if non-empty, only these tools are allowed
disallowedTools: [] # always blocked even if allowedTools is empty
# Claude Code permission mode (default, acceptEdits, bypassPermissions,
# delegate, dontAsk, plan).
permissionMode: default
# Hard cost cap per task in USD. null = no cap.
maxBudgetUsd: nullRunning as a Docker Worker Node
The included Dockerfile and docker-compose.yml run the daemon as a headless worker that accepts delegated tasks from the network.
Config is persisted in a named Docker volume at /home/node/.opententacle and auto-generated on first boot. Add a network block to the config file (inside the volume) to match the exposed ports — all other fields are auto-generated:
Note:
ANTHROPIC_API_KEYis sufficient for headless operation — theclaudeCLI does not require interactive login when an API key is present.
network:
libp2pPort: 4001
webhookPort: 8080Start the node:
ANTHROPIC_API_KEY=sk-ant-... docker compose up -dIf this node also needs to delegate tasks outward (not just receive them), set config.webhook to a publicly reachable URL for the container — for example, a Cloudflare Tunnel or ngrok address:
webhook: https://your-tunnel.example.comView logs:
docker compose logs -fRunning a Relay Node
A relay node is a lightweight always-on server that helps peers discover each other and connect through NAT. It has no Claude dependency — it only runs libp2p.
Add it to docker-compose.yml (same image as the worker, different command):
services:
relay:
build: .
command: ["node", "dist/index.js", "relay"]
environment:
- RELAY_PORT=4001
- ANNOUNCE_ADDR=/ip4/YOUR_PUBLIC_IP/tcp/4001
volumes:
- relay-data:/home/node/.opententacle
ports:
- "4001:4001"
restart: unless-stopped
volumes:
relay-data:Start it:
docker compose up -d relayOn first boot it prints its multiaddr — copy it, or derive it as /ip4/<ip>/tcp/4001/p2p/<peer-id>.
On each agent, add the relay as a bootstrap peer:
tentacle peer add YOUR_PUBLIC_IP:4001The relay's identity is persisted in the named volume. If it's lost the peer ID changes and all agents' configs need updating — use an EFS mount (or equivalent) for persistence on ephemeral infrastructure like AWS ECS/Fargate.
Opting out: Agents can remove the relay from their config to form a private network:
tentacle peer remove YOUR_PUBLIC_IP:4001Local test cluster
docker-compose.local-test.yml spins up a 3-node cluster on your machine for end-to-end testing. The helper script handles config generation, image building, and peer wiring automatically:
ANTHROPIC_API_KEY=sk-ant-... ./scripts/local-test-up.shOnce running:
| Node | libp2p | webhook | | ----- | -------------- | -------------- | | node1 | localhost:4001 | localhost:8081 | | node2 | localhost:4002 | localhost:8082 | | node3 | localhost:4003 | localhost:8083 |
Follow logs:
docker compose -f docker-compose.local-test.yml logs -fTear down:
docker compose -f docker-compose.local-test.yml down
rm -rf test-data/ # removes identities and configs