Note
No Door on This Side
The instinct is to hide the key better: blindfold the agent, encrypt the file, add a guard. The better move is to make sure the key was never on the agent's side of the wall at all. That idea is thirty years old, it has a hard ceiling, and the ceiling is the honest part.
For a while I was trying to solve the wrong picture.
The picture in my head was an agent walking into the room where my API keys are kept, wearing a blindfold. So everything I considered building was a better blindfold. Encrypt the file so it cannot read it. Add a hook so it cannot open it. Scrub the value out of the logs afterwards.
Every one of those designs shares a flaw, and it is the same flaw each time: the agent is in the room. Once it is in the room, protection is a policy you enforce against it forever, on every path, and you have to be right every single time. It has to be lucky once.
The correction is not a better blindfold. It is a wall.
The agent is not in the room with its eyes covered. It is in a different room, sliding paper under the door. There is no door on its side.
Once I had that picture the design stopped being a series of clever restrictions and became one boring piece of plumbing.
The plumbing
An outbound API call goes like this.
The agent builds a request to some provider and leaves the credential field as a placeholder. It sends that request to a local daemon. The daemon checks the destination against its policy, looks up the real credential, writes the credential into the request itself, and forwards it upstream. The provider answers. The daemon passes the answer back.
The agent got exactly what it needed: the response. The key was never in its process memory, never in its terminal output, never in its context window, never in a transcript uploaded to a model API. There was no moment at which the value existed on the agent's side of the wall.
That last clause is the whole design. Not "the agent was prevented from reading the key." The key was never there to read.
The difference matters because of what the realistic threat actually is. It is not someone stealing my laptop. It is an agent reading a config file in the ordinary course of doing its job, pulling those values into its context, and that context going up to an API and sitting in a log. That happens quietly, on a normal Tuesday, with nobody doing anything wrong. A design that makes the value absent defeats it completely, because absence is not a rule anyone can forget to apply.
This is thirty years old
I want to be clear that I did not invent anything here.
ssh-agent has worked exactly this way since 1995. It holds your private key. When a server challenges you, the agent performs the signing operation and hands back the signature. No SSH client has ever seen your key material, and nobody finds this exotic. It is how you already log into servers, today, probably several times a week.
We had this solved, and then we started handing AI agents plain text config files because that is how application config has worked since forever, and the pattern we had been relying on for three decades did not get carried across.
The generalisation is the useful part. The agent should receive a capability, not a credential.
For an HTTPS call, the capability is "this request gets sent, correctly authenticated." For anything that is not an outbound HTTPS call, a database password or a signing key, the same rule applies one level up: the daemon performs the operation and returns only the result. The agent gets the query results. It never gets the connection string.
There is a corollary that cannot be bent without collapsing the whole thing. The agent never receives ciphertext, and never receives a decryption key. Hand it either and you are back to the blindfold, because a thing that holds ciphertext plus a key holds the secret with extra steps. What it receives is a reference: a name that means something to the daemon and nothing to anyone else.
Where the risk actually is
When people hear "local proxy holds the keys," the first objection is usually that the agent could just unset the proxy setting and go direct.
It could. And then its request has no credential in it, so the call fails. That is self-denial, not exfiltration. An agent that bypasses the vault has removed its own ability to work, which is an annoying failure mode and not a security one.
The genuine risk runs the other direction, and it is worth naming plainly because it is the one bug class that could actually cost a key: an agent routes a request to a host it controls, and a loose matching rule cheerfully attaches the real credential to it.
That is why host matching has to be exact from the first commit. Never suffix matching, never wildcards. A rule that says "anything ending in the provider's domain" is a rule that trusts a subdomain someone else can register. Every credential names the exact hosts it may ever be sent to, and anything else is refused.
This is also why the injection logic lives in exactly one place. There is a plan to add a second transport later for tools whose base URL cannot be changed, and the constraint written down before any of it was built is that both transports share one implementation of "which credential may go to which host." Duplicating that gives the one bug class that actually leaks a key two separate places to live.
The instinct was right, the mechanism was wrong
At one point I proposed webcam face recognition. Fingerprint my face, store the template locally, require a face check to open the vault. Continuous verification that a human is present.
It got dropped, and the reasoning is worth keeping because the instinct underneath it was correct.
The requirement, "a human must be physically present," is exactly right. The webcam is the wrong way to get it, for four reasons. It does not solve the case I proposed it for, since that case was an attacker with root, and a camera check is software on the same machine that root can patch out or feed a saved frame to. A laptop camera is a plain 2D sensor, and 2D face matching is routinely beaten by a photograph on a phone screen, where Face ID uses an infrared depth projector inside secure hardware. An always-on camera costs battery, privacy, and a false rejection every time you glance away. And a face cannot be revoked. A leaked API key rotates in thirty seconds; a leaked biometric template is permanent. Storing one to protect the other is a bad trade in both directions.
The same requirement, done properly, is Touch ID with a key created inside the Secure Enclave under the biometryCurrentSet flag. The key never leaves the Enclave, and the Enclave itself declines to perform the unwrap without a live fingerprint. That is hardware refusing, not software checking, and root cannot patch out a refusal that happens on a different chip.
One detail here is worth carrying around, because it corrects a thing most people assume. The Secure Enclave holds P-256 elliptic curve keys and nothing else. You cannot put an API key inside it. What you do is use an Enclave key to unwrap the store's symmetric key. The Enclave protects the door, not the contents.
And it comes with a trap that has to be designed for on day one: with biometryCurrentSet, adding or removing a fingerprint, or changing the machine password, makes every protected item permanently inaccessible. That is Apple's intended behaviour, not a bug. It means an encrypted break-glass export is mandatory rather than optional, and its format has to be settled while the first version is being written, or a store created today stops being recoverable the moment the hardware layer lands.
The honest ceiling
Now the part the piece would be dishonest without.
None of this stops root.
If something has root on my machine, it can read the daemon's memory while the daemon is using a key, and it can disable whatever detector I build to notice. No application-level design changes that. Anyone telling you their local secrets tool defeats a root-level attacker is selling something.
The honest framing is that if something has root on my laptop, the API keys are not the largest thing I just lost. What I control is not whether that attack is possible. It is whether the damage is bounded and loud: keys scoped per project, spend capped per project, every use logged with a timestamp and a project name, fake credentials sitting in the store that nothing legitimate will ever touch so that any request for one is an unambiguous alarm, and rotation one action away.
There is a smaller version of the same honesty about the write-only behaviour. I wanted the semantics GitHub Secrets has: once saved, even I cannot read the value back, and to change it you replace it. That is worth implementing, but it is a user interface policy, not a cryptographic guarantee. A machine that can decrypt in order to use can decrypt in order to display. GitHub can genuinely enforce it because the secret lives on their server where I have no root. On my own Mac I always have root.
Its real value is behavioural, and it is substantial. It kills the habit of peeking at a key and pasting it into a terminal, a chat window, or a prompt. That habit is where keys actually leak in practice, far more often than anything an attacker does.
Where it actually stands
I will not describe this as finished, because it is not, and the gap is the interesting part.
What works today: an encrypted store, a plaintext catalog agents can read freely so they know what exists without ever seeing a value, the injection core with exact host matching, per-project attribution with cryptographic project identity rather than a self-declared string an agent could spoof, a usage meter, the local endpoint transport, a command line tool, and an end-to-end test that proves the upstream received the credential and the caller never did.
What does not exist yet: the service account. The design puts the daemon under its own background user so the store file is owned by that user and unreadable by mine, which converts "you cannot read this" from a policy my code enforces into an answer the kernel gives. That is the single change that makes the boundary real. It is designed, it is written down, and it is not installed. I checked the machine while writing this. There is no such account on it.
So today the store rests on encryption at rest and on convention, not on the kernel. The graphical app, the Secure Enclave wrapping, Touch ID and the honeytoken alarms are all still ahead.
Saying that out loud costs me the cleaner version of this post. It is also the only version worth publishing, because a security claim you cannot check is worth nothing, and the fastest way to find out whether someone is telling you the truth about their threat model is to see whether they will tell you where it stops.
The thing I would take from all of this, if you build with agents and you have not thought about it yet: stop asking how to stop your agent from reading the key. Ask why the key is on its side of the wall at all.
Most of the time, it does not need to be there. It needs the door answered, not the room opened.
This came out of the same work as the guard that could not tell looking from taking. The project is LoopsVault, MIT licensed, at github.com/on-play/loopsvault.
Sources
ssh-agent(1), the authentication agent, in continuous use since 1995: https://man.openbsd.org/ssh-agent.1- OpenSSH manual pages: https://www.openssh.com/manual.html
- LoopsVault: https://github.com/on-play/loopsvault
- Infisical Agent Vault, the closest prior art and an explicit reference design: https://github.com/Infisical/agent-vault
- Agent Vault announcement: https://infisical.com/blog/agent-vault-the-open-source-credential-proxy-and-vault-for-agents
- Apple,
SecureEnclave.P256, on the curve restriction: https://developer.apple.com/documentation/cryptokit/secureenclave/p256 - Apple, biometric security overview: https://support.apple.com/guide/security/biometric-security-sec067eb0c9e/web
- fnox, injection into a child process rather than brokering: https://github.com/jdx/fnox
- SOPS: https://github.com/getsops/sops and age: https://github.com/FiloSottile/age
Audio transcript
You are listening to "No Door on This Side," a note by Jain Yagi.
For a long time I was solving the wrong picture.
The picture in my head was an agent walking into the room where my API keys are kept, wearing a blindfold. So every design I considered was a better blindfold. Encrypt the file so it cannot read it. Add a hook so it cannot open it. Wipe the value out of the logs afterwards.
All of those share one flaw. The agent is in the room. Once it is in the room, protection becomes a rule you enforce against it forever, on every possible path, and you have to be right every single time. It only has to be lucky once.
The correction is not a better blindfold. It is a wall. The agent is not standing in the room with its eyes covered. It is in a different room, sliding paper under the door, and there is no door on its side.
The plumbing that follows from that is almost boring. The agent builds its request and leaves the credential field as a placeholder. It sends that to a local service. The service checks where the request is going, writes the real credential into it, and forwards it. The provider answers, and the answer comes back. The agent got what it needed, which was the response. The key was never in its memory, never in its terminal, never in its context window, never in a transcript uploaded to some model API. There was no moment when the value existed on the agent's side of the wall.
That is the whole idea. Not that the agent was prevented from reading the key. The key was never there to read.
And I want to be honest that I invented none of this. The tool that holds your SSH key has worked exactly this way since nineteen ninety five. It keeps your private key, answers the challenge on your behalf, and no client has ever seen the key material. Nobody finds that exotic. It is how you already log into servers. We had this solved, and then we started handing agents plain text config files because that is how application config has always worked, and the thirty year old pattern did not get carried across.
The general form is that the agent should receive a capability, not a credential. For anything that is not an outbound web call, a database password or a signing key, the same rule applies one level up. The service performs the operation and returns only the result. The agent gets the query results. It never gets the connection string.
People usually object that the agent could just bypass the local service and go direct. It could, and then its request carries no credential, so the call simply fails. That is self denial, not a leak. The real risk runs the other way. An agent sends a request to a host it controls, and a sloppy matching rule attaches your real credential to it. Which is why host matching has to be exact. Never wildcards, never matching on the end of a domain name. A rule that trusts anything ending in a provider's domain is a rule that trusts a subdomain somebody else can register.
Now the part this would be dishonest without. None of it stops an attacker who already has root on the machine. Root can read the service's memory while it is using a key, and can switch off whatever detector you built to notice. No application level design changes that, and anyone claiming otherwise is selling you something. The honest framing is that if something has root on my laptop, the API keys are not the biggest thing I just lost. What I actually control is whether the damage is bounded and loud. Keys scoped per project. Spending capped per project. Every use logged. Fake credentials sitting in the store that nothing legitimate will ever touch, so that any request for one is an unambiguous alarm.
And I will not call it finished. The store is encrypted, the catalog works, the injection works, the metering works. But the design puts the whole thing under its own background account so the operating system itself refuses to hand my agent the file, and that account is not installed yet. I checked while writing this. It does not exist on my machine. So today it rests on encryption and on convention, not on the kernel.
Saying that costs me the cleaner version of the story. It is the only version worth publishing, because the fastest way to find out whether someone is telling you the truth about security is to see whether they will tell you where it stops.
Here is what I would take from it. Stop asking how to keep your agent from reading the key. Ask why the key is on its side of the wall at all. Most of the time it does not need to be there. It needs the door answered, not the room opened.


