Remote control plane for local Claude Code sessions. See the design spec.
Status: chat routing live — PWA can chat with Claude in both directions (PWA composer → channel injection; Claude reply tool → PWA chat panel). Plan 16 + plugin MCP rework + chat-routing all green.
- Bun ≥ 1.1
- macOS or Linux
In four terminals plus a one-shot pairing step:
# 0. Install
bun install
# 1. Start fake-IAS (terminal A) — substitute real SAP IAS in production
FAKE_IAS_PORT=17770 bun tools/fake-ias/fake-ias.ts
# → fake-ias listening at http://localhost:17770
# 2. Start the hub (terminal B)
HUB_PORT=17745 \
HUB_DB_PATH=./hub.sqlite \
HUB_JWT_SECRET="$(bun -e 'console.log(crypto.randomBytes(32).toString("base64url"))')" \
HUB_IAS_ISSUER=http://localhost:17770 \
HUB_IAS_CLIENT_ID=cc-remote \
HUB_IAS_CLIENT_SECRET=test-secret \
HUB_IAS_REDIRECT_URI=http://localhost:17745/auth/callback \
HUB_IAS_ALLOWED_SUBJECTS=i060912@sap.com \
HUB_PWA_URL=http://localhost:15173/ \
bun run packages/hub/src/index.ts
# 3. Issue a pairing code (one-shot, terminal C)
HUB_DB_PATH=./hub.sqlite \
bun run packages/hub/src/admin.ts issue-pairing-code i060912@sap.com macbook
# → ABC-DEF
# 4. Pair this machine (one-shot, terminal C)
bun packages/daemon/bin/cc-remote.ts pair \
--hub ws://localhost:17745 \
--code ABC-DEF \
--daemon-id macbook
# → paired as daemon_id=macbook ...
# 5. Run the daemon (terminal C)
bun packages/daemon/bin/cc-remote.ts daemon
# → daemon macbook ready; ... auth=on
# 6. Run a fake Claude Code session (terminal D)
bun tools/fake-claude/fake-claude.ts --session-id demo --cwd "$PWD"
# 7. Run the PWA (terminal E)
VITE_HUB_URL=ws://localhost:17745 bun run --filter=@cc-remote/pwa dev
# → http://localhost:15173/
# 8. Open http://localhost:15173 → click Sign in → fake-IAS auto-redirects
# back with bearer in fragment → daemon list shows "macbook" with session "demo".
# Add allow_kill: true to ~/.cc-remote/config.json to enable remote kill_session.
# Add allow_start: true and allowed_cwd_prefix: ["/your/path"] to enable remote start_session.| Var | Purpose |
|---|---|
HUB_PORT |
Hub HTTP/WSS port (default 17745) |
HUB_DB_PATH |
Hub SQLite path (default ./hub.sqlite) |
HUB_JWT_SECRET |
HS256 secret for daemon JWTs (must be stable across restarts) |
HUB_DISABLE_AUTH |
"1" to bypass /ws/* auth (dev/test only) |
HUB_PWA_URL |
URL the /auth/callback redirect lands on (default /) |
HUB_IAS_ISSUER |
IAS OIDC issuer URL |
HUB_IAS_CLIENT_ID |
IAS client id |
HUB_IAS_CLIENT_SECRET |
IAS client secret |
HUB_IAS_REDIRECT_URI |
Callback URL registered with IAS |
HUB_IAS_ALLOWED_SUBJECTS |
Comma-separated subject whitelist |
CC_REMOTE_STATE_DIR |
Daemon state directory (default ~/.cc-remote) |
CC_REMOTE_SOCKET |
Plugin's daemon socket path |
FAKE_IAS_PORT |
fake-IAS listen port |
FAKE_IAS_SUB |
fake-IAS subject (default i060912@sap.com) |
HUB_VAPID_PUBLIC_KEY |
VAPID public key (Web Push) — generate via bun -e "import('web-push').then(w=>console.log(w.default.generateVAPIDKeys()))" |
HUB_VAPID_PRIVATE_KEY |
VAPID private key |
HUB_VAPID_SUBJECT |
mailto: or https:// URL for VAPID subject |
VITE_VAPID_PUBLIC_KEY |
VAPID public key for PWA build (same value as HUB_VAPID_PUBLIC_KEY) |
| Package | Purpose |
|---|---|
packages/proto |
Wire frame TS types and length-prefixed JSON codec |
packages/plugin |
Claude Code channel plugin (MCP stdio) |
packages/daemon |
Long-running local process; cc-remote CLI lives here |
packages/hub |
VPS service with IAS OIDC + DPoP daemon auth |
packages/pwa |
React/Vite browser client |
tools/fake-claude |
Test harness that spawns the plugin |
tools/fake-ias |
Test-only OIDC mock |
e2e/ |
End-to-end tests |
bun test # all unit + e2e
bun test packages/ # unit only
bun test e2e/ # end-to-end (in-process, fast — merge gate)
bun test e2e-real/ # end-to-end (real components: docker hub, real claude under tmux)
bun run typecheck # all packagesA second e2e suite that exercises the v1 acceptance checklist against real
components: real hub binary in docker compose, real cc-remote daemon on host,
real claude driven through tmux interactive mode, and a scripted
PWA-equivalent client. Complements (does not replace) the in-process e2e/
suite. Acceptance baseline: 12 pass / 0 fail in ~5.4 min wall time. See
e2e-real/README.md and the
design spec. Cost:
~$0.20 of Anthropic API per full suite at default Haiku.
# Pre-validate the plugin manifest.
claude plugin validate packages/plugin
# Run claude with the plugin loaded. Requires a running daemon
# (cc-remote daemon at ~/.cc-remote/daemon.sock or $CC_REMOTE_SOCKET).
claude --plugin-dir packages/plugin -p "your prompt"--plugin-dir is fine for non-permission flows. For the channel-permission relay to engage end-to-end (PWA Allow/Deny banner driving real tool gating), Claude Code must be run interactively (no -p) AND loaded via --mcp-config, not --plugin-dir:
# Generate an mcp-config.json once:
cat > /tmp/cc-remote-mcp.json <<'EOF'
{ "mcpServers": { "cc-remote": {
"command": "bun",
"args": ["run", "/path/to/repo/packages/plugin/src/index.ts"]
} } }
EOF
# Then launch interactively (any terminal — tmux not required for manual use):
claude --mcp-config /tmp/cc-remote-mcp.json \
--dangerously-load-development-channels server:cc-remote \
--setting-sources project,localEmpirical findings + flag rationale: docs/superpowers/research/2026-05-20-p-mode-permission-spike.md.
- Plan 1: vertical slice — plugin/daemon/hub/PWA wired up, sessions visible in PWA
- Plan 2: auth — IAS OIDC for PWA, DPoP-bound JWT for daemons,
cc-remote pairCLI - Plan 3: real-time conversation streaming — daemon tails Claude Code's session JSONL and streams every line to the PWA's per-session pane
- Plan 4: permission relay — when Claude Code asks to run a tool, an amber banner appears in the PWA with Allow/Deny buttons; the decision flows back to the plugin and is recorded in the daemon's SQLite audit table
- Plan 5: Web Push notifications — when a permission request arrives, all of the user's registered browsers/PWAs receive a push notification via VAPID-signed Web Push, with a service worker showing an OS-level notification
- Plan 6: operational polish — "My devices" settings panel (list/rename/revoke),
cc-remote daemon rotate-tokenfor periodic credential rotation, hub/pair/refreshendpoint - Plan 7: history scroll-back — scroll up in any SessionPane to load older events from the JSONL file
- Plan 8: launchd / systemd installer —
cc-remote installwrites the right service file for your platform and starts the daemon - Plan 9: push preferences — Settings panel has a Notifications toggle to opt out of permission-request push
- Plan 10: acceptance suite — automated benchmarks asserting the design spec's quantitative criteria
- Plan 11: daemon-offline push — opt-in notification when a daemon stays offline ≥ 30s; cancelled if the daemon reconnects in time
- Plan 12: remote kill_session — opt-in dangerous action: ✕ button per session terminates the plugin
- Plan 13: remote start_session — opt-in dangerous action: launches a new tmux session running
spawn_commandin a chosen cwd - Plan 14: task-completed events — daemon detects
assistantlines withstop_reason: "end_turn"and emits a typedtask_completedevent - Plan 15: idle events — after a task completes, daemon waits
idle_window_ms(default 30s) for activity; if none, emitsidle. Cancelled by any new JSONL line. Optional push - Plan 16:
cc-remote statusCLI — operator visibility for daemon configuration, pairing state, JWT expiry, and recent permission audit entries
- ✅ Permission round-trip P95 < 1s —
e2e/perf-permission.test.ts - ✅ 3 concurrent daemons surface within seconds —
e2e/multi-daemon.test.ts - ✅ Daemon-offline push debounce honors the 30s window —
packages/hub/tests/router.test.ts - ✅ kill_session terminates the plugin and emits session_close —
e2e/kill.test.ts - ✅ kill_session is ignored when allow_kill is false (default) —
e2e/kill.test.ts - ✅ start_session spawns via tmux when allowed —
e2e/start.test.ts - ✅ start_session is ignored when allow_start is false (default) —
e2e/start.test.ts - ✅ task_completed emitted on assistant + end_turn —
e2e/completed.test.ts - ✅ task_completed NOT emitted on stop_reason: tool_use —
e2e/completed.test.ts - ✅ idle event fires after idle_window_ms —
e2e/idle.test.ts - ✅ idle event is cancelled by activity within window —
e2e/idle.test.ts
- Hardware-bound keys — keystore abstraction layer is in place, but only file-backed Ed25519 ships; macOS Keychain (Security framework) and Linux libsecret bindings are deferred
- Real Claude Code channel-permissions wire format — for now
CC_REMOTE_FAKE_PERMISSIONsimulates the chain - Windows installer — Plan 8 covers macOS + Linux only
(TBD)