Can Claude Code read your .env file?

Yes. Claude Code reads files in your project without asking, and .env is a file in your project. Deny rules block the obvious ways in, but not all of them, and an app that loads dotenv reads the file whenever Claude runs it.

Why it can read it

Claude Code sorts its tools by risk. Its permissions docs list file reads and Grep as read-only tools that need no approval inside your working directory. Your .env sits in that directory, so reading it is as routine as reading package.json.

Usually Claude doesn't open the file to steal anything. It opens it because it's debugging: a test fails on a missing variable, so it looks at where the variables come from. Once the values are in the conversation, they're in the transcript, and from there they can end up in a script it writes. People report exactly that on GitHub, for example Claude Code reading .env files and hardcoding secrets into inline scripts.

There's a second path that has nothing to do with reading the file. When Claude runs npm test or python app.py, your app loads .env itself, through dotenv or your framework. The keys are now in a process Claude started, and anything that process prints (a verbose HTTP log, a stack trace, a debug line) comes back to Claude as output.

What doesn't work

  • .claudeignore. Many blog posts and AI answers recommend it. Claude Code's docs say plainly that if your project has a .claudeignore file, it has no effect, and that you should move its entries into Read deny rules.
  • .gitignore. It keeps the file out of git. It has nothing to do with what a process on your Mac can read.
  • A line in CLAUDE.md. "Don't open .env" is an instruction, and Claude usually follows it. It isn't enforcement, and a long debugging session or a prompt injection in a file Claude reads can override it.

Deny rules: what they block and what they miss

The supported way to block the file is a Read deny rule. This is the example from Claude Code's settings docs, for ~/.claude/settings.json:

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)"
    ]
  }
}

According to the docs, Read deny rules apply to Claude's file tools, to file commands it recognizes in Bash such as cat, head, tail and sed, and to redirections like < file. They don't apply to:

  • commands that read files without naming them, such as grep -r pattern . run from the folder that holds .env
  • scripts that open files themselves, like a Python or Node script

The second point covers your own app. A deny rule can stop Claude from opening .env, but it can't stop npm test from loading it, and you wouldn't want it to: your tests need the keys.

For enforcement at the operating system level, the docs point to sandboxing, which restricts what Bash commands and their child processes can touch. Turning it on isn't enough by itself: by default the sandbox still allows broad reads, so you add your secret paths to sandbox.filesystem.denyRead. Keep the Read deny rules too, because Claude's built-in file tools run outside the sandbox. Then you're back at the same trade-off: if a sandboxed command can't read .env, neither can the app it runs.

The fix that holds: no file to read

Every rule above guards a plain-text file that sits next to your code. You can skip that problem by not having the file. Keep the keys in a store outside the project and hand them to the command only when it runs:

$ accio myapp npm test

The keys go into the environment of that one process. There's no .env in the project for Claude to open or grep, and nothing on disk to commit by accident. Several tools work this way:

  • 1Password CLI: op run reads references from your vault. A good choice if you already pay for 1Password.
  • Doppler and Infisical: doppler run and infisical run, cloud services built for teams.
  • fidelius (ours): a Mac app that keeps keys by project in your iCloud Keychain. accio runs a command with them, and one click writes the setup into Claude Code's, Codex's and Gemini CLI's instructions.

This removes the file, but the values still reach your app, so they can still show up in its output. Some tools cover that too. op run conceals secrets in output by default. When Claude Code runs accio, key values in the output come back as <concealed by fidelius: STRIPE_KEY>, on a best-effort basis: very short values and keys you've set to stay visible aren't masked.

A short checklist

  1. Add Read(./.env) and Read(./.env.*) to your deny rules today. It's a two-line change, and it blocks the most common way the file gets read.
  2. Delete any .claudeignore and move its entries into deny rules, since the file does nothing.
  3. Move the keys out of the project and inject them at run time, with whichever tool fits how you work.
  4. Use test keys and restricted keys for local work, so a key that does leak can't move money or delete data.
  5. If a key has already been in a transcript you don't control, rotate it.

Related: How to keep secrets out of Claude Code, Codex and Gemini CLI at the same time and Where should MCP server API keys live?.