Guide · 8 min
How to give AI coding agents API keys without exposing them
To let an agent hit a real API you have to hand it a credential. The moment that key lands in a .env the agent can read, it is in the model's context, the logs, and one cat .env from leaking. Here is a safer model, and how it works in practice.
The problem
A .env file is not a security boundary.
Coding agents like Claude Code, Codex and Cursor autonomously read files to do their job. When they hit an authentication error, they look for credentials, and a plaintext .env in the project root is the first place they find one. Worse, in early 2026 it was reported that Claude Code can read .env contents even when the file is listed in .claudeignore or .gitignore. So the common advice, "just add it to .claudeignore", gives a false sense of safety.
Once the value is in the model's context, it can leak in ways you do not control: printed into a log, echoed into a transcript, or exfiltrated by an indirect prompt injection hidden in a file, a dependency, or a pull-request comment. GitGuardian counted nearly 29 million hardcoded secrets in public commits in 2025, with AI-assisted commits leaking at roughly double the base rate.
The options
Three ways people handle it today.
Rotate and pray
Keep the key in .env and rotate often. Cheap, but the secret is still fully exposed to the agent between rotations; a single leaked transcript defeats it.
op run / 1Password
Inject secrets at runtime with op run. Good for humans and CI, but it injects the value into the environment the agent can then read or echo. The model still gets the secret.
Deny rules
Block file reads in settings.json ("deny": ["Read(./.env)"]). This stops the agent reading the file, but then it can't use the key when it legitimately needs one. It's a wall, not a workflow.
A better model
Give the process the secret. Never the model.
The reliable defense is to never place the secret in the model's context in the first place. Instead of the raw value, you put a reference in your .env:
OPENAI_API_KEY=tramya://local/OpenAI/api-key The reference is safe to commit; it names where the secret lives, not the secret. When a command actually needs it, the reference is resolved only at execution time, behind a mandatory human approval. The real value is injected into the subprocess that runs the command, and masked everywhere the model can read: stdout, stderr, logs. The agent gets a working process; it never sees, stores or transmits the key.
This is the idea behind Tramya, a local-first desktop app often described as "1Password for AI coding agents". You store the credential once in an encrypted vault (AES-256-GCM), reference it with tramya://, and run your command through it:
tramya run --env-file .env -- npm run deploy An approval window opens locally: you see which command is asking for which secret, and nothing is injected until you click approve. Even if an injected instruction later tells the agent to print or upload your keys, there is no value in its context to leak.
Honest limits
What this does and doesn't fix.
Once a value is in a subprocess environment, a malicious process in that same tree can read it. That is true of every secrets manager, Vault and 1Password included; no tool can claim otherwise. What this model changes is which actor is on the secret's path: the LLM orchestrating the work, the one whose context gets logged and can be prompt-injected, never holds the value. It shrinks the exfiltration surface from "the model and everything it logs" to "a specific, human-approved subprocess". It is a real reduction, not a magic box.