Your agent acts as your users. Their credentials shouldn't live in your database.
Vault
If you are building an agent that does things for people — sends their email, books their travel, files their expenses — you have a problem that has nothing to do with the model. The agent has to authenticate as each user, somewhere, in whatever form that service supports. An OAuth grant for Gmail. An API key the user pasted in for Resend. A username and password for the site that has no API at all.
Every team building one of these solves the same problem, and most solve it the
same way: a user_credentials table, encrypted at rest with a key the
application server holds. It works on day one. It is also a table of your
users' passwords and tokens that your backend can read, your agent can be
talked into repeating, and your logs will eventually contain.
Seekrit Vault is that problem solved once. Your user connects an account on a hosted page. What they authorize is encrypted on their device to a key that exists only inside a small Worker in your own Cloudflare account — the executor. Seekrit stores the ciphertext and cannot open it. When your agent needs the account, your backend asks the executor to make the request; it decrypts just in time, injects the credential, forwards the call, and redacts any echo from the response. Your backend never stores a value. Your agent never receives one. Neither does seekrit.
import { Vault } from "@seekrit/sdk/vault";
const vault = new Vault(); // three env vars: platform key, executor URL, executor token
// The agent needs Resend for user u_123. Open a link, text it to them.
const link = await vault.connect.create({ userRef: "u_123", providerId: "resend" });
await sms.send(user.phone, `Connect Resend to Instinct: ${link.url}`);
// Days later: make the request as the user. No credential in this process, ever.
const res = await vault.fetch({
userRef: "u_123",
connectionId,
request: { method: "POST", url: "https://api.resend.com/emails", body },
});
The guide has the whole setup: five steps, three environment variables, under fifteen minutes. This post is about why the shape matters.
Where do your users' credentials end up today?
Trace one Gmail grant through a typical agent platform and count the places a plaintext lands:
- Your OAuth callback receives the code and exchanges it. The access and refresh tokens are now in your backend's memory.
- Your database stores them, encrypted with a key your application server also holds — so anyone with the server's access has the plaintext.
- Your agent runtime is handed the access token so its Gmail tool can use it. The token is now in the LLM's context, where a well-placed instruction in an email it reads can ask it to repeat the token back.
- Your logs and traces record the request the tool made, header and all, unless someone remembered to redact it.
Four copies, three of them in systems whose job was never to hold secrets. The usual response is to harden each one. Vault's response is to make the first three not exist.
What changes with an executor
The executor is the one component that ever sees a value, and it runs in your Cloudflare account, in your code, under your control. Everything else is arranged so it does not need to:
- Capture happens away from your backend. For a typed API key or password, the hosted Connect page encrypts it in the browser to the executor's public key before anything leaves the device. For OAuth, the provider redirects to your executor, which exchanges the code and encrypts the tokens to its own key. Your backend is not in either path.
- Storage is ciphertext. Seekrit holds encrypted blobs bound to the project, connection, provider, and revision they belong to, and metadata: which user, which provider, what status. It holds no key that opens them.
- Use is a request, not a value.
vault.fetchsends the executor a request without a credential. The executor checks it against the provider's allow rules — host, methods, paths, the same default-deny engine seekrit's credential broker uses — then decrypts, injects, forwards, and returns the response with any echo of the credential redacted. A request outside the rules is refused before anything is decrypted. - The agent gets a result. It asked for an email to be sent; it gets the provider's response. It can be as prompt-injectable as it likes and there is still nothing in its context to leak.
The trust model page states this as a table: who holds what, at rest and in use, and why. The one-sentence version is the promise the product makes: seekrit never sees your users' credentials, and your systems never store them. Read the trust model for the mechanics and for the limits.
What can you tell your users?
The hosted Connect page is white-labelled — your name, your logo, which provider, one plain sentence about what the agent will be able to do — and it carries a footer you did not have to write:
Secured by seekrit · Your credentials are encrypted on this device. Neither Instinct nor seekrit can read them.
That sentence is true because of where the key lives, not because of a policy. It is the difference between asking a person to trust a startup with their Gmail password and showing them that the startup never gets it.
Four kinds of credential, one model
Services differ in what "connect your account" means, and Vault covers the forms an agent platform actually meets:
| Method | How it is captured | How the agent uses it |
|---|---|---|
| OAuth (Google, Microsoft, GitHub presets, or any authorization-code provider) | Provider redirect to your executor; tokens encrypted there | vault.fetch injects the access token and refreshes it before it expires |
| API key | Typed on the Connect page, encrypted in the browser | vault.fetch injects it per the provider's rules |
| Username and password (optional authenticator secret) | Typed on the Connect page, encrypted in the browser | The executor types it into a sign-in form on the site's own login origin, never returning it |
| Browser session, for a site with no API | The person signs in through a Live View on their own phone; the executor captures the session | vault.browser.open restores it into a fenced browser your tools drive |
The data model and the trust boundary are the same for all four. Browser sessions get their own write-up in When the site has no API, the signed-in session is the credential.
The lifecycle you no longer own
Holding a credential is the easy part. Keeping it alive and cutting it off are where platforms spend the second year:
- Refresh runs in the executor, in a single writer per connection, with a journal that makes single-use refresh tokens safe. Your code never sees a stale token or a refresh token. The hard part of "connect your Gmail" is the refresh token explains why that needed its own design.
- Expiry marks a connection
reauth_requiredwith a reason, and a reconnect link replaces the credential as the next revision. - Revocation by you, by an org admin, or by the user from a manage link deletes the ciphertext and refuses every further release immediately.
- Webhooks (
connection.ready,connection.reauth_required,connection.revoked) are HMAC-signed and retried for about a day, so your product can react without polling.
What Vault is not
It is custody, not usage. There is no send_email tool. The agent uses its
own tools and Vault makes them authenticated. Each platform brings its own
OAuth clients — if seekrit owned the client, the authorization code would
land with seekrit and the zero-knowledge claim would be false. And Vault does
not host browsers or stream them: it loads a session into a browser you own
and saves it back.
Try it
A Cloudflare account on the Workers Paid plan, a seekrit org with Vault on its plan, and fifteen minutes:
npx @seekrit/vault-executor init my-vault-executor
cd my-vault-executor && npm install
npx wrangler secret put EXECUTOR_TOKEN
npx wrangler deploy
Then a registration code from the dashboard, three variables on your backend, and the code at the top of this post. The guide walks every step; the API reference has every route.