How I built a local, end-to-end encrypted bridge from Claude Code to my phone and watch
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
- Trust bootstrap is inherently physical. A no-account, no-cloud model means the QR pairing is the root of trust. That's the point, but it means recovery is your problem, not a "forgot password" link.
- Lid-closed on battery is a losing fight with thermodynamics. You can make it work; you can't make it not drain. Being loud about that beat being clever about it.
- Structured answers > keystrokes. The temptation is to send raw text back to the terminal. Modeling the actual decision (a choice, a plan, a permission) is more work and the only thing that makes it feel like a remote instead of a toy.
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.
Answering from the phone is Claude Code today; Codex and Grok are monitor-and-switch. Questions or corrections? hello@devfob.com.