Skip to content

Architecture

remote-cli ships as one unified CLI with three runtime components:

  1. Relay
  2. Agent
  3. PWA

Claude Code runs as a local subprocess on each agent machine.

Topology

text
Phone / PWA  ----\
                  \
Laptop agent  -----\
Desktop agent ------> Relay
Pi agent      -----/

All connections are outbound to the relay.

Unified CLI

The public binary is remote-cli.

It is installed through npm, downloaded from GitHub Releases, or built from source:

bash
go build -o remote-cli ./cmd/remote-cli

The CLI uses Cobra and exposes relay, agent, service, setup, doctor, and version commands.

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 embedded 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:

bash
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 status
remote-cli service logs

The agent stores config at:

text
~/.config/remote-cli/agent.toml

Older ~/.config/remote-cli/config.toml files are migrated automatically.

When running, it:

  1. opens a WebSocket to the relay
  2. sends device.register
  3. waits for session messages
  4. starts Claude Code when requested
  5. 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.

Release builds embed the built PWA into the Go binary using embed.FS. Development builds can serve an external static directory with:

bash
remote-cli relay start --static-dir pwa/dist

Pairing Flow

text
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 config

Pairing codes:

  • are 6 digits
  • expire after 5 minutes
  • are single-use
  • are bound to the requesting agent WebSocket connection

Session Flow

text
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_chunk

When 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:

text
device_id -> active agent connection
session_id -> device_id

When 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:

bash
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>.json

Claude 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:

text
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 run is 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.

Released under the MIT License.