Part of the AI coding agent security checklist
How to stop Claude Code reading your .env file
A deny rule stops the obvious reads. It does not stop every read, and a value the agent already read is sitting in its transcript.
Checked against the Claude Code, AWS and Stripe documentation and the incident reports listed at the end. Published .
Add a Read deny rule
Claude Code's built-in control for this is a deny rule in its settings. Three entries cover where most projects keep their secrets.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
]
}
}Commit it as .claude/settings.json and it applies to everyone who opens the project. Put the same block in ~/.claude/settings.json and it applies to you, in every project. For files that hold secrets, Claude Code's settings reference lists what a matching deny rule does:
Which files a pattern matches
Read rules use gitignore syntax, and the prefix decides where a pattern is anchored. It matters in a monorepo, where the .env that holds the live keys is often not at the top.
| Rule | Matches |
|---|---|
Read(./.env) | the .env in the current directory |
Read(.env)Read(**/.env) | any .env at or under the current directory, but not one in a parent directory or another project |
Read(//**/.env) | any .env anywhere on the filesystem |
Read(~/.aws/credentials) | a path under your home directory |
Read(/secrets/**) | in project settings, secrets/ in the project; in ~/.claude/settings.json, ~/.claude/secrets/ |
The last row is the trap. In user settings a single leading slash anchors at ~/.claude, not at the filesystem root and not at your project. A user-level rule meant to hold in every project needs // or ~/:
{
"permissions": {
"deny": [
"Read(//**/.env)",
"Read(//**/.env.*)",
"Read(~/.aws/credentials)",
"Read(~/.ssh/**)"
]
}
}.env.* also matches .env.example, the template an agent reads to learn which variables exist. In a project's deny list, a rule that starts with ! carves files back out of the relative rules listed before it, so Read(!.env.example) after Read(./.env.*) keeps the template readable. It cannot carve anything out of a // or ~/ rule.
What a deny rule does not stop
A Read rule is checked against the paths Claude names. A process that opens the file on its own never names it to Claude Code.
Claude Code's permissions documentation states the limit directly. Read and Edit deny rules cover the built-in file tools, the Bash file commands Claude Code recognises, and redirection targets. They do not cover a command that reads files without naming them, or a subprocess that opens files itself. Here is what a Read(**/.env) deny rule stops, and what it does not.
| Claude runs | Stopped by the deny rule |
|---|---|
cat .envsed -n 1,5p .env | Yes. Recognised file commands. |
grep -r API_KEY . | No, when run from the directory that holds the file. It reads .env without naming it, and prints every matching line, value included. |
python3 -c "print(open('.env').read())" | No. The subprocess opens the file itself. The same goes for a Node script, or any script of yours that loads .env and prints what it loaded. |
Bash deny rules have the same shape of gap. A Bash rule matches the command as Claude writes it, so Bash(curl *) stops curl https://example.com but not /usr/bin/curl https://example.com or sh -c 'curl https://example.com'. Adding a Bash rule per program does not close it, since any interpreter on the machine can print a file.
Claude Code's documentation gives one answer to this, in the permissions guide and the settings reference alike: for enforcement that covers every process, turn on the sandbox.
Close the gap with the sandbox
The sandbox is enforced by the operating system, so it applies to grep, Python and anything else a Bash command starts.
Run /sandbox in a Claude Code session and pick a mode. That writes sandbox.enabled to .claude/settings.local.json for the current project. Set it in ~/.claude/settings.json to sandbox every project.
This is the part that answers the question. With the sandbox on, Claude Code adds the paths from your Read(...) deny rules to the sandbox's own read deny list. The rule from step 01 then holds for grep -r and for a Python script as well, as long as they run as sandboxed Bash. The Config tab of /sandbox shows the resolved settings, so you can check the paths arrived.
By default, sandboxed commands can read the entire computer apart from a few denied directories. That still includes ~/.aws/credentials and ~/.ssh/, and there is no built-in credential deny list: only what you list is restricted. A user-level configuration that closes the usual holes:
{
"sandbox": {
"enabled": true,
"allowUnsandboxedCommands": false,
"filesystem": {
"denyRead": ["~/**/.env"]
},
"credentials": {
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
],
"envVars": [
{ "name": "GITHUB_TOKEN", "mode": "deny" },
{ "name": "NPM_TOKEN", "mode": "deny" }
]
}
}
}Two limits. The sandbox covers Bash commands and their child processes; Claude's Read, Edit and Write tools go through the permission system instead, so keep the deny rule. The two are layers, not alternatives. And sandbox.credentials applies to sandboxed Bash only. CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 strips the variables Claude Code recognises as credentials, such as Anthropic and cloud provider keys, from the Bash tool, hooks and MCP stdio servers, sandboxed or not. That works on environment variables. A .env file on disk is still a file, and still needs the rule and the sandbox.
The environment matters as much as the file. In 0DIN's demonstration of 25 June 2026, a setup command Claude ran on its own opened a reverse shell as the developer, and the authors list ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY and GITHUB_TOKEN among what that shell could reach. A shell started from a Bash command inherits that command's environment, so with the scrub it starts without the credentials Claude Code recognises. It can still open any file you have not denied to sandboxed commands, which is what denyRead and sandbox.credentials are for. The security checklist has the whole case.
If it already read the file
A deny rule added now does nothing for a value the agent has already seen. Treat that value as leaked.
There are two copies. One is on disk: when a tool reads a .env file, Claude Code's documentation says “that value is written to projects/<project>/<session>.jsonl”, under ~/.claude. Transcripts are plaintext, protected only by file permissions, and kept for 30 days by default. Large tool outputs go to <session>/tool-results/ beside it. The other copy went over the network: the model saw the value because Claude Code sent it with the conversation.
So rotate first. Masking or deleting the transcript afterwards stops a second leak from disk. It does not un-send anything.
Rotate
aws iam create-access-key --user-name NAME
# move every application to the new key, then:
aws iam get-access-key-last-used --access-key-id OLD_KEY_ID
aws iam update-access-key --access-key-id OLD_KEY_ID --status Inactive --user-name NAME
aws iam delete-access-key --access-key-id OLD_KEY_ID --user-name NAMEFind what it read
Without installing anything, search the transcripts for a variable name from your .env: grep -rl 'STRIPE_SECRET_KEY' ~/.claude/projects lists every transcript that mentions it. That finds names. It does not tell you which values are there or which file they came from.
ranwhat clean reads the same transcripts and reports each distinct secret with the project it was found in and, when the transcript names it, the file it was read out of. Then it leaves a review session open:
* Stripe live secret key sk_…dc 32 chars seen 8x read from api/.env in /Users/you/Desktop/app ranwhat> rotate what to rotate, grouped by provider ranwhat> mask all mask everything listed
rotate groups the findings by provider and says where to roll each one. Run mask all after rotating, not instead of it. It writes a backup to ~/.ranwhat/backups first, and those backups hold the values until you delete them. clean reads Claude Code transcripts, subagents' included, from the last 30 days by default (--days N), and does not read tool-results/ yet.
ranwhat watch reports the reads themselves: .env files, ~/.aws/credentials, ~/.ssh/id_*, .netrc, ~/.config/gcloud, service-account JSON, security find-generic-password and ~/.kube/config, through Bash or a file-reading tool. It does not flag grep -r API_KEY . or a python3 -c that opens .env: the same blind spot as the deny rule. What those commands printed is still in the transcript, and clean still finds it, naming the .env when the command or its output names the file. uvx ranwhat check runs both and changes nothing.
Other scanners
Both report. Masking the transcript is a separate step.
Keep less of it on disk
The deny rule and the sandbox decide what gets read. Retention decides how long a mistake stays readable.
{
"cleanupPeriodDays": 7
}cleanupPeriodDays sets how many days Claude Code keeps transcripts. The default is 30 and the minimum is 1: 0 fails validation. Deletion is a background sweep after a session starts, and a deleted session no longer appears in /resume.
Retention also decides what malware finds. Commodity infostealers now collect AI coding tools' prompt histories, conversation databases and the credentials in their MCP configs, Gen Digital reported on 8 September 2026. A .env value read into a transcript stays there until the sweep or a purge removes it, or ranwhat clean masks it. The history guide covers what to do.
CLAUDE_CODE_SKIP_PROMPT_HISTORY=1 claude write no transcript for this session claude -p --no-session-persistence "..." the same, non-interactive claude project purge ~/work/my-repo --dry-run what one project holds
CLAUDE_CODE_SKIP_PROMPT_HISTORY=1 skips the transcript and prompt history for that session, so it cannot be resumed either. claude project purge deletes what Claude Code keeps for one project: transcripts, memory, file history, the matching lines of prompt history and its entry in ~/.claude.json. It prints the plan and asks before deleting anything.
For the rest: what Claude Code keeps in ~/.claude and for how long, a security checklist for coding agents, and scoping the tokens an agent holds, so a key that does leak can do less. Every guide is listed under Guides.