pair your phone to your Mac and run real coding sessions on it from anywhere. the agent runs as you, in your own repo, with your shell and your auth; the boundary is pairing plus whatever macOS has granted, not a path check the app could talk itself out of. a menu-bar Mac app holds the link (round trip, uptime, frames, reconnects, spend) and an iOS app drives it: prompt from the phone, watch tool calls and diffs land, and see the live Mac screen when you need to look.
two outbound sockets, a stateless relay, claude as a child process
swift
swiftui
electron
node
python
fastapi
websockets
hmac
postgres
webcodecs
jpeg
bonjour
claude-code
apns
the problemA coding agent you cannot reach is a coding agent you do not have
Claude Code is useful on my Mac because it has my shell, my git credentials and my repos checked out where I left them. That is also why it is stuck: shut the lid, get on a train, and the useful thing is in a room I am not in.
I looked at three ways out and took the one where nothing listens.
three ways to reach your own machine
the ideaThe relay only introduces them
Identity is an HMAC-signed token that carries its own claims, so the relay verifies a signature and reads who may talk to whom. It looks nothing up, which is why the switchboard is 171 lines of dictionaries.
The default signing secret used to be compiled in, meaning a relay deployed without one booted fine and minted forgeable identities. Its absence is now a refusal to start, and a second key verifies without minting so rotation is a deploy rather than an outage.
pair/claim
the most valuable unauthenticated route here, so 8 attempts per caller and a global bucket of 200
pairing a phonea pair code becomes an authorised device row
how it worksOne prompt, phone to process and back
Text goes on the phone's socket, the relay writes it onto the Mac's socket, and the Mac writes it as one JSON line into a running claude child. Answers come back the same way, an event per line.
Streamed deltas are appended to a block, then the block is replaced wholesale when the final text arrives. Replacing rather than appending is what stops every answer rendering twice, and it is where the haptic fires, once per block instead of once per token.
event kinds
one child line becomes one of fourteen: delta, text, thinking, tool, tool_result, agent, title, done, outcome and the rest
one prompt, end to endone tool call, stdout to a card on the phonea photo becomes a path the agent can open
the fenceWhat the boundary actually is, and what it is not
There was an app-level fence once, one chosen folder and a path check on everything the phone named. I removed it because it stopped the honest route and none of the others: a paired phone could always start a session inside the folder and ask the agent to read the disk, and a symlink walked out of it anyway.
So the folder you pick is where the phone starts, not where it ends. An owner's session runs as the owner with bypassPermissions, because there is nobody at the Mac to answer a prompt, so if an agent must not reach a directory, do not grant Redeye access to it.
allowlist, not denylist
a forgotten allowlist entry means extra prompts, a forgotten denylist entry means a member skipping permissions on somebody else's Mac
one assignment
the clamp sits on the setter, not at the four places a mode can arrive from
the fence people assume vs the fence that holdsthe four TCC grants Redeye tracksowner session vs member session
the hard partA socket that says it is connected and is not
The bug that cost me most had nothing to do with agents. When the far end vanishes without a FIN, and a stream cut at the hour mark looks exactly like that, the socket stays ESTABLISHED, close never fires, and every frame written to it is retransmitted into nothing.
Screen capture was the other one. Shelling out per frame capped it near a slideshow, and a hidden renderer's compositor is so starved that a video element became the next ceiling, so frames are pulled off the track itself.
pacing
measured from grab time, not encode end, or encode time inflates every interval
two watchers
the stream runs at the highest fps anyone asked for
connected, and actually connectedthe watchdogone screen frame, capture to pixels
the stackFour apps, one frame vocabulary
Every message is the same shape and nothing about it is typed or generated, so adding one means touching three languages by hand and I have shipped a Swift decoder that disagreed with a JS producer. What it buys is a relay that never has to understand any of it.
The relay stores accounts, orgs and a session index, never a transcript and never a local path. A path carries a username and the shape of a disk, and two Macs disagree about it anyway, so a folder is identified by its git remote and each Mac resolves its own checkout.
four appswhat sits on whatthe same frames, two roads
where it isWhat works, what I know is wrong
It works end to end and there is a script that proves it: a host stood up, a simulated phone paired through the real deployed relay, a real Claude session driven, the tool calls and the answer asserted on arrival. A command-line iPhone takes the same path, which is how most of the protocol got debugged without opening Xcode.
The bug I am most wary of now was a field that was correct and unread. One flag had described which engines report a running cost total since the engine table was written, nothing consulted it, and every turn-driven session looked free after its first answer.
counted scalewhat a turn cost, receipt to chipopen