Architecture
remote-cli has three components:
- Relay
- Agent
- PWA
Claude Code runs as a local subprocess on each agent machine.
Topology
Phone / PWA ----\
\
Laptop agent -----\
Desktop agent ------> Relay
Pi agent -----/All connections are outbound to the relay.
Relay
The relay is a Go server.
It handles:
- HTTP API routes
- WebSocket connections
- admin login
- JWT validation
- pairing codes
- device registry
- offline/online/busy presence
- session routing
- SQLite persistence
- serving the PWA
Persistent state:
- admin password hash
- device records
- device token hashes
- last-seen timestamps
In-memory state:
- active phone connections
- active agent connections
- session-to-device mappings
- session-to-phone-owner mappings
- tool-use-to-session mappings
- busy device IDs
- pairing codes
Agent
The agent is a Go CLI.
Commands:
remote-cli pair --relay <url>
remote-cli pair --relay <url>
remote-cli run
remote-cli status
remote-cli unpair
remote-cli service install
remote-cli service uninstall
remote-cli service start
remote-cli service stop
remote-cli service logsThe agent stores config at:
~/.config/remote-cli/config.tomlWhen running, it:
- opens a WebSocket to the relay
- sends
device.register - waits for session messages
- starts Claude Code when requested
- streams Claude output back to the relay
PWA
The PWA is a React/Vite app served by the relay.
It handles:
- login
- WebSocket auth
- device list
- add device / pairing
- chat view
- streamed assistant output
- tool event display
In production, users open the relay URL in a browser. There is no separate PWA deployment.
Pairing Flow
Agent -> Relay: open /ws/agent
Relay -> Agent: connected(connection_id)
Agent -> Relay: POST /api/pair/request(connection_id)
Relay -> Agent: pair.code(code, url)
Phone -> Relay: POST /api/pair/redeem(code)
Relay -> Agent: pair.complete(device_id, device_token)
Agent: save configPairing codes:
- are 6 digits
- expire after 5 minutes
- are single-use
- are bound to the requesting agent WebSocket connection
Session Flow
PWA -> Relay: session.start(device_id)
Relay: reject if device is offline or busy
Relay: create session_id
Relay: map session_id -> device_id
Relay: mark device busy
Relay -> Agent: session.start(session_id)
Agent: spawn claude
Agent -> Relay -> PWA: session.started
PWA -> Relay -> Agent: message.user
Agent -> Claude: stdin stream-json message
Claude -> Agent: stdout stream-json events
Agent -> Relay -> PWA: message.assistant_chunkWhen the session ends, the relay removes the session mapping, marks the device online if the agent is still connected, and forwards session.ended to the phone.
Multi-Device Routing
Each agent registers with a unique device_id.
The relay stores:
device_id -> active agent connection
session_id -> device_idWhen the PWA sends a user message, it includes only session_id. The relay uses the session map to find the right device and forwards the message to that agent.
Claude Integration
The agent starts:
claude \
--input-format stream-json \
--output-format stream-json \
--verbose \
--include-partial-messages \
--no-session-persistence \
--permission-prompt-tool mcp__approval__request_permission \
--mcp-config /tmp/remote-cli-mcp-<session-id>.jsonClaude emits JSON lines. The agent parses those lines and converts them to remote-cli protocol messages.
For partial assistant messages, Claude emits cumulative text. The agent tracks the previous text per message ID and forwards only the new delta.
Tool Approval
When Claude wants to run a tool it requires permission for, it calls the request_permission MCP tool instead of prompting the terminal.
The MCP server is a subprocess of the agent binary (remote-cli mcp-server --socket <path>). It connects to a Unix socket (the permission bridge) running inside the agent.
Flow:
Claude -> MCP server subprocess: tools/call request_permission
MCP server -> Unix socket (bridge): bridgeRequest{tool_use_id, tool_name, tool_input}
Bridge -> Relay -> PWA: tool_use.request(awaiting_approval=true)
PWA: show Allow / Deny buttons
User taps Allow -> PWA -> Relay -> Agent: tool_use.approve
User taps Deny -> PWA -> Relay -> Agent: tool_use.deny
Bridge -> MCP server: bridgeResponse{allow: true/false}
MCP server -> Claude: {"behavior":"allow"} or {"behavior":"deny","message":"..."}If the bridge fails to start (e.g. socket error), the session falls back to auto-approval.
Disconnects
Phone disconnect:
- PWA reconnects automatically.
- Current in-memory UI state may be lost.
Agent disconnect:
- relay marks device offline
- active sessions for that device are ended
- agent reconnects when
remote-cli runis still active - if the agent is installed as a service, the service manager restarts it after failures
Relay restart:
- persistent device/admin state survives
- live sessions, pairing codes, and WebSocket connections are lost
Current Limitations
- No persisted chat history.
- No end-to-end encryption.
- One active session per agent.
- No
remote-cli service statuscommand yet.