How I built a local, end-to-end encrypted bridge from Claude Code to my phone and watch

An honest engineering writeup. No cloud, no account, no VPS.

The problem was dumb and specific: I'd start a long Claude Code run, walk away, and come back to find it had been parked on a single yes/no question the whole time. I didn't want a terminal on my phone. I wanted the question on my phone. Here's how that got built, and where it got genuinely hard.

The design constraint that shaped everything: no cloud

The easy version is a relay server. The Mac posts events to your backend, the phone long-polls it, done in a weekend. I didn't want that. It means your prompts, your file paths, and your agent's questions transit a server I run, plus an account, plus a thing to get breached. So the hard rule was: the Mac and the phone talk directly, and nobody in the middle can read anything. Every decision below falls out of that rule.

Watching the agent without driving it

Claude Code already has the hook I needed. A Notification / permission hook fires when the agent wants a decision, and I capture the structured prompt there rather than screen- scraping a terminal. The daemon that runs on the Mac watches those events, plus session state (which run is working, which is waiting, how long it's been stuck), and holds the current picture in memory. It also grabs TERM_PROGRAM so a tap on a session can foreground the exact terminal it's running in.

The subtle part is the return path: getting your answer back into a running session. That's the difference between a notifier and a remote. A notifier tells you it's stuck; a remote lets you unstick it. Wiring the answer back into the waiting prompt, structured (multiple choice, plan approval, permission grant) rather than as raw keystrokes, is most of the actual work.

Pairing: Curve25519, once, in person

Two devices that will speak privately over a hostile network need a shared secret neither the network nor any server ever sees. The pairing is a Curve25519 key exchange, bootstrapped by a QR code shown on the Mac and scanned by the phone. After that one exchange, both sides hold a shared key derived locally. There is no account, no server-side record of the pairing, nothing to log in to. Lose the phone, and the pairing is revoked from the Mac.

The wire: ChaCha20-Poly1305 frames over your LAN

Every message between Mac and phone is an authenticated, encrypted frame (ChaCha20-Poly1305) keyed off that pairing secret. On a normal home or office network the phone finds the Mac and they talk directly, LAN-first. Nothing leaves your network. If you're on the same Wi-Fi, the round trip to approve a plan is basically instant, because there is no cloud hop, there's just the two devices.

Why not just Tailscale + SSH? That solves reachability, but you still end up typing prompts into a terminal on a touchscreen, which is miserable, and it doesn't push you when the agent needs you. The whole bet here is that you want the decision, not the shell. See the longer comparison.

The part that actually got hard: leaving the network

LAN-first is lovely until you walk out the door. Two problems show up at once.

1. Reachability when Wi-Fi drops

When known Wi-Fi disappears, the Mac auto-joins your iPhone's personal hotspot, so the direct link survives you leaving the house. It's the same encrypted channel, just riding a different network. The phone is now both the client and the uplink, which is a slightly funny topology but it works.

2. Keeping the Mac awake with the lid shut

macOS enforces clamshell sleep on battery, and no amount of caffeinate beats it. The honest answer is pmset disablesleep, which is a loaded gun: forget to revert it and your laptop cooks in your bag. So the lid-closed mode wields it carefully, one admin grant, an automatic scripted revert, and loud warnings if a revert ever fails, plus real battery warnings because sustained lid-closed work on battery drains fast. Physics still applies; the app just refuses to pretend otherwise.

The watch: mirror through the phone, don't reinvent the transport

The Apple Watch doesn't get its own crypto channel to the Mac. It mirrors the phone's state over WatchConnectivity, and answers travel back the same way, watch to phone to Mac. On Wear OS it's the same idea over the Data Layer. This keeps exactly one secure transport to maintain instead of two, and it means the thing on your wrist can approve a migration strategy with a tap while the Mac stays put. Answering a Claude Code question from a watch face is a deeply silly sentence to type and it is genuinely the feature I use most.

Licensing with no license server

The no-cloud rule applies to payments too. Licenses are Ed25519-signed tokens: a signed payload of kind | email | issued-date | nonce, verified offline on the Mac against a public key baked into the app. No phone-home, no activation server, no license check that fails on a plane. The private signing key lives in exactly two places and never touches the app. The same cross-language golden vectors prove the JS signer and the Swift verifier agree byte for byte.

What I'd tell you is still hard

Disclosure: this is the engineering behind DevFob, which is my product. I wrote it up because the "no cloud, talk directly, encrypt everything" path is genuinely more interesting than a relay, and more people building agent tooling should take it.

Try DevFob free — 14 days $15 once · no account · no cloud · macOS 14+

Answering from the phone is Claude Code today; Codex and Grok are monitor-and-switch. Questions or corrections? hello@devfob.com.