Your .env File Isn’t Ready for AI Coding Agents

Your .env File Isn't Ready for AI Coding Agents

I had been using Claude Code for a while when I decided it was time to audit everything that had accumulated around it: CLAUDE.md, permission rules, and the rest of the settings.

So I did what most of us would probably do. I asked Claude to help me review them.

One finding stood out:

“You have database credentials stored in your settings file.”

They were development credentials, and there had been no breach. But the way they got there exposed a flaw in my workflow.

Several times, I had asked Claude to connect to a development database and perform a specific task, telling it that the credentials were available in .env. Claude generated the command and asked for permission. I recognized the command and what it was intended to do, so I selected “Yes, always” to avoid approving the same operation repeatedly.

What I had not considered was how that approval would be recorded. The command was persisted after the credentials had been resolved, so the settings file contained the complete command—including the credentials in plaintext.

That was already a problem in development. But it immediately raised a harder question:

What happens when the issue you need help debugging exists only in production?

There will be cases where we want an AI coding agent to inspect logs, query a database, or trace an integration problem. That does not mean it should receive a production password or unrestricted credentials.

At that point, I stopped treating secrets as only a file-storage problem. Moving them from .env into another file would simply relocate the risk.

AI coding agents should be given the access they need, not the credentials that provide it.

That is the boundary this article is about.


The three changes that matter

1- Block direct access to secret-bearing files.

Claude Code supports Read deny rules, and they are useful. But Anthropic’s own documentation describes some of that enforcement as best-effort. The rules cover Claude’s file tools and shell commands it recognizes, but not every arbitrary subprocess that might open a file. If the boundary needs to apply to child processes as well, enable the sandbox and use its filesystem restrictions.

2- Keep secrets out of the environment that launches the agent.

Environment variables move from parent processes to their children. If I export a production token and then start Claude Code from that terminal, Claude inherits it. A better pattern is to start the agent from a clean shell and inject a secret only into the specific application or command that needs it.

That reduces ambient exposure, but it is not complete isolation. If the agent can change code that later runs inside the secret-bearing process, it may still cause that process to print or transmit the value. For stronger separation, the agent needs a sandbox or development environment that never receives production credentials in the first place.

3- Change is the one with the greatest impact: give the agent a separate, least-privilege identity.

For a database, that may be a non-production role with access only to the schemas or rows needed for the task. For an API, it may be a narrowly scoped token with a short expiry. Read-only is useful, but it is not harmless; read access to customer or production data can still be a serious breach. Where production access is unavoidable, restrict schemas, rows, and columns, exclude PII where possible, and audit the queries made through that identity.

File and process controls reduce the probability of exposure. Least-privilege identities limit the consequence. You need both.

For the local store in this example, I use macOS Keychain. It is already on the machine, managed by the operating system, and stores the secret encrypted rather than leaving it in a plaintext project file. The same pattern can use 1Password, Doppler, HashiCorp Vault, or a cloud secret manager.


Want the agent to build the setup? Give it this prompt

The non-negotiable rule is that the agent generates the scripts and configuration snippets. It never reads the existing secret values.

There is a deliberate limitation in that prompt: it asks the agent to create an audit script rather than inspect the files itself. An agent cannot search a secret-bearing configuration for literal values without reading them.


The local setup

1. Block the obvious paths

At minimum, add Read deny rules for the secret-bearing files in the projects where you use the agent:

Then configure the Claude Code sandbox to deny those paths at the filesystem level. The distinction matters: Read rules reduce exposure through Claude’s normal tools, while sandbox rules also constrain arbitrary child processes.

Claude Code v2.1.187 and later can deny explicitly listed credential files and environment variables through sandbox.credentials. Environment-variable masking requires v2.1.199 or later and additional network configuration. I would use those controls where they fit. I still keep the Keychain pattern because it is not tied to one coding agent, and the two layers can work together.

Broad .env* rules will also hide .env.example. That may be exactly what you want, but if the agent needs a list of required variable names, keep a placeholder-only template under a readable name such as env.example. Never put real values in it.

2. Put the values in Keychain

Add a secret from your own terminal:

With no value placed after -w, macOS prompts for it. The secret does not become part of the command in your shell history.

To retrieve it, a trusted script can use:

That command prints the secret, which is why the agent should not run it directly.

On the first read, macOS may show a Keychain permission dialog. I use Allow for that request rather than Always Allow. The important point is not to approve a Keychain request that the agent initiated without first understanding the command it is trying to run.

3. Inject a secret into one command

This is the wrapper I use for local development:

The values never enter the shell that launched the agent. They exist in the launched application and anything that application starts, then disappear when that process tree exits.

That last sentence is also the boundary of the pattern. Every process in that tree can access the values. If the agent controls the code running there, use development-only credentials and assume the application could expose them.

4. Migrate a straightforward .env file

Run this only after closing the agent session, from your own terminal.

There is one tradeoff worth making explicit: automating security add-generic-password with -w “$value” places the value briefly in that command’s process arguments. If another same-user process inspecting arguments is part of your threat model, do not use this convenience script. Add each item interactively with security … -w, use Keychain Access, or use a small tool that calls the Keychain API directly.

This parser is intentionally limited. Dotenv files can contain multiline values, substitutions, whitespace rules, and quoting edge cases. If yours does, add the entries manually in Keychain Access or use a parser you already trust. Do not make a migration script more “helpful” by evaluating the file as shell code.

After migration, keep env.example with names and placeholders for documentation. Remove only the files that contain actual values.

A policy the agent can follow

I also keep the following at user level in ~/.claude/CLAUDE.md, so it applies across projects:

Instructions in CLAUDE.md help the agent make better decisions. They are not a security boundary, so they sit behind permissions, sandboxing, and scoped identities rather than replacing them.

What changes in the application?

Usually, very little.

Applications consume environment variables; .env and dotenv are only ways of populating them. process.env.STRIPE_KEY receives the same value whether it came from a file, Keychain, or a secret manager.

The main change is how the application starts:

Some frameworks load .env files automatically, and some projects explicitly fail when a file is missing. Those cases need a small configuration change. The application should also validate its required environment variables at startup rather than failing later with an unrelated error.

For MCP configuration, ${VAR} references are still better than literal tokens in .mcp.json, but they solve a narrower problem: they keep the value out of the file. The variable still has to be supplied to the MCP server.

Where possible, I prefer OAuth. For an API-key-only MCP server, I use a trusted per-server launcher that retrieves one scoped Keychain item and executes one fixed server command. The launcher lives outside the writable project and is not available to the agent as a general shell tool. The MCP server and its package supply chain then become part of the trusted boundary.

What this pattern does and does not solve

This setup reduces several common forms of accidental exposure:

  • Secrets no longer sit in plaintext project files.
  • The agent does not automatically inherit everything from the developer’s shell.
  • Normal file reads are denied, and sandboxing can extend that boundary to child processes.
  • Any identity available to the agent can be limited and audited separately.

It does not make an agent-controlled process trustworthy, turn read-only access into harmless access, or remove the need to rotate a credential that may already have been exposed.

That is the broader lesson for me. Agent-ready architecture does not start with MCP. It starts with what the agent can see, what it can do, and how much damage one mistake can cause.


References

Leave a Reply

Your email address will not be published. Required fields are marked *