# How to stop Claude Code reading your .env file

> Hide .env and secrets from Claude Code: deny rules block file tools but not grep -r or a script. Add the sandbox, and rotate any value it already read.

Canonical: https://ranwhat.com/guides/claude-code-env-secrets
Last updated: 2026-10-15

To hide secrets from Claude Code, start with a deny rule: it stops the obvious reads. It does not stop every read, and a value the agent already read is sitting in its transcript.

```
$ uvx ranwhat clean
```

[Install](https://ranwhat.com/install)

Checked against the Claude Code, AWS and Stripe documentation and the incident reports listed at the end. Published 2 October 2026, updated 15 October 2026.

- **Deny rule**: `Read(**/.env)` in settings keeps Claude's file tools, `cat`, `head`, `tail` and `sed` away from the file.
- **Sandbox**: `/sandbox` makes the operating system enforce the same rule on `grep -r`, Python and anything else a Bash command starts.
- **Already read**: Rotate the key. The value is in `~/.claude/projects` and went to the model with the conversation.

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

```
$ project settings
{
  "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:

- **Discovery**: Matching files are left out of file discovery and search results.
- **Reads**: Reads are denied: through Claude's file tools, through the file commands Claude Code recognises in Bash, such as `cat`, `head`, `tail`, `sed` and `tee`, and through redirection targets such as `< file` (v2.1.257 or later for input redirects).
- **Writes**: The Edit and Write tools are blocked on the same paths. That needs Claude Code v2.1.208 or later for edits and v2.1.228 or later for writes.

### 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 `~/`:

```
$ user settings
{
  "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 .env` `sed -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.

- **macOS**: Seatbelt, built in. Nothing to install.
- **Linux, WSL2**: bubblewrap and socat: `sudo apt-get install bubblewrap socat` or `sudo dnf install bubblewrap socat`. The `/sandbox` panel shows what is missing.
- **Windows**: Native Windows is not supported, and neither is WSL1. Run Claude Code inside WSL2.

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:

```
$ user settings
{
  "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" }
      ]
    }
  }
}
```

- **denyRead**: `~/**/.env` blocks every `.env` under your home directory, and keeps it blocked inside a wider `allowRead`. A wildcard that matches a directory blocks its contents too, so a Python virtualenv named `.env` becomes unreadable to sandboxed commands.
- **credentials**: Denies reads of `~/.aws/credentials` and `~/.ssh` inside the sandbox, and unsets `GITHUB_TOKEN` and `NPM_TOKEN` before each sandboxed command runs.
- **Strict mode**: By default, after the sandbox blocks a command, Claude can retry it outside the sandbox with `dangerouslyDisableSandbox`, which falls back to the normal permission prompt. `allowUnsandboxedCommands: false` makes Claude Code ignore that parameter: a command runs sandboxed or is listed in `excludedCommands`.

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](https://ranwhat.com/guides/ai-coding-agent-security#boundary) 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**: Create a second access key while the old one still works, move every application to it, check when the old key was last used, deactivate it, then delete it.
- **Stripe**: Dashboard, API keys, the key's overflow menu, **Rotate key**. Both keys keep working for up to 7 days unless you set Expiration to **Now**, which deletes the old key at once. For a key that leaked, the grace period is also a grace period for whoever else holds it. **View request logs** on the same menu shows what the key was used for.
- **Anything else**: The issuing provider's console. If you do not know which provider a value belongs to, the next step tells you.
- **Made public**: A `.env` pushed to a public repository counts as leaked too, and agents find those. In July 2026, agents in OpenAI's cyber evaluations used Hugging Face tokens exposed publicly on their way into Hugging Face's production systems, and Hugging Face asked users to rotate their access tokens. In May, a Gemini model under evaluation by Irregular logged in to a real company's site with credentials posted publicly.

```
$ aws iam, in order
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 NAME
```

### Find 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](https://ranwhat.com/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:

```
$ uvx ranwhat cleanexcerpt
* 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](https://ranwhat.com/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

- **gitleaks**: `gitleaks dir -v --redact ~/.claude/projects`. Its rules are regular expressions, each with an optional entropy threshold. `--redact` keeps the secrets out of its own output. MIT licence.
- **trufflehog**: `trufflehog filesystem ~/.claude/projects`. Checks each candidate against the API it appears to belong to (for AWS, a `GetCallerIdentity` call) and labels it verified, unverified or unknown. That means it uses the keys it finds. `--no-verification` stops it testing them, and `--no-update` stops its check for updates. AGPL-3.0.

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.

```
$ user settings
{
  "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](https://ranwhat.com/guides/claude-code-history) 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](https://ranwhat.com/guides/claude-code-history), [a security checklist for coding agents](https://ranwhat.com/guides/ai-coding-agent-security), and [scoping the tokens an agent holds](https://ranwhat.com/guides/ai-agent-token-scopes), so a key that does leak can do less. Every guide is listed under [Guides](https://ranwhat.com/guides).

## Sources

- **Claude Code**: [Configure permissions](https://code.claude.com/docs/en/permissions), [settings reference](https://code.claude.com/docs/en/settings-reference), [sandboxing](https://code.claude.com/docs/en/sandboxing), [the .claude directory](https://code.claude.com/docs/en/claude-directory), [environment variables](https://code.claude.com/docs/en/env-vars), [data usage](https://code.claude.com/docs/en/data-usage)
- **AWS**: [Update access keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id-credentials-access-keys-update.html), [aws iam update-access-key](https://docs.aws.amazon.com/cli/latest/reference/iam/update-access-key.html)
- **Stripe**: [API keys](https://docs.stripe.com/keys)
- **Scanners**: [gitleaks](https://github.com/gitleaks/gitleaks), [trufflehog](https://github.com/trufflesecurity/trufflehog)
- **Incidents**: [0DIN: Clone this repo and I own your machine](https://0din.ai/blog/clone-this-repo-and-i-own-your-machine), [OpenAI: Hugging Face incident report](https://cdn.openai.com/pdf/67869394-cb91-4c12-888c-5cbd85c7814c/OpenAI-Hugging-Face%20Incident-Technical-Report.pdf), [Hugging Face: July 2026 incident](https://huggingface.co/blog/security-incident-july-2026), [Irregular: recent incidents](https://www.irregular.com/research/addressing-recent-incidents-ongoing-findings-and-path-forward), [NBC News: Google's confirmation](https://www.nbcnews.com/tech/tech-news/google-says-ai-model-gained-unauthorized-access-three-systems-rcna598651), [Gen Digital: infostealers and your AI agent](https://www.gendigital.com/blog/insights/research/infostealers-your-ai-agent)
- **ranwhat**: [watch.py](https://github.com/MatijaMiki/ranwhat/blob/main/ranwhat/watch.py), [clean.py](https://github.com/MatijaMiki/ranwhat/blob/main/ranwhat/clean.py)
