Keeping API keys out of our AI agent's sandbox, and what that doesn't solve Doozy, an AI agent that queries customer databases, Stripe and GitHub from Slack threads, runs each task in a fresh sandbox holding only a signed, short-lived job token rather than any API key, with a credential proxy outside the sandbox validating the token and injecting the real key into a single call, engineer Shiv Sarthak Sabhlok wrote. Database queries run read-only with a timeout and a row cap, and the job token names one workspace and one job, expires with the job, and pins the model the job started on. Sabhlok said the design stops key leakage but does not stop prompt injection from misusing the tools a job has enabled, which he called the control that matters most. Keeping API keys out of our AI agent's sandbox, and what that doesn't solve Doozy runs tasks against our customers' databases, Stripe and GitHub from a Slack thread. Here's how the agent does that without ever holding a key, and the risks a missing key doesn't remove. Shiv Sarthak Sabhlok https://github.com/shivsarthak ยท 9 min read - AI agent security - Prompt injection - Credentials - Postgres - Slack Doozy is an AI agent people mention in Slack. Someone writes "@Doozy why was Acme charged twice?" in a support thread, and it goes and checks: it queries the database https://askdoozy.com/integrations/postgres , reads Stripe https://askdoozy.com/integrations/stripe , looks at the code in GitHub https://askdoozy.com/integrations/github , and posts what it found back in the thread. So the first question every engineer asks me is about AI agent security, and it's the right one: you're giving an LLM production credentials? This is the long answer. Most of it is deliberately boring. The interesting part is what's left over once the keys are gone. The short version The agent never holds an API key. Each task runs in a fresh sandbox that holds only a signed, short-lived job token. A credential proxy outside the sandbox checks that token, decides whether the tool is allowed, and adds the real key to that one call. Database queries run read-only, with a timeout and a row cap. That stops keys leaking. It doesn't stop prompt injection misusing the tools you've switched on, so those are the controls that matter most. Why our AI agent never holds API keys Every task runs in its own sandbox, a small VM that exists for that one job and is thrown away after. Nothing in it is a credential for your systems. No API keys, no database password, no model provider key, no GitHub token. When the agent needs something outside, the call leaves the sandbox and goes to a gateway we run. The gateway decides whether the call is allowed, attaches the right credential, forwards it, and passes the response back. The agent sees results. It never sees the secret that produced them. None of this is new. A credential proxy, a service that injects keys into requests so the caller never sees them, is an old pattern, the one you'd draw on a whiteboard in five minutes. That's the point. When the thing on the other side of the boundary is a language model that will read anything it can reach, I want the boundary to be the most boring part of the system. It matters because prompt injection isn't solved, and I don't expect it to be soon. A support ticket, a web page, a PDF someone drops in the thread: any of it can carry instructions. If one gets through, the agent can misuse the tools it has. What it can't do is print a key, write it to a file or send it somewhere, because there isn't one to find. Job tokens: the only credential in the sandbox The sandbox isn't empty. It holds a job token, and every call it makes carries it. If you're thinking "so that's the credential", you're right. The design is about making it a poor one to steal. - It names one workspace and one job, and we sign it. The gateway reads the workspace from the token, never from anything in the request, so no prompt, header or Slack message can point a job at another workspace's tools. - It dies with the job. It expires, and on every call the gateway also checks the job is still running. Once the job finishes or someone stops it, the token is refused, even before it expires. - It pins the model. The model the job started on is written into the token, and the gateway relays only to that one. The claims are small enough to read at a glance: { "tid": "