DEV Community

Umang Kumar
Umang Kumar

Posted on

Protecting .env Secrets From AI Agents: Six Layers, and How to Test Each One

Almost every project has a .env file, and almost every coding agent can read files. Put those together and you have the shortest path from a prompt injection — or a careless "let me check your config" — to a live credential in a model's context window. Protecting .env secrets from AI agents is less about one clever setting and more about closing every route to the file, then proving you closed them.

Here are six layers, roughly in order of how much they buy you.

Layer 1: don't have the secret on disk

The most effective control is boring: production credentials should not live in a .env file on a laptop where agents run.

  • Use a secret manager and inject values only into the process that needs them. Tools such as op run (1Password CLI) or doppler run -- start a command with secrets in its environment without writing them to the working tree.
  • Keep a .env.example with placeholder values for onboarding.
  • Use separate, low-privilege credentials for local development.

If the file isn't there, no agent setting matters. Every layer below exists because, realistically, some secrets will be on disk anyway.

Layer 2: deny the file in each agent's own controls

Each agent has its own mechanism. Use all that apply:

  • Claude Code: deny rules such as "Read(./.env)" and "Read(./.env.*)" in .claude/settings.json.
  • Cursor: add .env and .env.* to .cursorignore, since file reads otherwise need no approval.
  • Codex CLI: keep the default sandbox; its workspace-write mode limits writes and keeps network off by default, but files inside the workspace are still readable, so Layer 1 matters even more.

These controls are worth having, but most of them are scoped to one tool — the agent's file reader. Which leads to the important part.

Layer 3: close the other routes (and test them)

A secret file can be read by many tools, not just "Read":

  • cat .env, less .env.local, head .env.production through the shell
  • grep -r API_KEY . across the repository
  • An MCP filesystem server with its own read tool
  • git show HEAD:.env if it was ever committed
  • A script the agent writes and then runs

Every one of those is a separate path to the same bytes. The only way to know a path is closed is to try it.

Here is a concrete example of why testing matters. The Cirvix repository ships a default.policy baseline with a shell rule that allows commands its classifier rates MEDIUM or below. When I ran shell variants through cirvix policy test (package 0.3.0):

  ✓ cat aws creds  → deny (deny-critical-unnamed)
  ✗ cat env
      expected  deny
      actual    allow  by allow-safe-shell
      call      shell.exec   risk MEDIUM
  ✗ grep env
      expected  deny
      actual    require_approval  by approve-high-risk-shell
  ✗ less env
      expected  deny
      actual    require_approval  by approve-high-risk-shell
Enter fullscreen mode Exit fullscreen mode

cat ~/.aws/credentials was classified as credential access and denied. cat .env was not — it matched the safe-shell allow. grep and less on env files were held for approval rather than denied. A reasonable-looking baseline, and one of the routes was open.

The fix is an explicit rule for the shell route alongside the file-read rule:

deny:
  name = deny-dotenv-read
  tool = filesystem.read
  path = **/.env*
  reason = "Agents never read .env files directly."

deny:
  name = deny-dotenv-shell
  tool = shell.exec
  command = ".env"
  reason = "Shell access to .env files is a read by another name."

test "read tool":
  tool = filesystem.read
  path = ./.env.production
  expect deny
test "cat env":
  tool = shell.exec
  command = cat .env
  expect deny
Enter fullscreen mode Exit fullscreen mode
  ✓ read tool  → deny (deny-dotenv-read)
  ✓ cat env  → deny (deny-dotenv-shell)
  ✓ source code  → allow (allow-workspace-read)
  ✓ npm test  → allow (allow-safe-shell)
  4/4 PASSED
Enter fullscreen mode Exit fullscreen mode

Trade-off: command = ".env" is a substring match, so it also blocks cat .env.example. That is usually acceptable; if not, give the example file a different name such as env.sample.

The general lesson applies to any tool you use: write the deny rule, then write tests for cat, grep, less, head, and your MCP servers' read tools, and keep them in CI.

Layer 4: give the agent handles, not values

Often the agent doesn't need to see a secret; it needs a request to be authenticated. A secret-brokering pattern gives the agent an opaque handle and substitutes the real value at the moment an approved call goes out to an approved destination. The model can use the credential without ever holding it.

This needs explicit wiring in whatever system you use. It is worth it for the handful of tokens agents use constantly (GitHub, your package registry, an internal API).

Layer 5: close egress after secret access

Assume Layers 2 to 4 will eventually miss something — a token pasted into an issue, a credential in a log file. The backstop is a rule about what happens next:

deny:
  name = deny-secret-egress
  tool = network.request
  touched_secret = true
  external = true
  reason = "This session has touched secret material; external egress is closed."
Enter fullscreen mode Exit fullscreen mode

Once a session has read something secret-shaped, outbound calls to external hosts are refused for the rest of the session. A read followed by an exfiltration attempt fails at the second step, even when each call alone would be allowed. (I wrote about this pattern in more depth in an earlier post on session taint.)

Layer 6: detect and rotate

  • Run a secret scanner such as gitleaks or trufflehog in CI and pre-commit, so a secret an agent copies into code or a commit is caught.
  • Inventory where readable credentials exist on the machine. cirvix scan reports env-readable findings for .env files with secret-shaped, non-placeholder values, plus credential-readable for common paths like ~/.aws/credentials and ~/.ssh/id_ed25519. It reads home-directory locations, so run it only where that is authorised.
  • If a secret has ever been in an agent's context, rotate it. You cannot audit what a model provider logged.

Where Cirvix fits

Cirvix AgentControl is an open-source policy layer that implements Layers 3 and 5 for governed tool calls — calls routed through its MCP gateway, its Claude Code hook, or functions wrapped with its SDKs. Every governed call gets permit, hold or deny before it executes, with deny as the default.

npx --yes @cirvix_ai/agent-control check --action fs.read --resource .env.production
# DENY  fs.read …/.env.production   rule deny-dotenv-read
cirvix policy test --policy secrets.policy
Enter fullscreen mode Exit fullscreen mode

It does not see calls that bypass those paths, does not stop prompt injection, and redaction is not a universal information-flow guarantee. Layer 1 remains the strongest control you have.

Checklist

  • [ ] Production secrets injected at runtime, not stored in .env
  • [ ] Deny rules in every agent you use
  • [ ] Tests for cat, grep, less, head and MCP read tools against env files
  • [ ] Handles for frequently used tokens
  • [ ] Egress closed after secret access
  • [ ] Secret scanning in CI; rotate anything an agent has seen

Cirvix AgentControl: https://github.com/CIRVIX/agent-control
Docs: https://cirvix.com

Top comments (0)