← back

redeye

2026

your computer, without being at your computer

redeye

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

the problemClaude Code is only useful where your Mac is

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.

the phone's home screen, paired to a Mac. it lists every folder on that Mac and which ones have a claude session running right now, read from the Mac over the relay.
three ways to reach your own machine
move it to a serverexpose the Macboth ends dial out
none of your authan inbound port on a personal machineno inbound port anywhere
none of your checkoutsa tunnel or dynamic DNSthe relay only copies frames
set up a laptop again every sessionfragile behind hotel and café NATsredeploying the relay unpairs nothing

the ideaThe relay only introduces the two ends

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.

the Mac showing a pairing code
pairing on the Mac, and the account on the phone. the Mac shows a six-character code that expires quickly. once a phone claims it, the Mac becomes a device on that account, and any phone signed into the account can reach it.
pair/claim
the most valuable route that needs no login, so it allows 8 attempts per caller and 200 in total
pairing a phone
  1. 1mac shows codeK7F-2QM, 10 min TTL
  2. 2phone claimsPOST /v1/pair/claim
  3. 3code used upsingle use
  4. 4device tokennames one host
  5. 5socket opensboth ends outbound

how it worksOne prompt, from the phone to the process and back

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.

one prompt, mid-run and finished. on the left, the agent is writing regression tests, and each tool call becomes a card as its event arrives. on the right, the same session is done, with one file edited in 39.3s for $0.253.
one prompt, end to end
iphonerelaymacosChatViewcomposerAppStatesocket, sessions/v1/ws/devicehub.pycopies, storesnothinghost.js45-case framerouteragent.jsstream-json bothwaysclaude childyour shell, yourauthyour reposession.inputby keystdinsession.event

the fenceWhere the security boundary really is

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.

the five macOS permission grants
the real boundary is these five macOS permission grants. a session started from the phone can do exactly what redeye itself is allowed to do, and skipping a grant turns off only the feature that needed it.
allowlist, not denylist
a missing allowlist entry means extra prompts, while a missing denylist entry would let a member skip permissions on somebody else's Mac
the boundary people assume, and the real one
assumedactual
a path check in the apppairing, done by a person
one allowed foldermacOS TCC grants
beaten by a symlinksymlinks don't help
enforced by my codeenforced by the OS

the hard partA socket that says it's connected when it isn't

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.

the watchdog
  1. 1pingevery 5s
  2. 2wait2x the interval
  3. 32 misses~20s of silence
  4. 4terminate()close would wait forever
  5. 5reconnect1s to 30s, jittered

then back to 1

the stackFour apps that share one set of messages

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.

the Mac's screen on the phone. jpeg frames travel on the same socket as every other message, in the same message format.
the same frames, two roads
phonetransportmacAppStaterelayanywherelan.jssame wifihost.jscan't tell which32-bytesecret

where it isWhat works, and what I know is wrong

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.

open
  • pushbuilt, needs an APNs key
  • live activitynot tested with a real push
  • password resetneeds a mail provider
  • forked historyrefused for now
  • codexits own json, not supported