Keep Claude Code from reading your .env file
On this page
  1. The short version
  2. What the deny rule blocks on its own
  3. Where it leaks
  4. The sandbox closes it
  5. Don't leave the side door open
  6. Let the app read secrets, not the agent
  7. A checklist
  8. Where Joxo fits
  9. Frequently asked questions
  10. Does Claude Code read .env files?
  11. How do I stop Claude Code from reading my secrets?
  12. Is a deny rule enough without the sandbox?
  13. Try it with your team

Blog / claude code

Keep Claude Code from reading your .env file

A Read deny rule stops Claude Code opening .env, even with cat. It misses node -e and cat .e*. What we tested, and the sandbox setting that closes the gap.

Your .env holds real keys, and a coding agent that reads it can paste them into a log, a commit or a chat. The usual advice is to add Read(./.env) to Claude Code's deny list. That rule works better than most people expect, and worse than most people assume. We tested it on Claude Code 2.1.312 with a throwaway key, and it blocked the Read tool, cat .env and grep API .env. But node -e and cat .e* printed the key straight away. Turning on Claude Code's sandbox closed both gaps.

Here is the setup we ended with, then what each part does.

The short version#

Put this in .claude/settings.json in your project and commit it, so everyone on the team gets the same protection:

json
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)"
    ]
  },
  "sandbox": {
    "enabled": true,
    "allowUnsandboxedCommands": false
  }
}

The deny rules say which files are off limits. The sandbox makes the operating system enforce that for every command Claude Code runs, not only for the commands Claude Code can recognise. The last setting removes the way out: without it, Claude Code can ask you to run a blocked command outside the sandbox, and a tired "yes" undoes the protection.

What the deny rule blocks on its own#

We made a folder with a .env containing API_KEY=sk-test-PURPLE-OTTER, denied Read(./.env), and on purpose allowed Bash(cat *) and Bash(grep *) so nothing else would stop the commands. Then we asked Claude Code to print the key three ways:

What we asked forResult
Read .env with the Read toolRefused
cat .envRefused, even though cat was allowed
grep API .envRefused, even though grep was allowed

So a Read deny rule is not limited to the Read tool. When a shell command names the file directly, Claude Code matches it against the rule and refuses. That covers the common cases: an agent that wants to "check the config" usually reaches for cat or grep.

Where it leaks#

The rule works by looking at the command. A command that reads the file without naming it in a way Claude Code can match gets through. Two examples from the same folder, with Bash(node *) allowed:

bash
node -e "console.log(require('fs').readFileSync('.env','utf8'))"
cat .e*

Both printed API_KEY=sk-test-PURPLE-OTTER. The same goes for a script that loads the file, a Python one-liner, or a test that logs its environment. An agent doesn't need to be malicious to do this: it may be debugging why a variable is missing, and printing the whole file is the obvious next step.

If you allow broad commands such as Bash(node *), Bash(python *) or Bash(npm run *), a deny rule alone is a polite request, not a lock.

The sandbox closes it#

Claude Code's sandbox runs each shell command with operating-system limits on what it can read and write (Seatbelt on macOS, bubblewrap on Linux). The Read deny rules from your settings become part of those limits. We added "sandbox": { "enabled": true } to the same folder and asked again:

CommandWithout sandboxWith sandbox
node -e "…readFileSync('.env')…"Printed the keyFailed, Node got a permission error
cat .e*Printed the keycat: .env: Operation not permitted

Nothing about the command matters any more: the file can't be opened by anything the agent starts. You can also turn the sandbox on for one session with the /sandbox command, but the setting in the shared file is what keeps it on for everyone.

Don't leave the side door open#

When a command fails inside the sandbox, Claude Code can offer to run it again without the sandbox, and asks you first. That's useful for a build tool that really needs more access, and it's exactly how a secret gets printed: the prompt looks routine, and you approve it.

"allowUnsandboxedCommands": false removes that option, so every command stays inside the sandbox. If a tool genuinely needs to run outside it, list it under sandbox.excludedCommands on purpose rather than approving it in the moment.

Let the app read secrets, not the agent#

Your tests and dev server still need the keys. Two habits keep that working without showing the agent the values:

  • Load the file in the program, not in the chat. node --env-file=.env server.js, dotenv, or your framework's own loader read the file inside the process. The agent sees the program's output, not the file.
  • Ship a .env.example. Same variable names, fake values, committed to git. The agent learns which variables exist from it and never needs the real file. Add a line to CLAUDE.md: "Variable names are in .env.example. Never print or read .env."

Watch for output that echoes secrets anyway: a test that prints process.env, or an error that includes a connection string. The sandbox stops the agent reading the file, not a program you wrote from printing what it loaded.

A checklist#

  • Read(./.env) and Read(./.env.*) under permissions.deny, plus any other secret folders.
  • sandbox.enabled: true and sandbox.allowUnsandboxedCommands: false.
  • The settings in .claude/settings.json, committed, so the whole team has them.
  • A .env.example and one line in CLAUDE.md saying where variable names live.
  • No test or log line that prints the whole environment.
  • Test it yourself: put a fake key in .env, ask Claude Code to print it with node -e, and check it fails.

Where Joxo fits#

Joxo connects your agent with your teammates' agents on one project: shared tasks, hand-offs, and questions that reach you on your phone. It doesn't need your .env and doesn't ask for it. Setup is one line pasted into your agent. For the rest of the permission list, see stop Claude Code asking permission for every command, safely.

Frequently asked questions#

Does Claude Code read .env files?#

It can, like any file in the project, unless you deny it. A Read(./.env) deny rule blocks the Read tool and shell commands that name the file directly, such as cat .env. Commands that read it indirectly, like node -e with readFileSync, still work unless the sandbox is on.

How do I stop Claude Code from reading my secrets?#

Add deny rules for the secret files in .claude/settings.json, turn on the sandbox with sandbox.enabled, and set allowUnsandboxedCommands to false. In our test that combination blocked every way we tried to print the key.

Is a deny rule enough without the sandbox?#

Only if you never allow broad commands. Once node, python or a package script can run without asking, the agent can read the file through them. The sandbox enforces the rule at the operating-system level, so the command doesn't matter.