your computer, without being at your computer


pair your phone with 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. access is limited by the pairing and by the permissions macOS has granted, and there's no path check inside the app for the agent to talk its way past. a menu-bar Mac app keeps the connection up and shows round trip, uptime, frames, reconnects and spend, and an iOS app drives it: send a prompt, watch tool calls and diffs come in, and see the Mac's screen live when you need to.
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
Claude Code is useful on my Mac because it has my shell, my git credentials and my repos where I left them. That's also the problem. Once I close the lid and leave, all of that is in a room I'm not in.
There are three ways to reach it from somewhere else. I picked the one where nothing listens for incoming connections.

| move it to a server | expose the Mac | both ends dial out |
|---|---|---|
| none of your auth | an inbound port on a personal machine | no inbound port anywhere |
| none of your checkouts | a tunnel or dynamic DNS | the relay only copies frames |
| set up a laptop again every session | fragile behind hotel and café NATs | redeploying the relay unpairs nothing |
Identity is an HMAC-signed token that carries its own claims. The relay checks the signature and reads who may talk to whom without looking anything up, which is why the switchboard is 171 lines of dictionaries.
The default signing secret used to be compiled in, so a relay deployed without one would start anyway and issue forgeable identities. Now a missing secret stops it from starting. A second key can verify tokens without issuing them, so keys can be rotated with a normal deploy and no downtime.
Text goes onto 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 process. Answers come back the same way, one event per line, and each line becomes one of fourteen event kinds, such as delta, text, tool and done.
Streamed deltas are appended to a block, and the block is replaced in full when the final text arrives. Replacing it instead of appending is what stops every answer from showing twice, and it's also where the haptic fires, once per block instead of once per token.


Redeye used to restrict the phone to one chosen folder, with a path check on everything it asked for. I removed it because it only got in the way of honest use. A paired phone could always start a session inside the folder and ask the agent to read the rest of the disk, and a symlink got out of the folder anyway.
So the folder you pick only sets where a session starts. An owner's session runs as the owner with bypassPermissions, since nobody is at the Mac to answer prompts. A member's session is capped at acceptEdits and runs with its HOME, agent config and credentials moved elsewhere. If an agent mustn't reach a directory, don't give Redeye access to it.
| assumed | actual |
|---|---|
| a path check in the app | pairing, done by a person |
| one allowed folder | macOS TCC grants |
| beaten by a symlink | symlinks don't help |
| enforced by my code | enforced by the OS |
The bug that cost me the most had nothing to do with agents. When the other end disappears without sending a FIN, which is what a stream cut at the one-hour mark looks like, the socket stays ESTABLISHED, close never fires, and every frame written to it is retransmitted into nothing. The app showed Connected, with a cached latency figure, while the phone received nothing for hours.
Screen capture was the other hard one. Shelling out for each frame made it a slideshow, and a hidden renderer gets so little compositor time that a video element became the next limit. Now frames are taken straight off the capture track, at 12 fps from a 30 fps track, timed from when each frame was grabbed, and identical frames are dropped.
then back to 1
There are four apps: a Python relay, an Electron Mac app, a SwiftUI iPhone app and a Next.js site. Every message has the same shape, and none of it is typed or generated, so adding one means editing three languages by hand, and I have shipped a Swift decoder that disagreed with a JavaScript producer. In exchange, the relay never has to understand any of it.
The relay stores accounts, orgs and a session index, but never a transcript or a local path. A path contains a username and the layout of a disk, and two Macs would disagree about it anyway, so a folder is identified by its git remote and each Mac finds its own checkout.

It works end to end, and a script proves it. The script starts a host, pairs a simulated phone through the real deployed relay, drives a real Claude session, and checks the tool calls and the answer as they arrive. A command-line iPhone takes the same path, which is how most of the protocol got debugged without opening Xcode.
The bug I'm most wary of now was a field that was correct and never read. Since the engine table was written, a flag had recorded which engines report a running cost total, but nothing checked it, so every turn-based session looked free after its first answer.