Skip to content
Founders$49 once → 2 years of Pro ($98 value)Become Founder →
ClauLock

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:

  1. 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.
  2. Agents search directories aggressively. "Find out why the app won't start" naturally ends with cat .env, grep -r SECRET, or ls -la. These aren't rogue behaviours; they're exactly what you want the agent to do for non-secret debugging.
  3. 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.
  4. 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 ps and 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:

  1. The plaintext lives in a local daemon that encrypts it at rest and holds it in locked memory.
  2. The agent references secrets by name, not value: curl -H "Authorization: Bearer {{GITHUB_TOKEN}}" ….
  3. A hook before the command runs rewrites command to resolver -- command. The agent's transcript keeps the placeholder; the actual execution substitutes the value in the resolver's memory and calls execve.
  4. 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:

  1. Audit every .env* file in every repo you work on. Grep for anything matching sk_, ghp_, AKIA, xoxb-. If you find plaintext, rotate those credentials today. Don't rationalize.
  2. Check your git history for committed secrets. git log --all -p -- '*.env*' is a good starting point. Tools like truffleHog and gitleaks go further.
  3. 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.
  4. Delete the .env files. 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.