nexting-cc-bridge
v0.13.1
Published
Nexting Claude Code bridge — runs Claude Code through a local hub so your phone mirrors and controls your terminal sessions (two-way synced).
Downloads
5,056
Readme
nexting-cc-bridge
Runs on a user's Mac so their phone mirrors and controls their Claude Code terminal sessions, two-way synced — one model, no read-only/bubble split.
How it works (the "manager"): your claude is wrapped by a thin shell that
runs the real, official claude in a pseudo-terminal. Your terminal stays
native; the shell also mirrors the session to your phone and injects what the
phone types. A single background hub daemon owns the cloud connection and
multiplexes every wrapped session, so you can run many claudes at once and the
phone switches between them.
The one hard limit (OS): a claude already running bare in a terminal can't
be reached — macOS won't let an outside process inject into another process's tty.
So only sessions started through the manager are phone-controllable. The
installer makes that invisible by putting a claude shim first on your PATH, so
you keep typing claude and every new session is automatically controllable.
Design: docs/superpowers/specs/2026-06-07-cc-manager-unified-design.md (current);
earlier: …2026-06-06-cc-shared-session-json-shell-design.md, …2026-06-04-….
Install (end users)
In the Nexting app: Claude Code → Connect, which shows a one-line command (it embeds an account token, so no second login):
curl -fsSL https://nexting.ai/install-cc | NEXTING_CC_TOKEN=<token> bashThis binds the Mac to your account, installs the manager, puts the claude shim
on PATH, and starts the hub daemon (launchd). Then just run claude as usual.
- Bypass once:
NEXTING_CC_DISABLE=1 claude - Remove everything:
nexting-cc-bridge uninstall
(No app token? curl -fsSL https://nexting.ai/install-cc | bash falls back to
device-code browser auth.)
Windows
Codex is fully supported on native Windows (since 0.10.0):
irm https://nexting.ai/install-codex.ps1 | iexThat logs in (device-code browser auth), installs this package, and registers a
per-user Scheduled Task (NextingCodexBridge, at-logon + restart-on-failure —
the launchd equivalent). The bridge drives codex app-server --stdio per
session; the official managed-daemon control socket is never dialed on Windows
(it's broken there — openai/codex#24090). Terminal mode (phone-started PTY) is
not available on Windows yet; app-server control covers send/steer/live sync.
Remove with nexting-cc-bridge codex-uninstall.
Claude Code is fully supported on native Windows (since 0.11.0):
irm https://nexting.ai/install-cc.ps1 | iexThat logs in, installs this package, and sets up the Windows translation of the
mac trio: a claude.cmd shim in ~/.nexting/bin (prepended to the USER Path —
fully quit + reopen your terminal app for it to take effect; new tabs inherit
the old environment), and a NextingCcBridge Scheduled Task running the hub
(cli.js hub, named pipe \\.\pipe\nexting-cc-hub-<user> instead of the unix
socket). Typing claude routes through claude-shim (same bypass rules as the
bash shim: NEXTING_CC_DISABLE=1, piped stdio, -p/--print, non-interactive
subcommands go straight to the real claude — resolved from known install
locations first, %USERPROFILE%\.local\bin\claude.exe topmost; where.exe
minus the shim dir is only a last resort).
Interactive runs are wrapped in a ConPTY (node-pty) and mirror to the phone;
phone-started sessions spawn the same way. Caveats: ConPTY is a translating
layer (not byte-passthrough) and Claude Code has known ConPTY rendering issues
upstream — expect occasional repaints; sessions not started through the shim
show as idle (no ps/lsof on Windows). Remove with
nexting-cc-bridge uninstall.
Region-locked egress (e.g. China)
A phone-created Claude Code session starts a real Mac-side PTY, and Codex starts
its Mac app-server child. Those processes run under the launchd daemon, so they
do NOT inherit the https_proxy your terminal claude wrapper uses. If your
region needs a proxy to reach Anthropic/OpenAI, the agent can exit direct and
fail 403 Request not allowed (while the terminal works). Worse, a different
exit IP on the same account is an account-ban risk.
Fix: set engineEnv so bridge-started agents exit from the same residential
IP / timezone / locale as your terminal. If your shell rc defines CLAUDE_PROXY
(or CLAUDE_PROXY_{USER,PASS,HOST,PORT}):
nexting-cc-bridge sync-proxy # writes engineEnv to ~/.nexting/{cc,codex}-bridge.json
launchctl kickstart -k gui/$(id -u)/ai.nexting.cc-bridgeOtherwise add an engineEnv object by hand to those config files. It fails
closed (a dead proxy errors the request, never falls back to direct) and the
daemon log prints engine egress: PROXIED via … / DIRECT so you can verify it.
Re-run sync-proxy after rotating the proxy. US users need none of this.
How discovery works
- Scans
~/.claude/projects/<encoded-cwd>/*.jsonl— each JSONL is one session,mtime= last active. (encoded-cwd= every non-alphanumeric char →-.) - Finds live sessions via
ps, matching the CLI by argv0 basenameclaude(since 0.5.1 — covers npm, pnpm, and the native installer's~/.local/bin/claude; the desktop app is excluded), deduped to root processes by PPID. Procs started with--session-id <uuid>(every wrapper-run session) bind pid↔session exactly; only bare procs without it fall back to the heuristic:lsofthe proc's cwd, then with N live procs in a project the N newest JSONLs touched within the last 30 minutes arerunning, the restidle(the freshness bound keeps hours-old sessions from showing as running just because unrelated procs exist in the same project); extra procs with no JSONL yet are virtualpending-<pid>. - List rows read only head 16KB (title/cwd) + tail 128KB (recent messages). Full transcript is read on demand when the phone opens a session.
- Row title (since 0.4.1) = the session's live recap: the freshest
{type:"system", subtype:"away_summary"}entry Claude Code keeps appending to the JSONL (tail window beats head). Falls back toai-title, then the first user message — slash-command wrappers render as/name, local-command caveats are skipped. Codex sessions have no recap and keep their meta title. - Codex transcripts (since 0.5.1) parse
~/.codex/sessions/rollout JSONL and hide the bookkeeping the Codex TUI never renders — developer/system-role instruction messages and user-role<environment_context>/<user_instructions>/<turn_aborted>prefix messages — so the phone shows exactly what Codex shows. - Live streaming (since 0.5.0): while a phone has a session open, the cloud
arms a watch (
cc_watch/ aliascc_watch_start) and the bridge tails that session's JSONL (transcript-watcher.ts+watch-manager.ts), pushing new entries ascc_event{kind:"transcript_append", startIndex, …}(idempotent by absolute index) pluswatch_ok{entryCount}reconciliation heartbeats, so terminal-driven turns appear on the phone block-by-block instead of on the next poll. Watches stop oncc_unwatch/cc_watch_stopor expire on their own TTL;watch_failedtells the phone to fall back to polling.
Commands
# normal usage (set up by the installer; you don't run these by hand)
nexting-cc-bridge install # install the `claude` shim + PATH + start the hub (idempotent)
nexting-cc-bridge uninstall # remove shim + PATH entry + hub launchd (reversible)
nexting-cc-bridge hub # the background daemon: holds the cloud link, multiplexes all shells
nexting-cc-bridge term -- ... # one wrapped `claude` that joins the hub (what the shim calls)
# debug / legacy
nexting-cc-bridge once # print one discovery snapshot as JSON and exit
nexting-cc-bridge start # legacy read-only discovery daemon
nexting-cc-bridge mirror # legacy single-session mirror (one bridge per user)Architecture: hub (one daemon, one cloud WS, listens on
~/.nexting/cc-hub.sock) ← many term shells (each = one pty-wrapped claude,
registers over the local socket). The hub multiplexes all shells to the cloud and
routes phone input back to the right one by termId. A shell exiting (Ctrl-C /
crash) sends bye, so the phone never sees a ghost terminal.
Local dev / end-to-end proof (no cloud needed)
npm install
npm test # unit tests (122)
node --import tsx src/dev-server.ts & # cloud stand-in (ws://localhost:7799)
NEXTING_CC_URL=ws://localhost:7799/cc-bridge/connect NEXTING_CC_TOKEN=dummy \
node --import tsx src/cli.ts start & # bridge against real ~/.claude/projects
node --import tsx src/phone-probe.ts # simulates the phone: lists + opens a transcriptEnd-to-End Encryption
The Pinclaw cloud acts as a relay and store, not the agent host. By default
the cloud can see the session content it relays. E2E encryption is an opt-in
feature (e2eEnabled: true in ~/.pinclaw/cc-bridge.json) that makes the cloud
a pure ciphertext forwarder: it sees routing metadata and opaque blobs but no
plaintext content.
Trust model
- Trusted endpoints: the Mac bridge and the user's phone.
- Untrusted: the Pinclaw cloud server (treated as a relay that may be compromised).
- When E2E is on, only the endpoints can decrypt transcript text, titles, summaries, and stream deltas.
What gets encrypted
| Content | Encrypted when E2E on |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Session titles (cc_snapshot) | Yes |
| Session summaries (cc_snapshot) | Yes |
| Recent message text (cc_snapshot) | Yes |
| Transcript entries (cc_event transcript_append) | Yes |
| Streaming text and thinking deltas (cc_event stream_*_delta) | Yes |
| Inbound user sends (cc_send content) | Yes |
| Inbound question answers (cc_answer updatedInput) | Yes |
| Media/image bytes | Yes — src/media-hydrator.ts encrypts blob bytes with sealBytes (session CEK) before upload and marks them encrypted: true; the phone decrypts with openBytes |
| Terminal mirror frames (cc_term_output, cc_term_input) | No — these are already opaque binary blobs; the content stream above is where the readable text lives |
| Control / handshake frames (reg, cc_hello, cc_term_hello, etc.) | No |
Cryptographic details (auditable)
All algorithms are in src/e2e/crypto.ts (Node built-in node:crypto only —
no third-party crypto libraries).
Content encryption (per session, text fields):
- Algorithm: AES-256-GCM
- Per-session key: 32-byte Content Encryption Key (CEK), one per session, generated fresh by the bridge at session creation.
- Wire envelope:
e2e:v1:<base64(nonce[12] || ciphertext || tag[16])> - AAD (authenticated additional data):
"<sessionId>|0|<field>"— binds every ciphertext to its session and field, preventing cross-session replay. The0is the sequence placeholder (current protocol has no per-frame sequence number). Field tags are:title,summary,msg,text,delta,send,answer.
Media encryption (binary):
- Algorithm: AES-256-GCM (same
sealBytes/openByteshelpers). - Wire layout: raw bytes —
nonce[12] || ciphertext || tag[16](no text prefix; anencrypted: truemetadata flag marks the blob). - AAD:
"<sessionId>|0|media"
Key wrapping (CEK delivery to the phone):
- Algorithm: ephemeral X25519 ECDH → HKDF-SHA256 → AES-256-GCM
- Derivation:
HKDF-SHA256(ikm=X25519_shared, salt=UTF8(sessionId), info=UTF8("pinclaw-e2e-cek-wrap"), L=32) - Wire format:
wrap:v1:<base64(ephPubSPKI[44] || nonce[12] || ct || tag[16])> - The bridge generates an ephemeral X25519 keypair per wrap, computes the shared secret with the recipient device's long-term public key, derives a per-wrap AES-256-GCM key, and seals the CEK. The recipient (phone) reverses the process.
Device identity key:
- Each endpoint (Mac bridge, phone) has an X25519 long-term keypair.
- The bridge private key is stored in the macOS Keychain (service
pinclaw-bridge-e2e, written viasecurity add-generic-password); it never leaves the machine. Seesrc/e2e/keychain-identity.ts. - Public keys are published to the cloud key directory so endpoints can fetch each other's keys for wrapping.
Safety number (human-verifiable MITM protection):
SHA-256(sort(pubA, pubB))— the first 10 bytes encoded in base32 (A-Z2-7), displayed asXXXXX-XXXXX.- Compare this five-plus-five string between your Mac terminal and the Pinclaw app. If they match, no cloud-side key swap has occurred.
What you can verify
- Algorithms: read
src/e2e/crypto.ts— every cipher call is there. Node's built-innode:cryptomodule, no opaque libraries. - Which fields are sealed/unsealed: read
src/e2e/codec.ts(EnvelopeCodec.encryptOutbound/decryptInbound). The switch cases enumerate every frame type that the codec touches. - Key storage: read
src/e2e/keychain-identity.ts— the only I/O issecurity find-generic-password(read) andsecurity add-generic-password(write) to the macOS login keychain. - Tests and interop vectors:
test/e2e-crypto.test.ts,test/e2e-codec.test.ts,test/e2e-wire.test.ts— the vectors ine2e-crypto.test.tsare the shared ground truth used to verify byte-level compatibility between the bridge, cloud, iOS, and Android.
To run just the E2E tests:
npm test -- --testPathPattern="e2e-"Wire protocols
Terminal mirror — hub ⇄ cloud (the current model):
Hub → cloud: cc_term_hello {termId,cwd,title,cols,rows}, cc_term_output
{termId,data(b64)}, cc_term_bye {termId}. Cloud → hub: cc_term_input
{termId,data}, cc_term_resize {termId,cols,rows}.
Shell ⇄ hub (local socket, newline-JSON, hub-protocol.ts):
shell → hub reg / out / bye; hub → shell in / resize.
Phone ⇄ cloud (routes/cc.ts): GET /cc/term/list, GET /cc/term/stream
(SSE bytes, replays the rolling buffer then live), POST /cc/term/input, POST
/cc/term/resize, and POST /cc/pair (logged-in phone mints an account-bound
install token). Cloud relay: services/cc-term-service.ts (per-term rolling
256KB buffer, replay-then-live, drops on bye).
Legacy (bubble/discovery, still present): cc_hello/cc_snapshot/cc_event +
/cc/sessions|attach|send|stream; migrations 049_claude_code_mode.sql +
050_cc_sessions_controllable.sql. The unified iOS UI uses the terminal path only.
