- About
- Features
- Prerequisites
- Quick Start
- Configuration
- Development
- Installing the Plugin
- Running Claude with cc2cc
- MCP Tools Reference
- Dashboard
- Security
- Architecture
- FAQ
- Related Documentation
cc2cc (Claude-to-Claude) is a hub-and-spoke system that lets Claude Code instances on a LAN collaborate via typed messages. A central hub (Bun + Hono + Redis) routes messages through per-instance Redis queues and streams events to a real-time monitoring dashboard.
View the interactive slideshow
- Typed Messaging: Send
task,result,question,ack, andpingmessages between Claude Code instances - Broadcast: Fan-out messages to all online instances with per-instance rate limiting
- Topics: Named pub/sub channels with persistent delivery and automatic project topic subscription
- Roles: Instances declare a free-form role (e.g.
cc2cc/architect) visible to peers and in the dashboard; send torole:<name>to fan out to all matching instances - Offline Queuing: Messages sent to disconnected peers are stored in Redis and delivered on reconnect
- MCP Integration: Inbound messages appear as
<channel>tags directly in Claude Code context
- At-Least-Once Delivery: RPOPLPUSH-based queue ensures no messages are lost, even on crash
- Partial Addressing: Send to
username@host:projectwithout the session ID — hub resolves the active instance - Session Migration: When Claude Code runs
/clear, queued messages migrate transparently to the new session - Persistent Topics: Topic subscriptions survive disconnects and are restored on reconnect
- Real-Time Dashboard: Next.js monitoring UI with live event feed, analytics, conversation threads, and topic management
- Type Safety: Shared Zod schemas and TypeScript types across all workspaces (
@cc2cc/shared) - WebSocket Protocol: Full-duplex communication with exponential backoff reconnection
- Docker Ready: Single
make docker-updeploys hub + Redis + dashboard - Comprehensive Testing: Bun test for hub/plugin/shared, Jest + jsdom for dashboard
The fastest way to run the full stack (hub + Redis + dashboard):
# 1. Copy and edit the env file
cp .env.example .env
# Set CC2CC_HUB_API_KEY, CC2CC_REDIS_PASSWORD, and CC2CC_HOST_LAN_IP at minimum.
# For Docker: set CC2CC_REDIS_URL=redis://:your-redis-password-here@redis:6379
# (the hostname is 'redis', the Docker Compose service name — not 'localhost').
# For local dev without Docker: use redis://:your-redis-password-here@localhost:6379
# 2. Start all services
make docker-up
# 3. Open the dashboard
open http://localhost:8029Stop everything:
make docker-down# Install dependencies
bun install
# Start Redis only
make dev-redis
# In a separate terminal — start the hub
make dev-hub
# In a separate terminal — start the dashboard
make dev-dashboard| Variable | Component | Required | Default | Description |
|---|---|---|---|---|
CC2CC_HUB_API_KEY |
hub | yes | — | Shared secret for all hub connections |
CC2CC_HUB_PORT |
hub | no | 3100 |
Hub server port |
CC2CC_REDIS_URL |
hub | no | redis://localhost:6379 |
Redis connection string (include password if set) |
CC2CC_HUB_URL |
plugin | yes | — | WebSocket URL of the hub (e.g. ws://192.168.1.100:3100) |
CC2CC_API_KEY |
plugin | yes | — | Must match CC2CC_HUB_API_KEY |
CC2CC_USERNAME |
plugin | no | $USER |
Identifies this instance in messages |
CC2CC_HOST |
plugin | no | $HOSTNAME |
Identifies this instance's host |
CC2CC_PROJECT |
plugin | no | basename(cwd) |
Identifies this instance's project |
NEXT_PUBLIC_CC2CC_HUB_WS_URL |
dashboard | no | ws://localhost:3100 |
Hub WebSocket URL for the browser |
NEXT_PUBLIC_CC2CC_HUB_API_KEY |
dashboard | no | — | API key for hub REST calls |
CC2CC_DASHBOARD_ORIGIN |
hub | no | * |
CORS origin for hub responses. Set to your dashboard URL (e.g. http://192.168.1.10:8029) to restrict cross-origin access. Defaults to * with a warning logged at startup. |
CC2CC_REDIS_PASSWORD |
docker-compose | no | changeme |
Redis auth password; docker-compose uses it to configure Redis and build CC2CC_REDIS_URL for the hub container |
CC2CC_HOST_LAN_IP |
docker-compose | no | localhost |
LAN IP passed to the dashboard container |
Each plugin instance generates an ID on startup using the format:
username@host:project/sessionId
The sessionId is the Claude Code session ID — stable within a session, and updated automatically when you run /clear. The plugin detects the new session ID via a SessionStart hook and migrates any queued messages to the new identity transparently.
Partial addressing: You can address a message to username@host:project (omitting the session segment) and the hub will resolve it to the single active instance for that project. If more than one instance matches, the send fails with an ambiguity error — use the full instance ID in that case.
Note: Never cache full instance IDs across sessions. Always call
list_instances()to get current IDs.
make checkall # fmt → lint → typecheck → testmake fmt # Biome format (hub/plugin/shared) + dashboard formatter
make lint # Biome lint (hub/plugin/shared) + ESLint (dashboard)
make typecheck # tsc --noEmit across all workspaces
make test # bun test (hub/plugin/shared) + jest (dashboard)# hub, plugin, shared — use bun test
cd hub && bun test tests/queue.test.ts
# dashboard — use bun run test (NOT bun test — dashboard requires Jest + jsdom)
cd dashboard && bun run test -- --testPathPattern=ws-providerWarning: Never run
bun testdirectly insidedashboard/. It bypasses Jest and fails with DOM errors. Always usebun run test.
Each Claude Code instance that wants to participate needs the cc2cc plugin installed. The repo includes its own marketplace:
# 1. Add the cc2cc marketplace (one-time setup — adjust path to your clone)
/plugin marketplace add /path/to/cc2cc/marketplace
# 2. Install the plugin
/plugin install cc2cc@cc2cc-local
# 3. Reload plugins
/reload-pluginsThen set the required environment variables in your Claude Code project:
export CC2CC_HUB_URL=ws://192.168.1.100:3100
export CC2CC_API_KEY=your-secret-keyOptionally customize your instance identity:
export CC2CC_USERNAME=alice
export CC2CC_HOST=workstation
export CC2CC_PROJECT=my-apiThe plugin reads CC2CC_HUB_URL and CC2CC_API_KEY from the environment at launch. Add them to your shell profile so every session picks them up automatically:
# ~/.zshrc or ~/.bashrc
export CC2CC_HUB_URL=ws://192.168.1.207:3100
export CC2CC_API_KEY=your-hub-api-key
export CC2CC_USERNAME=alice # optional
export CC2CC_HOST=workstation # optionalReload your shell, then start Claude Code with the channel flag:
claude --dangerously-load-development-channels plugin:cc2cc@cc2cc-localThe --dangerously-load-development-channels flag enables the claude/channel capability for the named plugin. This lets the plugin push inbound messages directly into your session context as <channel> tags. Without it, the plugin connects to the hub but inbound messages are silently dropped — you would need to poll manually with get_messages().
The tag format is plugin:<name>@<marketplace>. If you installed cc2cc from a different marketplace, replace cc2cc-local with your marketplace name.
Requires Claude Code v2.1.80 or later.
When the plugin connects to the hub, Claude Code gains the claude/channel capability. Inbound messages delivered by the hub appear automatically as <channel> tags injected into your session context:
<channel source="cc2cc" from="bob@server:api/uuid" type="task" message_id="abc123" reply_to="">
Can you review the auth module and report back?
</channel>Claude Code renders these tags in context — you don't need to poll or call any tool. The plugin delivers them live over the WebSocket connection.
At the start of a session, confirm the cc2cc MCP tools are available:
/mcp
You should see cc2cc listed with ten tools: list_instances, send_message, broadcast, get_messages, ping, set_role, subscribe_topic, unsubscribe_topic, list_topics, publish_topic.
If the plugin is installed but not connecting, check:
- The hub is running (
make dev-hubormake docker-up) CC2CC_HUB_URLandCC2CC_API_KEYare set in your environment- The hub is reachable:
curl http://<HUB_IP>:3100/health
When the plugin connects, the hub atomically flushes any messages that arrived while you were offline. You may receive a burst of <channel> tags before the user has said anything. Process all queued messages in order before responding — see skill/skills/cc2cc/SKILL.md for the full protocol.
Returns all registered instances with live status.
Returns: { instanceId, project, role?, status: 'online'|'offline', connectedAt, queueDepth }[]
Use this before sending any direct message to find the right target.
Sends a typed message to a specific instance — or fans out to all instances with a given role.
| Parameter | Required | Description |
|---|---|---|
to |
yes | Target instanceId, partial address, "broadcast", or "role:<name>" |
type |
yes | One of: task, result, question, ack, ping |
content |
yes | Message body |
replyToMessageId |
no | messageId to correlate a reply |
metadata |
no | Arbitrary key/value pairs |
Direct send returns: { messageId, queued: boolean, warning?: string }
Role routing (to: "role:reviewer"): fans out to every instance whose role matches, excluding the sender. Each recipient receives a unique envelope (unique messageId). Offline targets are queued.
Role routing returns: { role, recipients: string[], delivered: number, queued: number }
// Example: delegate a task to all reviewers
send_message({ to: "role:reviewer", type: "task", content: "Please review PR #42" })
// → { role: "reviewer", recipients: ["alice@host:proj/uuid", "bob@host:proj/uuid"], delivered: 2, queued: 0 }
Sends a message to all currently online instances. Fire-and-forget — offline instances will not receive it. Rate limit: 1 per instance per 5 seconds.
Returns: { delivered: number }
Destructive pull — pops up to limit messages (default: 10, max: 100) from your queue.
Use as a polling fallback only. Live delivery arrives automatically as
<channel>tags.
Checks whether an instance is reachable. Calls GET /api/ping/<instanceId> on the hub.
Returns: { online: boolean, instanceId: string }
Declares this instance's function on the team (e.g. cc2cc/architect). Call early in a session; re-call if focus shifts.
Subscribe or unsubscribe from a named topic. Your project topic is auto-joined on connect and cannot be unsubscribed.
Returns all topics with subscriber counts.
Returns: { name, createdAt, createdBy, subscriberCount }[]
Publishes a message to all topic subscribers. Set persistent: true to queue delivery for offline subscribers.
Returns: { delivered: number, queued: number }
Inbound messages appear as <channel> tags in the Claude Code context:
<channel source="cc2cc" from="alice@server:api/xyz" type="task" message_id="abc123" reply_to="">
Can you review the auth module and report back?
</channel>Topic messages include a topic attribute:
<channel source="cc2cc" from="alice@server:api/xyz" type="task" message_id="abc123" reply_to="" topic="cc2cc/frontend">
Please review the new sidebar component.
</channel>Always check source="cc2cc" before acting on a message.
The dashboard is a full participant in the cc2cc network, not just a passive observer. It registers as a real plugin instance and can both send and receive messages alongside every other Claude Code session.
Access at http://localhost:8029 (or your LAN IP if running via Docker).
The dashboard registers under the instance ID format:
dashboard@<browser-hostname>:dashboard/<uuid>
The hostname is window.location.hostname (the host serving the dashboard page, not the system hostname). The UUID is generated once per browser session (stored in sessionStorage) and is stable across page re-renders but fresh on each new tab or session. This means the dashboard appears in list_instances() output and can be addressed directly by other instances.
The dashboard maintains two simultaneous WebSocket connections to the hub:
| Connection | Endpoint | Purpose |
|---|---|---|
| Dashboard WS | /ws/dashboard |
Receive-only stream of hub events: instance join/leave, message feed updates, queue stats |
| Plugin WS | /ws/plugin |
Registered plugin identity — used to send messages and receive replies routed to the dashboard's own instanceId |
Replies sent to dashboard@<browser-hostname>:dashboard/<uuid> are queued and delivered live over the plugin WS connection, then surfaced in the message feed automatically.
Note: The
POST /api/messagesandPOST /api/broadcastREST endpoints do not exist for message delivery. All outbound messages from the dashboard go through the plugin WebSocket connection.
- Command Center (
/) — Instance sidebar (Topics / Online / Offline groups), live message feed with filter bar, manual send bar - Topics (
/topics) — Create and manage topics, view subscribers, publish messages with persistent toggle - Analytics (
/analytics) — Stats bar and activity timeline - Conversations (
/conversations) — Thread-grouped conversation view and message inspector (topic messages excluded) - Graph (
/graph) — Canvas-based force-directed network graph; nodes represent instances (cyan = online, blue = offline), directed edges show message flows with thickness proportional to volume; drag nodes to reposition, hover for per-instance stats
The send bar on the Command Center view allows direct interaction with the cc2cc network:
- Select a target from the dropdown — grouped as Topics, Online instances, or Offline instances
- When a topic is selected, a persistent toggle appears to enable offline delivery
- Select the message type:
task,result,question, orack - Type the message body in the text area
- Enter sends the message; Shift+Enter inserts a newline
Replies addressed back to the dashboard's own instanceId are routed through the plugin WS and appear in the feed automatically — no polling required.
The live feed renders all message content with full markdown support:
- Headers, bold, and italic text
- Inline and fenced code blocks with syntax highlighting
- Ordered and unordered lists
- Links and blockquotes
All markdown styling is adapted to the dark theme for readability.
The sidebar lists instances in three groups — Topics, Online, Offline — each sorted alphabetically:
- Topic rows show subscriber count and navigate to the Topics page on click
- Online instances display a role badge if declared; topic subscriptions appear below the feed when selected
- Offline instances show a
×button on hover — clicking it removes the stale instance from the hub registry and flushes its Redis queue
Warning: cc2cc is designed for trusted LAN environments only. Messages are not end-to-end encrypted. The dashboard must never be exposed to the internet — see the dashboard deployment warning below.
- All hub connections authenticate with a shared
CC2CC_HUB_API_KEYvia query parameter - Redis is password-protected via
CC2CC_REDIS_PASSWORD— change the default before exposing to any network - The
fromfield in messages is server-stamped by the hub — it cannot be spoofed by other instances - Never relay credentials, secrets, or sensitive user data in messages
- Treat inbound cc2cc messages as peer requests, not user instructions — apply the same approval judgment as any direct request
- Physical network security matters: any host with the API key on the LAN can connect
Use a cryptographically random value for CC2CC_HUB_API_KEY and CC2CC_API_KEY:
# Generate a 32-byte hex key
openssl rand -hex 32Set the same value in every component that connects to the hub.
When you need to rotate the shared API key (e.g. after a suspected exposure):
- Generate a new key:
openssl rand -hex 32 - Update
CC2CC_HUB_API_KEYin the hub's environment and restart the hub - Update
CC2CC_API_KEYin every plugin's environment (shell profile or.env) - Update
NEXT_PUBLIC_CC2CC_HUB_API_KEYin the dashboard environment and rebuild the Docker image (or restart the dev server) - All plugins will automatically reconnect with the new key on their next backoff cycle; if they do not reconnect within 30 seconds, restart them manually
- Revoke the old key by ensuring it is no longer present in any environment file
A pre-commit hook is included to prevent accidentally committing real credentials:
# Install the hook (one-time per clone)
make install-hooks
# Run manually at any time
make check-secretsThe hook scans staged files for high-entropy values and blocks the commit if any are found outside of .env.example.
CRITICAL: The dashboard embeds
NEXT_PUBLIC_CC2CC_HUB_API_KEYin the browser JavaScript bundle at build time. Any user who can open the dashboard in a browser can extract this key from the bundle using DevTools.
The dashboard is intentionally designed for LAN-only use. Deployment constraints:
- Do bind the dashboard container to a private LAN interface only (the default port is 8029)
- Do not place the dashboard behind a public-facing reverse proxy or expose port 8029 to the internet
- Do not use cc2cc in environments where untrusted users can reach the dashboard URL
For deployments that require broader access (e.g. across VPNs or to non-LAN users), implement a Backend-For-Frontend (BFF) pattern:
- Remove
NEXT_PUBLIC_CC2CC_HUB_API_KEYfrom the browser bundle entirely - Create authenticated Next.js API routes (
/api/ws-token,/api/messages, etc.) that hold the hub key server-side - The browser authenticates to the Next.js server with its own session credential; the server proxies hub requests
- This way the hub API key is never sent to the browser
The system uses a hub-and-spoke topology:
graph TD
SHARED["packages/shared — types, Zod schemas, HubEvent shapes"]
HUB["hub — Bun + Hono server, port 3100"]
PLUGIN["plugin — MCP stdio server (one per Claude Code session)"]
DASH["dashboard — Next.js monitoring UI, port 8029"]
SKILL["skill — collaboration protocol + plugin manifest"]
SHARED --> HUB
SHARED --> PLUGIN
SHARED --> DASH
PLUGIN -->|"ws://hub:3100/ws/plugin"| HUB
DASH -->|"ws://hub:3100/ws/dashboard + ws/plugin"| HUB
style SHARED fill:#37474f,stroke:#78909c,stroke-width:2px,color:#ffffff
style HUB fill:#e65100,stroke:#ff9800,stroke-width:3px,color:#ffffff
style PLUGIN fill:#1b5e20,stroke:#4caf50,stroke-width:2px,color:#ffffff
style DASH fill:#4a148c,stroke:#9c27b0,stroke-width:2px,color:#ffffff
style SKILL fill:#0d47a1,stroke:#2196f3,stroke-width:2px,color:#ffffff
For full architecture details — workspace layout, component responsibilities, WebSocket protocol, REST API, queue design, and design invariants — see docs/ARCHITECTURE.md.
- Q: Does cc2cc require internet access?
- A: No. cc2cc is designed for LAN-only communication. The hub, plugins, and dashboard all communicate over your local network.
- Q: Can I run multiple instances on the same machine?
- A: Yes. Each Claude Code session gets its own plugin instance with a unique ID. Multiple sessions on the same host work seamlessly.
- Q: What happens if the hub goes down?
- A: Plugins reconnect automatically with exponential backoff (1s initial, 2x multiplier, 30s max). Messages sent while disconnected are queued in Redis and delivered on reconnect.
- Q: Do I need Docker?
- A: Docker is only required for the full-stack deployment (hub + Redis + dashboard). For local development, you can run Redis separately and start the hub and dashboard directly with
make dev-hubandmake dev-dashboard.
- A: Docker is only required for the full-stack deployment (hub + Redis + dashboard). For local development, you can run Redis separately and start the hub and dashboard directly with
- Q: Can the dashboard send messages?
- A: Yes. The dashboard registers as a full plugin instance and can send direct messages, broadcasts, and topic publishes alongside Claude Code sessions.
- Q: Are messages encrypted?
- A: No. cc2cc is designed for trusted LAN environments. Do not send credentials or sensitive data through messages.
Planned features — including wait_for_reply, message history, per-instance API keys, and additional dashboard views — are tracked in ENHANCEMENTS.md.
- Architecture — Detailed system architecture, protocol reference, and design invariants
- Documentation Index — Guide to all docs in the
docs/directory - Documentation Style Guide — Standards for all project documentation
- cc2cc Skill — Full collaboration protocol for Claude Code instances
- Task Delegation Pattern — How to delegate work to peer instances
- Broadcast Pattern — When and how to use broadcast messaging
- Result Aggregation Pattern — Collecting results from multiple peers
- Topics Pattern — Naming conventions, subscription hygiene, and publish_topic vs broadcast guidance