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) ordoppler run --start a command with secrets in its environment without writing them to the working tree. - Keep a
.env.examplewith 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:
denyrules such as"Read(./.env)"and"Read(./.env.*)"in.claude/settings.json. -
Cursor: add
.envand.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.productionthrough the shell -
grep -r API_KEY .across the repository - An MCP filesystem server with its own read tool
-
git show HEAD:.envif 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
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
✓ 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
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."
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 scanreportsenv-readablefindings for.envfiles with secret-shaped, non-placeholder values, pluscredential-readablefor common paths like~/.aws/credentialsand~/.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
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,headand 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)