Blog · · QR Agent
Encrypted QR codes are not a contradiction
A QR code is a printed label. Anyone with a camera can read the characters encoded in it, and no amount of cryptography changes that — the code is on a box, on a poster, on a badge, and it is readable by design. So "encrypted QR code" sounds like a contradiction, and the first thing worth doing is saying exactly where the line falls.
What the code actually contains
Our codes encode one thing: an opaque URL on our own domain. No payload, no identifier you can reason about, nothing that means anything without asking the server. Scanning it tells you where to knock. It does not tell you what is behind the door.
The content is encrypted at rest under a key that belongs to your workspace, sealed to the specific code and the specific workspace it belongs to. A ciphertext copied from one record to another does not decrypt — not because we check, but because the decryption fails.
Where the guarantee ends
This is the part most pages skip, so:
- A person who passes the gate sees the content. The encryption protects the payload from us, from a database dump, and from anything automated. It does not protect it from a recipient you authorised, and anything they can see they can forward.
- Metadata is not content, and it is visible. Titles, folder names and scan counts are readable by anyone with the right access. Put nothing in a title you would not put on the outside of an envelope.
- A dead code is not a deleted secret. Archiving a code stops it resolving. It does not reach into the world and unprint it.
None of those are things we intend to fix, because two of them are not fixable and the third is a property of paper.
What it costs to hold the line
Every gated scan is a round trip: the scanner lands on our domain, passes a gate, and only then is the payload decrypted and served. That is slower than a bare URL, and it is the whole product. A code that resolved instantly for everyone would be a normal QR code with extra steps.
The trade we made is that the gate is stateful — a link with an expiry, a use count and a revocation, rather than a self-contained token in the URL. A token in a URL is a secret in your browser history, in every log between you and us, and in the referer header of whatever you visit next. A stateful link is one lookup and one thing to revoke.
Why this matters more now
An agent creating codes on your behalf makes the boundary sharper, not softer. Your agent can create a code, repoint it after it is printed, and tell you what it did — all without ever seeing what is inside. Reading a payload is a separate permission, qr:content:read, deliberately left out of the older read/write expansion, so a credential minted before that scope existed did not quietly gain plaintext on the day it shipped. A key that can list your codes cannot open them until you say so.
Grant it and the limit is worth saying plainly: every such read writes an audit row naming the credential, the reads are capped per credential per hour, and a payload that has entered a model's context has left our controls. Encryption protects the payload from us and from a database dump. It does not protect it from a recipient you authorised, or from an agent you handed the scope to.
That is not a restriction we added for agents. It is the same wall, and agents were simply the first callers for whom "can read metadata" and "can read content" needed to be visibly different things.