Remote control Claude Code sessions on your Mac from your iPhone.
CClaw connects to the Claude Code Agent SDK running on your Mac, letting you start conversations, view streaming responses, manage tool permissions, and handle multiple sessions — all from your phone.
+──────────────+ +──────────────────+ +──────────────────+ +────────────+
│ iOS App │── WSS ──▷│ Relay Server │◁── WSS ──│ Bridge Server │─────────▷│ Claude Code│
│ (SwiftUI) │ │ (Cloud VPS) │ │ (Your Mac) │ SDK API │ Agent SDK │
+──────────────+ +──────────────────+ +──────────────────+ +────────────+
| Component | Tech | Role |
|---|---|---|
| iOS App | SwiftUI, iOS 18+ | Chat UI, session management, QR/code pairing |
| Relay Server | Node.js + TypeScript | Cloud relay for NAT traversal, JWT auth, APNS push |
| Bridge Server | Node.js + TypeScript | Runs on your Mac, wraps Claude Code Agent SDK |
The relay server is a transparent message forwarder — it does not inspect or modify message content (E2E encryption is planned).
- Multi-session management — run multiple Claude Code conversations simultaneously
- Real-time streaming — see Claude's output as it types, including extended thinking
- Tool use visualization — file edits, bash commands, and other tool calls with colored indicators
- Permission handling — approve or deny tool use requests from your phone
- Markdown rendering — full GFM with syntax highlighting (Swift, TypeScript, Python, Bash)
- QR code pairing — one-time setup, then automatic reconnection via persistent
bridgeId - Message buffering — up to 500 messages / 5MB queued when the app is offline
- APNS push notifications — notified when Claude finishes while the app is backgrounded
- File transfer — upload files from iPhone to Mac, download files from Mac to iPhone
- MCP server management — add/remove/monitor Model Context Protocol servers
- Skills system — manage Claude Code skills (
~/.claude/skills/) - Usage tracking — view token usage, costs, and API rate limits
- Local mode — connect directly over LAN without a relay server
- Auto-start on login — launchd integration for the bridge server on macOS
| Component | Requirements |
|---|---|
| Relay Server | Any VPS with a public IP, Node.js 18+, a domain name (optional but recommended) |
| Bridge Server | macOS with Claude Code installed and authenticated, Node.js 18+ |
| iOS App | Xcode 15+, iOS 18+, Apple Developer account (free or paid) |
The relay server runs on a cloud VPS (any provider: AWS, GCP, Alibaba Cloud, DigitalOcean, etc.).
git clone https://github.com/Tomsabay/cclaw.git
cd cclaw/relay-server
npm install
npx tscThe relay server requires TLS. You can use a self-signed cert or a proper one from Let's Encrypt.
Self-signed (quickest):
cd certs
openssl req -x509 -newkey rsa:2048 \
-keyout key.pem -out cert.pem \
-days 365 -nodes \
-subj "/CN=your-server-ip-or-domain"
cd ..Let's Encrypt (recommended for production):
# Install certbot, then:
sudo certbot certonly --standalone -d relay.yourdomain.com
# Copy certs to relay-server/certs/
cp /etc/letsencrypt/live/relay.yourdomain.com/privkey.pem certs/key.pem
cp /etc/letsencrypt/live/relay.yourdomain.com/fullchain.pem certs/cert.pemcp .env.example .envEdit .env:
PORT=3200 # Listen port
JWT_SECRET=your-random-secret-here # REQUIRED — used to sign JWT tokens
DATABASE_PATH=./relay-usage.db # SQLite database path
FREE_MESSAGE_LIMIT=500 # Monthly message limit for free tier
# ADMIN_SECRET=your-admin-secret # Optional — needed for admin API endpoints
JWT_SECRETis required. The server will refuse to start without it. Generate one with:openssl rand -hex 32
# Example for ufw:
sudo ufw allow 3200/tcp
# Example for firewalld:
sudo firewall-cmd --permanent --add-port=3200/tcp && sudo firewall-cmd --reload
# Example for Alibaba Cloud / AWS:
# Open port 3200 (TCP) in the security group rules via the web consoleQuick start:
node dist/index.jsProduction (with PM2):
npm install -g pm2
pm2 start dist/index.js --name relay-server
pm2 save
pm2 startup # Auto-start on bootProduction (with systemd):
sudo tee /etc/systemd/system/cclaw-relay.service > /dev/null <<EOF
[Unit]
Description=CClaw Relay Server
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/cclaw/relay-server
ExecStart=/usr/bin/node dist/index.js
Restart=always
RestartSec=5
EnvironmentFile=/root/cclaw/relay-server/.env
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable cclaw-relay
sudo systemctl start cclaw-relay# From your local machine:
curl -k https://your-server-ip:3200/health
# Expected: {"status":"ok","totalRooms":0,"paired":0,"waitingForPair":0}curl -k -X POST https://your-server-ip:3200/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"your-password-min-8-chars"}'
# Returns: {"token":"eyJ...","email":"you@example.com","plan":"free"}The bridge server runs on your Mac and connects Claude Code to the relay.
RELAY_URL="wss://your-server-ip:3200" bash <(curl -sSL https://your-server-ip:3200/install.sh)This will:
- Install Node.js via Homebrew (if not present)
- Clone the repo to
~/.cclaw-bridge/ - Build the bridge server
- Prompt for your relay URL, email, and password
- Create a macOS launchd service (auto-start on login)
- Display a QR code and 6-character pair code
git clone https://github.com/Tomsabay/cclaw.git
cd cclaw/bridge-server
npm install
npx tsc
cp .env.example .envEdit .env:
RELAY_URL=wss://your-server-ip:3200
BRIDGE_EMAIL=you@example.com
BRIDGE_PASSWORD=your-passwordStart:
node dist/index.jsThe bridge server will:
- Log in to the relay server
- Register and receive a pair code (6 characters, displayed in the terminal)
- Display a QR code for scanning
- Wait for the iOS app to pair
Bridge ID is saved to
~/.cclaw-bridge-id. On restart, the bridge reclaims the same room — no re-pairing needed.
# View logs
tail -f ~/.cclaw-bridge/bridge.log
# Stop (launchd)
launchctl bootout gui/$(id -u)/com.cclaw.bridge-server
# Start (launchd)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.cclaw.bridge-server.plist
# Uninstall completely
launchctl bootout gui/$(id -u)/com.cclaw.bridge-server 2>/dev/null
rm -rf ~/.cclaw-bridge ~/Library/LaunchAgents/com.cclaw.bridge-server.plist ~/.cclaw-bridge-id ~/.cclaw-bridge-token- Open
zsffirstapp.xcodeprojin Xcode - Select the zsffirstapp target
- Under Signing & Capabilities:
- Set Team to your Apple Developer account
- The bundle identifier will auto-generate, or set it to something like
com.yourname.cclaw
- Connect your iPhone and select it as the build destination
- Build and Run (Cmd+R)
- Open CClaw on your iPhone
- Go to the Settings tab
- Enter your relay server URL:
wss://your-server-ip:3200 - Enter your email and password (same as registered in Step 1.7)
- Tap Connect
- Enter the pair code displayed by the bridge server (or scan the QR code)
- Go to the Sessions tab
- Tap + to create a new session
- Choose a working directory (defaults to
~) - Start typing — your prompts go to Claude Code on your Mac
Bridge Server Relay Server iOS App
│ │ │
│── relay.register ──────────────▷│ │
│◁── relay.registered ───────────│ (pair code: "AB3K") │
│ │ │
│ │◁── relay.join ───────────────│ (user enters "AB3K")
│◁── relay.paired ──────────────│ │
│ │── relay.paired ──────────────▷│ (includes bridgeId)
│ │ │
│◁══ messages forwarded both ways ══════════════════════════════▷│
- Bridge sends
relay.registerwith its persistentbridgeId - Relay creates a room, generates a 6-character pair code, responds with
relay.registered - User enters the pair code in the iOS app, which sends
relay.join - Relay links both WebSocket connections and sends
relay.pairedto both sides - The
bridgeIdis saved by the app — on subsequent connections, it reconnects automatically without a pair code - If the pair code expires (5 minutes), the relay auto-generates a new one and notifies the bridge
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET |
Yes | — | Secret for JWT token signing. Server exits if missing. |
PORT |
No | 3200 |
HTTPS/WSS listen port |
DATABASE_PATH |
No | ./relay-usage.db |
SQLite database file path |
FREE_MESSAGE_LIMIT |
No | 500 |
Monthly message limit per free user |
ADMIN_SECRET |
No | — | Secret for admin API endpoints (users/ban/plans) |
APNS_KEY_PATH |
No | — | Path to Apple Push Notification .p8 key file |
APNS_KEY_ID |
No | — | APNs Key ID from Apple Developer portal |
APNS_TEAM_ID |
No | — | Apple Developer Team ID for push notifications |
APNS_BUNDLE_ID |
No | com.cclaw.app |
App bundle ID for push notifications |
| Variable | Required | Default | Description |
|---|---|---|---|
RELAY_URL |
Yes* | — | WebSocket URL of your relay server (e.g. wss://relay.example.com:3200) |
BRIDGE_EMAIL |
Yes* | — | Email for relay server authentication |
BRIDGE_PASSWORD |
Yes* | — | Password for relay server authentication |
PORT |
No | 3100 |
Local WebSocket listen port (used in local mode) |
*Required for relay mode. If RELAY_URL is not set, the bridge runs in local mode (LAN-only, no relay needed).
cclaw/
├── zsffirstapp/ # iOS App (SwiftUI)
│ ├── zsffirstappApp.swift # App entry point, auto-connect logic
│ ├── Services/
│ │ ├── WebSocketService.swift # WebSocket connection (relay + local mode)
│ │ ├── SessionManager.swift # Session & message management
│ │ ├── SettingsStore.swift # UserDefaults persistence
│ │ ├── AuthService.swift # Relay authentication
│ │ ├── KeychainHelper.swift # iOS Keychain for credentials
│ │ └── PersistenceManager.swift # Local data persistence
│ ├── Views/
│ │ ├── Chat/
│ │ │ ├── ChatView.swift # Main chat interface
│ │ │ ├── ChatBubbleView.swift # Message bubbles
│ │ │ ├── MarkdownView.swift # Markdown + syntax highlighting
│ │ │ ├── ToolUseView.swift # Tool call visualization
│ │ │ ├── MessageInputView.swift
│ │ │ └── PermissionBannerView.swift
│ │ ├── Sessions/
│ │ │ ├── SessionListView.swift
│ │ │ └── NewSessionView.swift
│ │ ├── Settings/
│ │ │ ├── SettingsView.swift # Server URL, auth, pair code
│ │ │ ├── QRScannerView.swift # QR code scanner for pairing
│ │ │ └── ConnectionStatusView.swift
│ │ ├── MCP/ # MCP server management
│ │ ├── Skills/ # Skills browser
│ │ ├── Usage/ # Usage & cost tracking
│ │ └── MainTabView.swift # Tab navigation
│ └── Models/
│ ├── Session.swift
│ ├── ChatMessage.swift
│ └── BridgeProtocol.swift # Message type definitions
│
├── bridge-server/ # Mac Bridge Server
│ ├── src/
│ │ ├── index.ts # Entry point, WS server, message routing
│ │ ├── relay-client.ts # Relay connection, reconnection, buffering
│ │ ├── claude-session.ts # Claude Code Agent SDK wrapper
│ │ ├── session-manager.ts # Session lifecycle management
│ │ ├── protocol.ts # Message type definitions
│ │ ├── auth.ts # Local auth token management
│ │ ├── db.ts # SQLite session persistence
│ │ ├── mcp-manager.ts # MCP server configuration
│ │ ├── skills.ts # Skills management
│ │ └── file-handler.ts # File upload/download
│ ├── install.sh # One-line installer script
│ ├── .env.example
│ ├── package.json
│ └── tsconfig.json
│
├── relay-server/ # Cloud Relay Server
│ ├── src/
│ │ ├── index.ts # HTTPS + WSS server, HTTP routes
│ │ ├── relay.ts # Room pairing, message forwarding, buffering
│ │ ├── auth.ts # JWT token creation/verification (HS256)
│ │ ├── userService.ts # User registration, bcrypt passwords, plans
│ │ ├── apns.ts # Apple Push Notification Service
│ │ ├── pair-code.ts # Cryptographic pair code generation
│ │ ├── usage.ts # Monthly message usage tracking
│ │ ├── admin.ts # Admin API handlers
│ │ └── db.ts # SQLite database init
│ ├── certs/ # TLS certificates (gitignored)
│ │ └── README.md # Certificate generation instructions
│ ├── .env.example
│ ├── package.json
│ └── tsconfig.json
│
├── Info.plist # iOS app transport security config
├── LICENSE # MIT
└── README.md
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
None | Server health check |
POST |
/auth/register |
None | Register new user (rate limited: 5/min) |
POST |
/auth/login |
None | Login, get JWT (rate limited: 10/min) |
GET |
/user/info?token=<JWT> |
JWT | Get user info (email, plan, usage) |
POST |
/admin/set-plan |
Admin | Set user plan type |
POST |
/admin/ban |
Admin | Ban/unban user |
GET |
/admin/users |
Admin | List all users |
| Path | Used by | Description |
|---|---|---|
/bridge?token=<JWT> |
Bridge Server | Mac bridge connection |
/app?token=<JWT> |
iOS App | iPhone app connection |
If you don't want to set up a relay server, the bridge can run in local mode:
- Don't set
RELAY_URLin.env - Start the bridge server:
node dist/index.js - It will display a QR code with local connection info
- Your iPhone must be on the same Wi-Fi network as your Mac
- In the iOS app, scan the QR code or enter the local address manually
Limitations of local mode:
- No access when you're away from home
- No push notifications
- No message buffering when the app is offline
| Layer | Mechanism |
|---|---|
| Transport | All WebSocket connections use TLS (WSS) |
| Authentication | JWT tokens (HS256, 30-day expiry) for all connections |
| Passwords | bcrypt hashed (12 salt rounds) on the relay server |
| Credentials | Stored in iOS Keychain on device |
| Local auth | Cryptographically random 256-bit bearer token for local connections |
| Rate limiting | IP-based sliding window on auth endpoints |
| Message size | 20MB max WebSocket message, 4KB max HTTP body |
Current limitations:
- The relay server can read all message content in transit (it forwards raw JSON)
- E2E encryption is planned — ECDH key exchange + AES-GCM between app and bridge
- Self-signed TLS certificates require the iOS app to trust all certificates (acceptable for self-hosted relays)
Set the JWT_SECRET environment variable on the relay server. This is required.
- Check that the relay server is running:
curl -k https://your-server-ip:3200/health - Check firewall: port 3200 must be open
- Check
.envcredentials are correct - Check
RELAY_URLformat: must bewss://(nothttps://)
- Verify the relay URL in Settings matches your server (including port)
- Ensure you registered an account on the relay and entered correct credentials
- If using self-signed certs, the app handles this automatically
Pair codes expire after 5 minutes. The bridge auto-generates a new one — check the bridge server logs for the latest code.
Another device/instance is already paired. The new connection will take over automatically.
- Check bridge server logs:
tail -f ~/.cclaw-bridge/bridge.log - Check relay health:
curl -k https://your-server-ip:3200/health - Ensure both bridge and app show "paired" status
Contributions are welcome! Please open an issue or pull request.
Areas where help is needed:
- E2E encryption — ECDH + AES-GCM between iOS app and bridge server
- Android client — native Android app or React Native
- Web client — browser-based interface
- Better TLS — Let's Encrypt integration / ACME in the relay server
- Tests — unit and integration tests for all components