Blog
Why .env files are a security liability for AI agents in 2026
.env files solved a problem from 2011. The AI-agent threat model broke every assumption they were built on — here is what replaces them.
By Jesús E. Viera · · 9 min read
Everyone I've worked with on AI-agent tooling in the last year has a
version of the same embarrassing story. Someone's OpenAI key ended
up in a transcript, someone's GitHub token got committed by an
agent that decided .env was "probably needed for the
build," someone's Stripe secret leaked through a tool that printed
its own environment on start-up. In every case, the immediate
cause was different. The structural cause was the same:
a .env file sitting in the working
directory.
This post is about why .env files — which solved a
real problem very well in 2011 — are actively dangerous in the
AI-agent world in 2026, and what to do instead. No marketing
pitch; I'm not going to tell you which tool to use. But I am
going to tell you what to stop using, and why.
What .env was designed for
.env comes from the twelve-factor app manifesto,
written by Adam Wiggins at Heroku around 2011. Factor III says
config should live in the environment, not in the code. That was a
genuinely good idea. The specific .env file convention
— put your config in KEY=value lines, have your
process loader read them on start — was popularised by the
dotenv npm package and its countless clones.
The original use was narrow: load config during local
development without polluting your shell. You
rails s or node server.js, the process
reads .env, sets some env vars on its own child
environment, and boots. In production you would never ship a
.env; real secrets came from the orchestrator.
The file was always in .gitignore. That was the one
rule. As long as the file stayed untracked, the contract held.
What broke the contract
Between 2011 and 2026, a few things happened that the original design did not anticipate:
- Agents were given shell access. Claude Code,
Cursor's terminal, Devin, Continue, Aider — the default now is
that an LLM can run arbitrary commands in your working
directory. That directory contains your
.env. - Agents search directories aggressively. "Find
out why the app won't start" naturally ends with
cat .env,grep -r SECRET, orls -la. These aren't rogue behaviours; they're exactly what you want the agent to do for non-secret debugging. - Tools log aggressively. Every LLM provider retains prompts and completions for some period. Anything the agent reads and summarizes ends up in those logs. The secret has now left your machine.
- The transcript is the audit trail. Agent sessions are shared in Slack, pasted into bug reports, embedded in screenshots, sent to support. Anything in the context window gets laundered out into places you can't track.
The .env file survives on one implicit assumption:
nothing hostile or even noisy reads it. That assumption
was true for a local dev server. It is not true for an AI agent
with shell access.
Three concrete failure modes I've seen in 2025–2026
1. The "just source it" failure
A developer asks their agent to "set up the environment for this
project." The agent, being helpful, runs
set -a && source .env && set +a and
reports back the variables it loaded. The report is proactively
helpful: "Loaded OPENAI_KEY=sk-proj-…,
GITHUB_TOKEN=ghp_live_…, STRIPE_SECRET=sk_live_…."
Those three values are now permanently in the transcript, which
is permanently in the vendor's retention period.
2. The debug-output failure
The agent is debugging a deploy. It adds a print statement:
console.log('Starting with env:', process.env).
The output is piped back to the agent's context. Now the agent
has every environment variable — including the ones the
.env loader set — and will cheerfully quote them
back when asked.
3. The "commit by mistake" failure
.env is in .gitignore, but
.env.local isn't, or .env.production
isn't, or someone ran git add -f .env six months ago
and nobody noticed. Now the secrets are in the repo, and the
agent — when asked to "review this commit" — reads the diff,
summarises the content, and produces a helpful explanation of
the PR that quotes the token verbatim. This one is particularly
mean because the scan for "oops, the token is in git history"
has to happen on the prose output, not just the diff.
Why the obvious fixes don't fix it
.env.example + direnv
Better than plain .env, but only by degree. The
actual values still live somewhere on disk, readable by any
process the user runs, including the agent. direnv
reduces the blast radius to "when you're cd'd into
this directory", but an agent is always cd'd into
your directory. That's the whole problem.
Encrypted .env (git-crypt, sops)
The file on disk is encrypted — good. The decryption happens
when the process reads it — fine. But at the moment of reading,
the plaintext goes into the process's environment, which means
it goes into the agent's environment the next time the agent
does printenv. You've moved the risk window from
"forever" to "after you decrypt", but the risk window overlaps
precisely with the window in which you're doing work.
Cloud-fetched env (Doppler, Infisical, AWS Parameter Store)
These are strictly better than plaintext files. They centralize
rotation, they audit access, they integrate with your
orchestrator. For server workloads, they're excellent. On your
developer machine when a model has shell access, though, they
boil down to "the secret ends up as a plaintext env var in the
process". The leak point is identical to .env; only
the source of truth changes.
The actual fix: the secret never becomes a string the agent can read
The only robust answer is to stop putting plaintext secrets into any channel the agent can observe. That's stronger than it sounds. "Any channel" includes:
- The shell environment (
printenv,env). - Arguments visible to
psand friends. - Files in the working directory (
cat,grep). - Stdout from the tools the agent runs.
- Stderr, which tools love to leak into.
- Error messages that echo request headers.
- The transcript of commands the agent has written.
If the secret appears as plaintext in any of these, you have not solved the problem; you've just moved the leak point. The architecture that actually works is one where:
- The plaintext lives in a local daemon that encrypts it at rest and holds it in locked memory.
-
The agent references secrets by name, not
value:
curl -H "Authorization: Bearer {{GITHUB_TOKEN}}" …. -
A hook before the command runs rewrites
commandtoresolver -- command. The agent's transcript keeps the placeholder; the actual execution substitutes the value in the resolver's memory and callsexecve. - The resolver's stdout/stderr are piped through a scrubber that replaces any echo of a known secret value with its placeholder before returning to the agent.
ClauLock is one implementation of this. There will be others. The pattern is what matters; the tool is replaceable.
What this looks like in practice
You stop sourcing .env. You stop setting
OPENAI_KEY=… in your shell config. You stop
committing .env.production to a private repo
because "it's only the engineering team who can see it anyway".
In their place, you do this once, per developer, per machine:
# Pull your existing .env into an encrypted local vault.
clsec import dotenv ./.env
# Confirm the file is gone and the vault has the values.
clsec list
# Delete the .env.
rm .env And this, in every shell command the agent writes:
curl -H "Authorization: Bearer {{OPENAI_KEY}}" …
psql "postgres://{{DB_USER}}:{{DB_PASSWORD}}@…"
aws sts get-caller-identity --profile production # uses AWS profile
# which in turn uses
# {{AWS_ACCESS_KEY_ID}}
# via clsec-exec
The agent writes the command exactly as shown. The transcript
records it exactly as shown. Somewhere below the model's
awareness, a resolver swaps the placeholders for real values in
the exact instant before execve. The agent never
learns the plaintext, can never be tricked into revealing the
plaintext, and can never accidentally paste the plaintext into
a Slack message.
What you should do this week
Regardless of which tool you end up with:
- Audit every
.env*file in every repo you work on. Grep for anything matchingsk_,ghp_,AKIA,xoxb-. If you find plaintext, rotate those credentials today. Don't rationalize. - Check your git history for committed secrets.
git log --all -p -- '*.env*'is a good starting point. Tools liketruffleHogandgitleaksgo further. - Decide on a replacement strategy. If you pair
with an AI agent daily, your replacement needs to support the
{{PLACEHOLDER}}-in-argv pattern or something equivalent. Setting an env var doesn't count. - Delete the
.envfiles. Actually delete them. Don't move them to~/secrets/.env.bak. The file format is the problem; making another copy doesn't help.
Why I care enough to write this
I built ClauLock because I leaked my own OpenAI
key through Claude Code in January 2026 and spent the next week
being annoyed at myself that the tool I'd built my entire
workflow around had a foot-gun this obvious baked in. I don't
expect my solution to be the last word, and I don't think
everyone will pick it. But I'm fully confident that
.env files plus AI agents plus shell access
equals inevitable leaks, and that part is not
controversial — it's just arithmetic.
Pick a replacement. Any replacement that enforces "the agent
never sees plaintext" is better than the status quo. If you end
up at ClauLock, great. If you end up at something else, also
great. Just don't end up at a .env file with Claude
Code pointed at it. That's the one thing that's not going to
keep working.
Comments, corrections, or war stories: [email protected]. Part 1 of this pair covers what the replacement architecture has to look like.