How Innkeeper is built

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.

The three parties

Your phone

Holds the secrets. Signs approvals with a Secure Enclave key. It is the only place a value is ever stored.

The daemon

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.

The agent

Whatever wants a yes. Untrusted by design: nothing it says about itself is believed without checking.

What an approval actually is

A signature over six lines, and those six lines are rebuilt from the daemon's own record.

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:

  1. It signs what it rendered. The app parses the canonical text, re-encodes what it parsed, and refuses the whole request if a single byte differs. Then it hashes those bytes itself and compares. It never signs a digest handed to it over the network.
  2. The key is enclave-only. Access control is [.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.

What the Mac keeps

On the self-hosted build, nothing.

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:

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.

What gets written down

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.

The two roads

Hosted, and what we can see on it.

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.

Self-hosted, and what travels.

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.

Same app, same phone, same signature on both roads. The difference is who is in the middle, not what an approval means.

Where it lives

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.