Nº 009/Engineering/7 min read/
Designing for Zero-Knowledge
Before any code was written, there were a set of decisions that needed to be made about what AgentSecrets would and would not do. These decisions constrained everything that came after — the storage model, the proxy architecture, the SDK design, the cloud sync approach. Getting them wrong early would have meant rebuilding later.
This article covers those decisions: what we considered, what we ruled out, and why the architecture ended up where it did.
What zero-knowledge means here, precisely
Zero-knowledge is a term that gets used loosely, and in cryptography it has a precise technical meaning that is broader than what AgentSecrets uses it to mean. In the context of AgentSecrets it means something specific and narrower: the server holds ciphertext it cannot decrypt.
When you store a credential in AgentSecrets and sync it to the cloud, what reaches the server is an encrypted blob whose decryption keys live in your OS keychain and never leave your machine. The server has no access to those keys and no mechanism to request them, which means that someone with full access to the AgentSecrets database would find only ciphertext with no path to the plaintext.
That guarantee is structural, not a policy. The decryption keys are never transmitted to the server, there is no server-side decryption path, and the guarantee holds regardless of what happens to the server.
That definition shaped the storage decision before anything else was designed.
How we got to the OS keychain
The first instinct for local credential storage is usually encrypted files. Keep a file on disk, encrypt its contents, require a password or key to decrypt. It is simple and it works for many use cases.
The problem with encrypted files for this specific use case is that the file is still a file. It lives at a known path. Any process running as the same user can attempt to read it. The encryption protects the contents if the file is extracted, but on a running machine with a logged-in user, the attack surface is the decryption key and whoever or whatever has access to it.
A local database with conditional access was the next consideration — something that required specific conditions to be met before a read was permitted, with access control built into the storage layer rather than just the encryption. That is closer to the right model, but it means building and maintaining the access control logic, and it means another process running that can be targeted.
The OS keychain is what both of those approaches were trying to approximate, without the tradeoffs. macOS Keychain, Linux Secret Service, Windows Credential Manager — these are purpose-built secure storage systems maintained by the operating system, with access control enforced at the OS level. Other processes cannot read keychain entries belonging to a different application without explicit user consent. The user-scoped encryption is handled by the OS, so the attack surface becomes the OS security model rather than our implementation of it.
Storing credentials in the OS keychain meant we were not in the business of building secure storage. We were using secure storage that already existed, already worked, and was already trusted by the operating system the developer was running.
The proxy approaches we rejected
The architecture question was: how does the agent get the credential value into an API call without holding it. We considered three approaches before landing on transport-layer injection.
Passing encrypted secrets to the agent
The first idea was to give the agent an encrypted form of the credential and have the agent pass that ciphertext to the API. The API would decrypt it server-side.
This does not work for a simple reason: the APIs AgentSecrets needs to support are third-party services like Stripe, OpenAI, GitHub, and SendGrid. None of them accept encrypted credential blobs and decrypt them on arrival. They accept bearer tokens, API keys, and basic auth credentials in plaintext. The encryption has to be undone before the API call is made, which means the plaintext exists somewhere before the request goes out.
Giving the agent the key and clearing it from context
The second approach was to give the agent the credential value but build a mechanism to clear it from the agent's context immediately after use — inject it, use it, then wipe it.
This is the intuitive answer and it is worth explaining precisely why it does not work.
The context window of an LLM is not a memory location you can target and overwrite. Once a value appears in context, it has been processed by the model. The idea of "clearing" it assumes a level of control over the model's internal state that does not exist from the outside. You can stop sending the value in future messages. You cannot undo the fact that it was already there.
More practically: the window between "inject" and "clear" is the attack window. An injected prompt instruction that arrives during that window has access to the value. Making that window smaller reduces the risk but does not eliminate it, because the attack surface remains as long as the value was ever present.
Transport-layer injection
The approach that works is moving the injection point outside the agent entirely. The agent sends a credential name to a local proxy. The proxy resolves the value from the OS keychain, injects it into the outbound HTTP request, and forwards the request to the API. The agent receives the response.
The credential value never enters the agent's context because it is injected at a layer the agent does not control and cannot read — the proxy sits between the agent and the network, so the agent knows it is making an API call but has no visibility into what authenticated it.
This is why the SDK has no get() method. There is no operation that retrieves a credential value into the calling code. The only available operations are call and spawn — both of which route through the proxy, both of which keep the value on the proxy side of the boundary.
Why local-first mattered
A cloud-first architecture was possible. Resolve credentials through a remote service, inject at a hosted proxy, return the response. That model would work and it would not require anything running on the developer's machine.
We chose local-first for a specific reason: dependency. A cloud-first architecture means every credential resolution depends on network availability and on our infrastructure being up. For a developer running agents on their laptop, that is friction. For an enterprise that cannot route credential resolution through external infrastructure, it is a non-starter.
The local proxy means credential resolution works offline, works without trusting our uptime, and works without any data leaving the developer's machine unless they explicitly enable cloud sync. The zero-knowledge guarantee is easiest to reason about when the decryption and injection happen locally, on hardware the developer controls.
The cloud resolver is on the roadmap — it unlocks serverless environments, CI/CD pipelines, and production deployments where a local proxy cannot run. When it ships, the zero-knowledge guarantee will hold there too, with decryption happening in-memory at request time and no plaintext persisted anywhere. But the local proxy is the foundation, and keeping it as the default keeps the trust model simple.
What these decisions cost
Every architectural decision has a cost and it is worth naming them.
The OS keychain dependency means the proxy needs to run on a machine where the user is logged in, so headless environments and containers require the cloud resolver path, which is not yet shipped. That is a real gap for some use cases.
The local-first model means developers need the proxy running before their agent can make authenticated calls, which is one more process to manage and one more thing that can fail. The error messages try to make this recoverable, but the friction is real.
The no-retrieval constraint in the SDK means developers cannot write code that reads credential values for any purpose, including legitimate ones like displaying a masked version of a key in a UI, and those use cases require a different path.
These are the tradeoffs the architecture made to achieve the zero-knowledge guarantee. They were made deliberately, and understanding them is part of understanding why the architecture works the way it does.
Part 03 covers why the proxy is not the product — what the infrastructure layer actually is and what it means that the proxy is one component of a larger system rather than the thing AgentSecrets is.
AgentSecrets is open source and MIT licensed. The full architecture is at agentsecrets.theseventeen.co. The repository is at github.com/The-17/agentsecrets.