Your coding agent can read your .env
You open a repository, start a coding agent and ask it to fix a failing integration test. To do that, it reads files and runs commands with your user account. If a .env file sits next to package.json, it is within reach, and nothing about that requires the agent to misbehave.
What an agent actually touches
Claude Code, Cursor's agent, GitHub Copilot's agent mode and Codex all work in roughly the same way. They read and search the files in your workspace, and they run shell commands as you. Whatever those reads and commands return becomes part of the conversation: it is sent to the model and, depending on the tool, saved in a session transcript on disk.
That is the point of an agent. It also means a plaintext secrets file in the project is one tool call away from leaving your machine.
Four ordinary ways a .env gets read
None of these needs a malicious model or a prompt injection. They are the agent doing what you asked:
- Debugging a config error. The database connection fails, so the agent opens
.envto checkDATABASE_URL. Every line of the file is now in the context, not just the one it needed. - Searching. A recursive grep for a variable name matches
.envtoo, and prints the matching line with its value. - Printing state. To diagnose a failure, the agent runs
printenvor a command that dumps the resolved configuration. The output lands in the transcript. - Committing.
git add -Aand a commit, in a repository where.envwas never ignored, put the file in history. It stays there after you delete it.
Each one is a small accident. Over weeks of agent sessions, the realistic assumption is that a plaintext .env in the workspace gets read at some point.
Ignore files and deny rules help, within limits
Agents have settings to keep files out of reach. Claude Code supports permission deny rules such as Read(./.env), and Cursor reads a .cursorignore file. Both are worth setting up. Both vendors also document where they stop:
- The Claude Code permissions docs say that
Readdeny rules cover its built-in file tools and the file commands it recognizes in Bash, such ascat, but not a script or other subprocess that opens the file by itself. They also say a Bash rule matches the command text, so it is not a security boundary around a program. - The Cursor ignore-files docs say the terminal and MCP tools used by the agent can't block access to files listed in
.cursorignore.
That is an honest description of the problem. Once an agent can run a shell, a file on disk is readable by some path. The more reliable fix is to not have the file.
Take the file away
envsec stores each value in the credential store your OS already has: the macOS Keychain, the Secret Service on Linux (through secret-tool), or Windows Credential Manager. A small SQLite database keeps key names and timestamps, never values. Moving a project over looks like this:
$# Find plaintext .env files under the current directory (report only)$envsec rescue$# Import one file into a context, then delete the file$envsec -c myapp.dev load --input .env$rm .env$# Start the app with every secret of the context in its environment$envsec -c myapp.dev run --inject 'npm run dev'$# Or reference a single secret in the command$envsec -c myapp.dev run 'psql "{database.url}"'load turns DATABASE_URL into the key database.url, and run --inject turns it back into DATABASE_URL in the environment of that one process. A {key} placeholder works without --inject: envsec passes the value in an environment variable and rewrites the placeholder into a reference to it, so the value never appears in the command text. When the process exits, the values go with it.
There is no file for the agent to open while debugging, nothing for a grep to match, nothing to index and nothing to commit. If you have many projects, the rescue post covers finding and importing every .env in a directory tree in one pass.
What envsec does not stop
This part matters more than the rest. An agent that runs shell commands runs them as you, and envsec is a program you are allowed to run. So:
- It can run
envsec -c myapp.dev get database.url, and the value prints. On macOS, envsec writes and reads Keychain items through Apple'ssecuritytool, whose help text says the application that creates an item is trusted to read it without a warning; in my tests, reads raise no dialog. On Linux, a keyring that was unlocked at login typically answers any process in your session. Windows Credential Manager is readable by processes running as you. - If you start the agent inside
envsec shell, or in a terminal where you raneval "$(envsec env)", the secrets are in the agent's own environment. Every command it runs inherits them, andenvprints them all. - A process started with
envsec run --injectcan print its own environment, and so can a test that fails with a dump ofprocess.env.
| Plaintext .env | envsec | |
|---|---|---|
| Agent opens or greps the file | exposed | nothing to read |
| Editor indexes the workspace | exposed | nothing to index |
| Agent runs git add -A | committed | nothing to commit |
| Agent runs a command that prints it | exposed | exposed |
| Agent runs in a shell with secrets loaded | exposed | exposed |
envsec removes the accidental paths: the file read while debugging, the grep hit, the indexed file, the commit. It does not defend against an agent that has been told to fetch your secrets, whether by you or by a prompt injection hidden in a README or an issue. Nothing that runs as your user can fully do that. The envsec threat model goes through each of these limits in detail.
A setup that holds up
This is what I do, and what I would suggest to anyone using an agent on a project with real credentials:
- Start agents from a clean shell. Not inside
envsec shell, and not aftereval "$(envsec env)". Useenvsec runfor the commands that need secrets, so they live only as long as that process. - Read commands before approving them. If you have allowed most commands to run without asking, add ask rules for the envsec commands that print or write values. In Claude Code, that is a few lines in
settings.json. Since these rules match the command text, treat them as a prompt, not a wall. - Give the agent a dev context. Contexts are names like
myapp.devandmyapp.prod. They are not access control, but they decide what--injectloads. Keep production values in a context you never use in an agent session. - Use test or narrowly scoped keys, and date them. Store them with
--expires. Expiry is a reminder, not a lock:getwarns about an expired secret andenvsec auditlists it, but the value stays readable until you rotate it at the provider. - Look at OS-level sandboxing. Claude Code has a sandbox mode that restricts the filesystem and network access of the commands it runs. I have not tested it against keychain access, so check what it allows before relying on it for this.
- Rotate anything that reached a transcript. Deleting the conversation afterwards does not undo what was already sent to the model.
To check where you are and clean up an earlier eval:
$# envsec shell sets this variable in the shell it starts$echo $ENVSEC_CONTEXT$# Undo an earlier eval of envsec env in bash or zsh$eval "$(envsec -c myapp.dev env --unset)"The ask rules for Claude Code, in .claude/settings.json or ~/.claude/settings.json:
{
"permissions": {
"ask": [
"Bash(envsec get *)",
"Bash(envsec env *)",
"Bash(envsec env-file *)",
"Bash(envsec share *)"
]
}
}And a key with a rotation date:
$envsec -c myapp.dev add stripe.key --expires 30d$# Later: list expired and soon-to-expire secrets$envsec auditIn short
A plaintext .env next to a coding agent is a file that will be read sooner or later, through ordinary work. Moving the values into the OS credential store removes those accidental paths. It does not make an agent with a shell trustworthy: for that you still need approvals, scoped credentials and rotation. Start with envsec rescue to see what is on disk, check the docs for the full command list, and read the threat model before deciding how much to rely on it.