One idea holds the whole thing up. The phone owns every secret, and an approval is a signature over the exact bytes you were shown, made by a key that cannot leave the Secure Enclave.
Holds the secrets. Signs approvals with a Secure Enclave key. It is the only place a value is ever stored.
Your Mac, or our gateway. Turns an ask into a typed request, signs it, checks the signature that comes back, and hands the answer over once.
Whatever wants a yes. Untrusted by design: nothing it says about itself is believed without checking.
Every request has one canonical byte sequence. Keys sorted, no floats, no nulls, one encoding and no second opinion. The digest is over those bytes, and what the phone signs is this:
innkeeper-v1 <daemon_id> <request_id> <digest — sha256 of the canonical bytes> <issued_at> <expires_at>
The two rules the app follows without exception:
[.privateKeyUsage, .biometryCurrentSet] — no passcode
fallback and no "or". There is no if faceIDPassed branch
anywhere in the app. The enclave refusing to sign is the
check, and adding a finger or a face to the phone invalidates the key.And the daemon's rule, the mirror of those: it verifies against the six lines it rebuilds from its own stored record, never against a string the app sent it.
There is no vault on the Mac and no file it could read if it wanted to. A credential exists there only between an approval and a use: it arrives sealed to a key made at boot, is opened after the signature verifies, is held for a measured number of seconds, is handed to exactly one caller, and is overwritten. Ask twice and you get asked twice.
Three checks stand between a process on that Mac and a credential, and they are of different kinds on purpose, so breaking one does not help with the next:
getpeereid names the calling process's user rather than
believing anything it says about itself.The honest limit: a program running as you can read the files you can read, including an agent's own token. What that buys an attacker is the ability to ask. Your phone still lights up, and you still have to say yes.
The daemon keeps one log line per event, appended and never rewritten, and the rule for what may go in it is short: names, ids, verdicts, counts. Never a value, never the length of one, never a digest of one. A log that could be mined for a secret would be exactly the thing this design exists to avoid, written slowly.
The desktop app reads that log back in plain English — Claude asked for gmail, you approved gmail for Claude — with the mark of whichever client asked beside it. A client that names itself something the app has never heard of is written out exactly as given, with a plain mark. Nothing guesses a logo.
To draw a tool call on your phone we have to read it. So on the hosted side we see every call and every argument in plain text. We would rather say that plainly than have you discover it. What we still never see is a secret held on your phone, and what we still cannot do is forge an approval: the signature is over bytes your phone re-encoded, made by a key we have no copy of.
Run the daemon on your own Mac and approvals go straight from it to your phone over the local network. When your phone is elsewhere they travel through our relay as sealed envelopes: keys agreed between your Mac and your phone, the relay moving boxes it has no way to open, and a request id is all it can count.
The hosted gateway speaks OAuth 2.1 to the MCP specification of 28 July 2026: RFC 9728 protected resource metadata, PKCE on every flow, RFC 8707 resource indicators on both legs, and no token passthrough in either direction. The front page has the shape of it, and a call gets you the rest.