The security model
Last updated July 21, 2026
The model in one sentence: anyone can put keys into your vault; only your devices can take them out. This page explains what that means in practice: what’s encrypted, what your devices hold, what an agent can and can’t touch, and where we’re asking you to trust us rather than math.
The vault
Every account has a vault keypair. The public half is stored on our servers in plaintext, so anything can encrypt into the vault with it. The private half never exists on our servers at all. It’s wrapped separately for each device you’ve approved and stored in that device’s OS keychain (macOS Keychain, libsecret on Linux, DPAPI on Windows later).
Your API keys, OAuth tokens, and private skill blobs are stored on our servers only as ciphertext sealed to the vault’s public key. Decrypting them happens exclusively on a device you’ve approved, in memory, at the moment something needs to use them.
When you set up your account, we show you a recovery code once: the vault private key encoded as a string. It’s the only backup of the key that ever exists outside a device keychain. Save it somewhere safe; we can’t show it to you again.
How keys get in
There is exactly one way a secret enters your vault today, for every provider we support (GitHub, Vercel, Cloudflare, Resend): you paste a token you generated on that provider’s own site, and it’s sealed to the vault’s public key before it ever leaves the device you typed it into.
From the web app, that sealing happens client-side, in your browser, before anything is uploaded. Our servers receive and store ciphertext; they never see the plaintext token. From the CLI (caddi connect github, for example), the sealing happens on your device before the request is even made, which skips the browser entirely.
There is no OAuth flow. No token is ever exchanged with a provider by our servers, and no plaintext token transits Caddi’s infrastructure at any point, for any provider: not for a millisecond, not in memory, not logged. That’s a deliberate, current limitation, not an oversight: it means connecting a service costs you one extra copy-paste step (generating a token in the provider’s dashboard) in exchange for a strictly smaller trust surface. If we add an OAuth-based flow for a provider in the future, this page will say so plainly and explain exactly what changes.
How agents get access
Your coding agent talks to the local gateway over MCP and gets tools, not keys. It calls send_email or deploy_project; it never sees the underlying API key or OAuth token, because those are decrypted inside the gateway, used, and discarded.
The gateway exposes a curated subset of tools per connected service, and you can narrow that further per project with an optional caddi.json, an allowlist of which tools are available when the agent is launched from that directory tree.
Raw secret access exists as an escape hatch for cases like a Next.js app that needs RESEND_API_KEY at runtime: caddi env pull writes allowlisted secrets to .env.local, and a get_secret MCP tool exists for agents that need a value directly. get_secret is off by default and has to be explicitly enabled per project. When it is, every call is flagged prominently in the audit log.
Every tool call the gateway makes is written to a local audit log: the tool name, the project, the timestamp, the outcome, but never the argument values. We record that an email was sent, not who it went to; that a domain was attached, not which one. You can review it anytime with caddi audit, and metadata-only events sync to the web dashboard so you can see agent activity across your machines.
What a breach would get
If our database were compromised, an attacker would get two things, and we want to be plain about both.
First, ciphertext: your secrets, sealed to a vault public key whose private half isn’t on our servers. Without a device keychain, that ciphertext doesn’t decrypt.
Second, metadata. Which services you’ve connected, which team or account they belong to, what scopes were granted, and when you connected them are stored in plaintext, so the web UI can show your connections list without needing a device present. A breach would tell an attacker that you use GitHub and Vercel, even though it couldn’t read your tokens for them.
What we ask you to trust
Encryption schemes don’t eliminate trust, they relocate it. Here’s where it lands for Caddi, without hedging.
The web app we serve. When you paste a key into caddi.build, your browser encrypts it using the vault public key our server hands it. A compromised server could, in principle, serve JavaScript that captures the plaintext before sealing it, or hand your browser a substituted public key. This is the residual trust point of any web-based end-to-end encryption product, the same one you’re implicitly accepting with any browser-based password manager. Two mitigations: the vault public key has a short fingerprint shown by caddi status and on every connect form in the web UI, so a substituted key is detectable rather than silent; and caddi connect <service> from the CLI avoids the browser for pasted keys entirely.
We’re not going to dress this up with compliance badges or claim “military-grade” anything. It’s a real design with a real, named trust boundary, not a marketing gap.
Rotation
If you revoke a compromised device, or just want a clean break, run caddi vault rotate on a device that still holds the vault key. It decrypts every connection locally, generates a fresh vault keypair, re-seals every secret to it, re-wraps the new private key for every currently-active device, and prints a new recovery code. The old one stops working immediately. Devices that were already pending or revoked are unaffected and stay that way. If you suspect a specific connection (not the whole vault) was compromised, disconnecting and reconnecting that one service is enough; a full rotation is for when a device itself might have been exposed.
Lost devices
A stolen laptop doesn’t hand over your vault: the device private key sits behind your OS login in the keychain, and you can revoke that device from another machine or the web UI, which deletes its wrapped key copy from our servers. A device that was already compromised could have kept copies of what it decrypted, so after revoking one, reconnecting the affected services is the honest remediation, and the app will tell you so.
The honest failure mode is this: if you lose every approved device and don’t have your recovery code, the vault private key is gone, and the secrets sealed to it are unrecoverable. Your account isn’t lost. You reconnect your services and keep going, but that data doesn’t come back. We’d rather tell you that up front than pretend there’s a backdoor.
| Threat | What happens |
|---|---|
| Database breach | Ciphertext only. Vault private keys never touch our servers. Connection metadata (which services, when) is plaintext. |
| Active server compromise | Could serve malicious JS to capture a pasted key before it’s sealed, or swap the vault public key. Mitigated by the vault-key fingerprint and CLI-based pasting (caddi connect <service> skips the browser entirely). There is no OAuth relay to compromise: no provider token ever transits our servers in plaintext. |
| Stolen device | Device key is behind OS login in the keychain. Revoke it from another device or the web UI to cut off future syncs. |
| Malicious or prompt-injected agent | Agents get tools, not keys. get_secret is off by default; every call is audited; project scoping limits blast radius. |
Want the deep technical version (algorithms, key wrapping, threat model)? Read the security model docs.
Questions about any of this? [email protected], or see security.txt.