A communication layer for orchestrating multiple Claude Code terminals on one machine. An attention multiplexer.
- See all terminals:
/statusshows every Claude Code session, what they're working on - Relay approvals: When Terminal 2 blocks on a permission prompt, Terminal 1 gets a notification — press
1to approve remotely - Send tasks: "Ask #CleverGrove to list the folders" routes work between terminals
- Persistent identity: Close a terminal, reopen it — it reconnects to its slot, pending messages deliver
- Zero context cost: Approval notifications use keystroke interception at the pty layer, not the conversation
git clone https://github.com/DrishtantKaushal/AgentCommons.git
cd AgentCommons
# Build the Go binary
go build -o commons .
# Build the MCP server (TypeScript)
cd mcp && npm install && npm run build && cd ..# Option A: Copy to a directory already on your PATH
sudo cp commons /usr/local/bin/
# Option B: Or add the repo directory to your PATH
export PATH="$PATH:$(pwd)"macOS users: If the binary gets killed on first run, macOS Gatekeeper is blocking it. Fix with:
# Build directly to PATH (avoids quarantine)
go build -o /usr/local/bin/commons .
# Then codesign it
codesign --sign - --identifier "com.agentcommons.commons" --force /usr/local/bin/commonscommons installThis does three things:
- Creates
~/.commons/directory with a SQLite database - Writes a default config at
~/.commons/config.toml - Registers the MCP server in Claude Code's settings (
~/.claude/settings.local.json)
Verify it worked:
cat ~/.claude/settings.local.json # Should show "commons" MCP server entryOpen two separate terminal windows and run in each:
# Terminal 1
commons run claude
# Terminal 2
commons run claudeEach terminal gets an auto-assigned name (e.g., "DawnSentry", "CleverGrove"). You'll see the name printed at the top. The daemon auto-launches on first connection — no manual setup needed.
In either terminal, type:
/status
You should see both terminals listed with their names, state, and working directory.
In Terminal 1, ask Claude to delegate work using #Name:
Can we ask #CleverGrove to list all folders in the current directory?
(Replace #CleverGrove with Terminal 2's actual name from /status)
Terminal 2 receives the task, executes it autonomously, and sends results back.
In Terminal 1:
Ask #CleverGrove to run ls -la in the home directory
When Terminal 2 hits the "Do you want to proceed?" permission prompt:
- Terminal 1's title bar shows the approval request
- Press
1to approve,2to deny, or3to dismiss - Terminal 2 automatically proceeds
| Problem | Solution |
|---|---|
commons: command not found |
Add the binary to your PATH (see step 2) |
killed on macOS |
Codesign the binary (see step 2, macOS note) |
/status shows no agents |
Both terminals must use commons run claude, not just claude |
| MCP server not connecting | Run commons install again, then restart Claude Code |
| Daemon won't start | Check if port 7390 is in use:lsof -i :7390 |
| Terminal name collision | Use commons run claude --name MyCustomName |
┌─────────────┐ ┌─────────────┐
│ Terminal 1 │ │ Terminal 2 │
│ ┌─────────┐ │ │ ┌─────────┐ │
│ │Claude │ │ │ │Claude │ │
│ │Code │ │ │ │Code │ │
│ └────┬────┘ │ │ └────┬────┘ │
│ ┌────┴────┐ │ │ ┌────┴────┐ │
│ │PTY │ │ │ │PTY │ │
│ │Wrapper │ │ │ │Wrapper │ │
│ └────┬────┘ │ │ └────┬────┘ │
│ ┌────┴────┐ │ │ ┌────┴────┐ │
│ │MCP │ │ │ │MCP │ │
│ │Server │ │ │ │Server │ │
│ └────┬────┘ │ │ └────┬────┘ │
└──────┼──────┘ └──────┼──────┘
│ WebSocket │
└────────┬──────────┘
┌──────┴─────┐
│ Daemon │
│ localhost │
│ :7390 │
│ ┌───────┐ │
│ │SQLite │ │
│ └───────┘ │
└────────────┘
Daemon — Background Go process on localhost:7390. Manages slots, routes messages, tracks liveness.
PTY Wrapper — commons run claude wraps Claude Code in a pty. Detects approval prompts via regex, injects keystrokes remotely, intercepts notification responses.
MCP Server — TypeScript process spawned by Claude Code. Provides tools (commons_status, commons_push, commons_approve, etc.) and pushes notifications via channel protocol.
SQLite — Persistent storage at ~/.commons/commons.db. Slots survive session restarts.
The MCP server exposes these tools to Claude Code, enabling inter-terminal communication:
| Tool | Description | Example trigger |
|---|---|---|
commons_status |
List all agent terminals, their state, and working directory | "What terminals are running?", /commons:status |
commons_push |
Send a message or task to another terminal by name | "Ask #CleverGrove to list the folders" |
commons_inbox |
Check pending notifications and messages from other terminals | "Any updates?", /commons:inbox |
commons_approve |
Approve a pending permission request for another terminal | "Approve #CleverGrove", /commons:approve |
commons_deny |
Deny a pending permission request | "Deny #CleverGrove" |
commons_history |
Show message history between terminals | "What was sent to #CleverGrove?" |
commons_report_state |
Report this agent's state change to the daemon | Used internally for state tracking |
Claude Code invokes these automatically based on natural language. When you say "ask #CleverGrove to check the auth module", Claude calls commons_push(target="CleverGrove", message="check the auth module").
| Command | Description |
|---|---|
commons run claude |
Launch Claude Code with approval relay |
commons run claude --name MyAgent |
Launch with a specific name |
commons install |
One-time setup |
commons server start |
Manually start daemon (auto-launches normally) |
commons server stop |
Stop daemon |
commons server status |
Daemon health check |
commons status |
List all terminals |
/commons:status— See all terminals/commons:inbox— Check pending notifications/commons:approve @Name— Approve a blocked terminal/deny @Name— Deny a request
- Terminal 2 runs a bash command -> Claude Code shows "Do you want to proceed?"
- The PTY wrapper detects the prompt via regex
- Broadcasts an approval request to the daemon
- Terminal 1's wrapper receives it -> shows notification in title bar
- User presses
1-> wrapper intercepts keystroke, sends approval to daemon - Daemon routes to Terminal 2's wrapper -> injects
1keystroke into the pty - Claude Code on Terminal 2 proceeds
Zero context pollution — the keystroke never enters either terminal's conversation.
sequenceDiagram
participant U1 as User (Terminal 1)
participant W1 as PTY Wrapper 1
participant D as Daemon :7390
participant W2 as PTY Wrapper 2
participant C2 as Claude Code (Terminal 2)
U1->>W1: "Ask #Terminal2 to run build"
Note over W1,D: Task routed via MCP push (see below)
C2->>C2: Runs bash command
C2->>W2: stdout: "Do you want to proceed?"
W2->>W2: Regex detector matches prompt
W2->>W2: Compute SHA-256 hash of prompt
W2->>D: approval_request {prompt_hash, prompt_text}
D->>D: Store in SQLite, set state to blocked_on_approval
D->>W1: approval_broadcast to all other wrappers
Note over U1,W1: Title bar notification appears
U1->>W1: Presses "1"
W1->>W1: Intercepts keystroke (not sent to Claude)
W1->>D: approval_response {action: "approve", prompt_hash}
D->>W2: approval_granted {prompt_hash}
W2->>W2: Verify hash matches pending prompt
W2->>C2: Inject "1" keystroke into pty master fd
C2->>C2: Proceeds with execution
User mentions a terminal name with # prefix. The MCP server resolves the name and routes through the daemon.
- User says
#CleverGrove list the foldersin Terminal 1 - Claude Code calls the
commons_pushMCP tool - MCP server sends
push_messageto the daemon via WebSocket - Daemon resolves
#CleverGroveto a slot, stores the message in SQLite - If the target slot has an active session, daemon delivers via WebSocket
- Target terminal's MCP server receives the push and delivers it to Claude via channel protocol
- Claude on Terminal 2 executes the task
sequenceDiagram
participant U as User (Terminal 1)
participant C1 as Claude Code 1
participant M1 as MCP Server 1
participant D as Daemon :7390
participant DB as SQLite
participant M2 as MCP Server 2
participant C2 as Claude Code 2
U->>C1: "#CleverGrove list the folders"
C1->>M1: commons_push(target="CleverGrove", message="list folders", type="task")
M1->>D: push_message {target_slot_name, content, message_type}
D->>DB: INSERT message (persists for offline delivery)
D->>D: Resolve slot name, find active session
alt Target is online
D->>M2: message_push {from_slot_name, content, message_type}
M2->>C2: Channel push notification
C2->>C2: Executes "list the folders"
else Target is offline
Note over DB: Message queued as "pending"
Note over DB: Delivered on next session bootstrap
end
Slots are persistent identities that survive terminal restarts. Sessions are ephemeral processes that bind to slots.
stateDiagram-v2
[*] --> Registration: commons run claude
Registration --> NewSlot: Name not found in DB
Registration --> ReclaimSlot: Name exists, no active session
NewSlot --> Active: INSERT slot + session, bind session to slot
ReclaimSlot --> Active: Create new session, bind to existing slot
Active --> Active: Heartbeat every 10s
Active --> BlockedOnApproval: Approval prompt detected
BlockedOnApproval --> Active: Approval granted/denied
Active --> Inactive: Terminal closed (clean deregister)
Active --> Inactive: Process crashed (reaper detects stale heartbeat)
Inactive --> Inactive: Slot persists in SQLite with last_cwd snapshot
Note right of Inactive: Pending messages queue here
Inactive --> ReclaimSlot: Terminal reopens with same name
state Active {
[*] --> Idle
Idle --> Working: Claude executing task
Working --> Idle: Task complete
}
FSL-1.1-Apache-2.0 (Functional Source License). Free to use, modify, and redistribute for any purpose except competing with AgentCommons as a commercial product. Automatically converts to Apache 2.0 on 2028-03-23.

