All posts
6 min readDavid Nussio

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:

  1. Debugging a config error. The database connection fails, so the agent opens .env to check DATABASE_URL. Every line of the file is now in the context, not just the one it needed.
  2. Searching. A recursive grep for a variable name matches .env too, and prints the matching line with its value.
  3. Printing state. To diagnose a failure, the agent runs printenv or a command that dumps the resolved configuration. The output lands in the transcript.
  4. Committing. git add -A and a commit, in a repository where .env was 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:

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:

Terminal
# 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:

Plaintext .envenvsec
Agent opens or greps the fileexposednothing to read
Editor indexes the workspaceexposednothing to index
Agent runs git add -Acommittednothing to commit
Agent runs a command that prints itexposedexposed
Agent runs in a shell with secrets loadedexposedexposed
What changes when the values move from a file to the OS credential store.

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:

  1. Start agents from a clean shell. Not inside envsec shell, and not after eval "$(envsec env)". Use envsec run for the commands that need secrets, so they live only as long as that process.
  2. 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.
  3. Give the agent a dev context. Contexts are names like myapp.dev and myapp.prod. They are not access control, but they decide what --inject loads. Keep production values in a context you never use in an agent session.
  4. Use test or narrowly scoped keys, and date them. Store them with --expires. Expiry is a reminder, not a lock: get warns about an expired secret and envsec audit lists it, but the value stays readable until you rotate it at the provider.
  5. 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.
  6. 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:

Terminal
# 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:

json
{
  "permissions": {
    "ask": [
      "Bash(envsec get *)",
      "Bash(envsec env *)",
      "Bash(envsec env-file *)",
      "Bash(envsec share *)"
    ]
  }
}

And a key with a rotation date:

Terminal
$envsec -c myapp.dev add stripe.key --expires 30d
# Later: list expired and soon-to-expire secrets
$envsec audit

In 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.