Assume the Agent Read Your .env

Ask a coding agent why the app can’t reach the database. A sensible first step, for the agent or for you, is to open .env and check the connection string, and now the database password is in the session transcript, a file that outlives the bug.
I don’t think the fix is hiding secrets from the agent. Reading files and running commands is its job, and anything in the working directory or shell environment is one command away. So I assume it has read all of it, and make what it read useless to anyone else.
“The provider doesn’t train on my data” doesn’t help. The transcript sits in plain text under your home directory, gets copied by backups and sync folders, and gets pasted into bug reports and team chats. And it’s the smaller copy: everything the agent read also went to the provider’s API and sits in their request logs, and in any gateway between you and them, for as long as their retention says. Same answer: make what it read useless.
Two moves do most of the work: .env holds references that a secrets manager’s run wrapper resolves for one command at a time, and development credentials expire within hours. Everything else goes on top of those.
Per-command secrets
Since the file will be read, keep nothing in it that matters. Put references in .env (where each value lives in your secrets manager) and let the manager’s run-style wrapper, something like secrets run -- npm test, resolve them into the environment of that one command. The value exists only while the command runs, so nothing sits in the file, your shell or your history, and some wrappers also mask the values in the output.
This won’t stop a determined agent. If it can run the wrapper, it can wrap printenv. What changes is the default: a secret reaches the transcript only when the agent goes looking, in a command you can see and refuse.
Isn’t this against twelve-factor, which says to store config in the environment? Twelve-factor separated config from code, and its litmus test (could the codebase be open-sourced without compromising any credentials?) is about the repo. It never considered another reader of the environment, and it preferred environment variables partly because, unlike config files, they’re hard to commit by accident. A .env is a config file in the working directory. The wrapper keeps the twelve-factor shape: the app still reads config from its environment.
The pineapple test
The first thing most people reach for is a deny rule: block reads of .env* and credential files (~/.aws/credentials, ~/.npmrc, ~/.kube/config) in the agent’s permission settings. Keep it, but test it, since what it covers depends on the agent. A rule on the file-read tool may not cover the shell at all, leaving cat .env open. Even agents that catch the obvious shell commands can miss a grep -r from the project root, a script that opens the file itself, or docker compose config, which prints your compose file with its variables filled in from .env.
Put a dummy line such as LEAK_TEST=pineapple-42 in .env and ask the agent for the value three ways: read the file, cat it, and grep -r LEAK_TEST .. If any of them answers, the rule is a speed bump. Blocking for real takes an OS-level sandbox that denies the path to every process.
What the agent inherits
Every command the agent runs inherits your exported variables, so one env while it chases a path problem copies every token you exported into the transcript. If a shell plugin loads .env when you enter the directory, that includes the file you just locked down. Cloud CLIs keep their logins in your home directory, and the agent runs as you.
Start agent sessions from a stripped environment (env -i HOME="$HOME" PATH="$PATH" TERM="$TERM" <agent>). That hides exported tokens until the agent opens a shell that sources your .bashrc or .zshrc, which puts them back, so move the exports out of those files too. It doesn’t hide your logins; for those, use a separate OS user or a container with a clean home. What the agent does need, give it short-lived and narrow: a cloud session that expires within the hour instead of a permanent access key, a read-only token for one repository instead of your everyday one.
None of this tells you a leak happened. A canary does: a real credential that grants nothing useful, in .env under a believable name, with an alert on any use. On AWS, for example, the audit log records denied calls with the caller’s identity and IP address, so the alert is a filter on the canary’s user. Pick a name your code never reads, or your own tests set it off, and use one per machine so the alert says which copy got out.
A grep through your history
Find where your agent keeps session history (often a dot-directory under your home) and search it for well-known formats. AWS access key IDs start with AKIA (long-lived) or ASIA (temporary). GitHub tokens start with ghp_, github_pat_ and similar prefixes GitHub added so leaked tokens are easy to spot (replace ~/.agent-history/ with yours):
grep -rlE 'A[KS]IA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{30,}|BEGIN [A-Z ]*PRIVATE KEY' ~/.agent-history/
A database password has no prefix, so search for your actual values too:
grep -v '^#' .env | cut -d= -f2- | tr -d "\"'" | awk 'length >= 8' | grep -rlFf - ~/.agent-history/
Run both yourself, not through the agent, and keep -l so only file names print, or the check copies your secrets into a new transcript. Expect noise from the second one: values like localhost or production match everywhere, so look at what surrounds each hit. Deleting a file doesn’t undo backups or sync, so treat anything you find as leaked. Then run the LEAK_TEST check.
Friction
The secrets manager is one more tool on every laptop and in CI, and when it’s down, local development stops. Without your logins the agent can’t deploy, query staging or use your SSH agent (env -i drops SSH_AUTH_SOCK), so you do those yourself or grant access per task. Short-lived credentials expire mid-session. None of it helps once you hand the agent a secret on purpose.
It’s worth it for a transcript you can share without thinking twice, because the worst it holds is a reference, an expired token or a canary.
Takeaways
- Anything in the working directory, the environment or your home can end up in a transcript that gets synced and shared.
- Test deny rules through the shell and indirect reads before trusting them.
- Keep references in
.envand inject values one command at a time. - Run the agent without your logins, with credentials that expire soon and reach little.
- A canary tells you when a leaked key gets used; a grep tells you what’s already out there.
Start with the grep, and rotate whatever it finds today.