macOS has the perfect way to protect your API keys
I wanted coding agents to use my API keys without exposing their values, with less setup for each project. macOS Keychain gave me the secure storage, but keeping keys out of agent-written scripts took a local proxy too.
Coding agents need API keys to work with APIs, and I wanted mine to know the keys it could use and use them without sacrificing security.
Ideally, the agent already knows which keys are available and what each one is for, so I can ask it to use my personal OpenAI key and let it handle the setup. I could select the key by its description without retrieving the secret myself or putting it in the conversation.
That was what I built Agent KeyBank for. My keys went in one secrets file, while the agent got a separate catalog of key IDs and descriptions. With instructions to read only the catalog, the agent could select an ID and have the tool prepare an environment file or supply the corresponding key to a program.
That reduced the repeated setup: I could describe a key once and reuse it across projects. Once I had that workflow, I started looking more closely at what it protected and where it still relied on the agent following instructions.
A secrets file is still a readable file
I saw three separate security risks with this setup:
- An agent reading a key during ordinary work. Even without an attack, an agent might read a credentials file while setting up or debugging a program, bringing the secret into its conversation or tool output.
- Malware stealing stored keys. Malicious software running on my computer could search readable files for credentials and send them to an attacker, whether or not I was using an agent.
- Prompt injection causing an agent to send a key away. An attacker could put malicious instructions in content the agent encounters and try to persuade it to retrieve a key or write a script that sends the key to an outside server.
The catalog made the keys easier to use, but it didn't restrict access to the secrets file. I wanted to preserve that workflow while moving the keys into a protected store.
1Password looked like the answer
I already used 1Password, so I was excited to learn that its command-line tool could supply secrets to scripts.
With 1Password's secret references, an environment file can contain a reference such as op://Personal/OpenAI/credential instead of the actual key. A command like this resolves the references and starts the script with the secrets in its environment:
op run --env-file=.env -- python script.pyThe agent could work with references while 1Password handled the secret values, which sounded like exactly what I wanted. I wouldn't have to keep plaintext keys in each project, and the agent could go about its ordinary work without seeing them.
I still needed to check whether this workflow could prevent the agent from reading the secrets.
The CLI also has an op read command that returns a secret's value. If the agent can invoke the authenticated CLI with access to that item, telling it to use only op run doesn't remove its ability to request the value through op read.
I considered blocking my agents from using op read and allowing only the commands they needed. I use several agents across different platforms, though, and keeping those restrictions consistent would mean maintaining a separate set of rules for each one. I decided against that approach because I wanted a cleaner solution, without that ongoing upkeep.
I had hoped this would cover the whole workflow, so it was disappointing to discover that limitation. 1Password offered a useful way to keep plaintext secrets out of project files, but its CLI also supported retrieving them. For my particular use case, I was looking for a way to grant API access without also granting access to the credential's value.
macOS already had the secure store
I was close to moving on when I wondered how hard it would be to build my own version. Could I store the keys securely, expose my own CLI, and leave out every command that displays a secret?
I asked my agent how hard this would be I did some research and discovered that macOS already had the perfect solution: Keychain, its system for storing passwords and other credentials in encrypted databases.
An application can store a secret in Keychain and ask macOS for it later, with Keychain applying access controls to the request. For this project, I used the login Keychain and configured the stored items to trust my signed program. The catalog could remain an ordinary file for the agent to read, since it contained descriptions rather than secret values.
I appreciated how much of the storage work Apple had already done. The encrypted credential store and its APIs gave me a foundation to build on, so I could focus on how agents would use the keys.
At that point, the design seemed close to what I wanted: store the key in Keychain, give the agent a CLI with no “read secret” command, and let the CLI supply keys to scripts.
I still had one part of the design to work through: what happened after a script received the key.
The agent writes the script that receives the key
Suppose my CLI starts an agent-written Python script with the API key in its environment. The script can use that key to call the intended API, but it can just as readily read the environment variable and send its contents to an attacker-controlled server.
A prompt-injected agent wouldn't need a “show secret” command, since it could write the equivalent of “read the API key, then send it to evil.com” into the program I had just authorized to use the key.
The same problem applies to supplying a secret through op run: the script receives the value so it can authenticate, and whoever controls the script controls what it does with that value.
I had been focusing on what the agent could ask the CLI to print, without fully accounting for what it could make its own programs do.
A proxy can use the key on the script's behalf
I wanted to make the key usable only in a request to a host I had approved, such as api.openai.com, without ever giving the credential to the generated script.
A local proxy could provide exactly what I wanted. Instead of calling the provider directly, the script sends an unauthenticated request to a localhost address on my Mac. The proxy handles the authenticated part: it retrieves the key from Keychain, attaches it to the outgoing request, and sends the request to the approved API host before returning the response to the script.
The script still has an address it can call to get its work done, but it never receives an API key it could print, save, or attach to a request to some other server.
Since the proxy controls the outgoing connection, it can also refuse redirects and ignore proxy environment settings that could otherwise redirect an authenticated request. It checks response bodies for the secret and withholds a body if it detects it, in case a server echoes the credential back. That response check isn't a guarantee against every possible encoding or transformation, but it addresses the case where the server returns the secret unchanged.
I approve the allowed host through Touch ID, and the program rejects unsupported hostname forms, including Unicode and punycode, to reduce opportunities for misleading hostnames. The trusted program is also code-signed, so an agent can't preserve its signed identity merely by replacing it with a modified executable of the same name. I have to protect the signing credentials too, since someone who can sign a replacement could undermine that protection.
How the pieces fit together
The resulting workflow keeps the catalog that had worked well for me in KeyBank. The agent reads the key IDs and descriptions, picks the appropriate ID, and uses the CLI to start its program with a local API endpoint. Behind that endpoint, the proxy retrieves the selected credential from Keychain and uses it for requests to the approved host.
I add each key in my own terminal and describe what it's for, while the installed skill teaches the agent how to inspect the catalog and write programs that use the local endpoint. Once that's set up, I no longer have to manage that API key for each project. My agent can use it whenever it needs to, as long as I give it enough context to know which key to use.
The tool asks for Touch ID on each run by default, though I added the option to approve a time window or disable individual run prompts for longer sessions. A verified helper process holds that preference in memory, so the tool returns to asking on every run if the helper exits or I restart the Mac. Changes to the preference and allowed hosts still require approval.
What this protects, and what it doesn't
Keychain supplies the secure storage I wanted, and the proxy keeps the credential out of agent-written programs. There are still limits to what I can claim about the whole system.
In a platform test, a different program could ask the login Keychain for the stored item and trigger a macOS Allow dialog, which released the secret when approved. That's a separate path from the tool's Touch ID prompt, and software with sufficient Accessibility permissions may be able to interact with it. Since the proxy doesn't close that path, I can't promise that an agent with unrestricted control of the Mac has no possible way to obtain a key.
There's also a difference between stealing a key and spending it. While the local proxy runs, another local program can use it to call the approved host, even though it doesn't learn the credential. Those requests can still cost money or perform unwanted actions, so the provider's permissions and spending controls continue to matter.
With those limits in mind, the improvement I can point to is specific: an agent-written script gets API access through the proxy without receiving the raw secret. That lets me keep the convenience of agent-written scripts while reducing their access to the credential itself.
Available as Agent Keylock
I've made the CLI and agent skill public as Agent Keylock. The repository includes the setup instructions, platform-test results, and security limitations.
The install includes the skill that teaches the agent how to choose and use a key. Once I've added a credential, I can ask:
List five available OpenAI models using my OpenAI API key.
The agent selects the key from its description and makes the request through the proxy, without me creating an environment file or handing the secret to its script.