How to stop Claude Code from reading secrets and .env files

To stop Claude Code from reading .env files, add Read deny rules such as Read(./.env) to a settings file. Those rules also block cat, head and other file commands that Claude Code recognizes in Bash. They do not stop grep -r or a script that opens files itself, so add sandboxing for those cases.
Add Read deny rules for secret files
Read deny rules are the first layer. They are the only one here that covers recognized Bash file commands such as cat without extra setup. Put them under permissions.deny. The steps below follow the Claude Code settings docs, checked as of 2026-10-27. The docs do not name a CLI version for these rules, so record the version you test with.
- Open
.claude/settings.jsonat the project root, or create it. - Add the two entries under
permissions.deny. - Save the file, then run
/statusin Claude Code and confirm the file appears in theSetting sourcesline.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)"
]
}
}
The settings docs say deny and ask rules in a committed project file apply right away. Allow rules from that file wait until each developer trusts the folder, so a deny list in the repository is the safer place for this rule.
Know which reads a Read rule covers
A Read rule covers more than the Read tool. The permissions docs say Read and Edit deny rules apply to the built-in file tools, to file commands Claude Code recognizes in Bash such as cat, head, tail, sed and tee, and to the targets of Bash redirections. The same rule also applies to @file mentions in your prompt, and it blocks Edit and Write on the same path.
The gap is the commands that never name the file. The docs say these are not covered: grep -r pattern . run from the folder that holds the file, and any subprocess that reads a file indirectly, such as a Python or Node script. A Bash rule does not close that gap either. It matches the command text Claude writes, so Bash(rm *) stops rm -rf build/ but not /bin/rm -rf build/ or bash -c 'rm -rf build/', as the permissions docs show.
Read-only commands add a second point. The docs say cat runs without a permission prompt in every mode inside your working directories, so without a deny rule a cat .env in Bash runs silently.
| Way Claude reads the file | Read deny rule | Bash deny rule | Sandbox, when on |
|---|---|---|---|
Read tool on .env |
Blocked | Not applicable | Not covered, since the Read tool runs outside the sandbox |
cat .env in Bash |
Blocked, as a recognized file command | Only that exact form, such as Bash(cat *) |
Blocked when the file is in a denied path |
grep -r from the folder with .env |
Not covered | Only if you deny grep itself |
Blocked when the file is in a denied path |
Python or Node script opens .env |
Not covered | Not covered | Blocked when the file is in a denied path |
Add a PreToolUse hook for the Read tool
A PreToolUse hook runs before each tool call, and it can block the call with exit code 2. This hook matches the Read tool only, so it covers the cases in the table where the Read tool is the reader. Steps:
- Save the script below as
.claude/hooks/block-secret-reads.sh. - Make it executable with
chmod +x. - Register it under
hooksin.claude/settings.json, with the matcherRead.
The hook receives the call as JSON on stdin. The permissions docs name file_path as the primary field for Read, so the script reads .tool_input.file_path. The script matches by file name, so environment.ts and env-notes.md pass through.
#!/bin/bash
# .claude/hooks/block-secret-reads.sh
input=$(cat)
file_path=$(jq -r '.tool_input.file_path // empty' <<<"$input")
name=$(basename "$file_path")
case "$name" in
.env|.env.*)
echo "Blocked: $file_path is a secrets file" >&2
exit 2 # Blocking error: the Read call is prevented
;;
esac
exit 0 # No decision: the normal permission flow applies
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-secret-reads.sh"
}
]
}
]
}
}
The permissions docs say a blocking hook takes precedence over allow rules, so an allow entry cannot let the read through. The matcher is an exact tool name, so Grep and Glob need their own hook. Their path field is path, not file_path.
Use sandboxing for subprocesses
Sandboxing is the layer for processes that read files without naming them. It is off by default. Turn it on with /sandbox in a session, or set sandbox.enabled to true in a settings file. The docs say the sandbox runs on macOS, Linux and WSL2, and that native Windows runs commands without it.
The sandbox covers shell commands and the processes they start. It does not cover the Read tool, MCP servers or hooks. The docs also say there is no built-in credential deny list, so only the files and variables you list are restricted. This example from the sandboxing docs blocks two credential paths and removes two tokens from the environment of sandboxed commands:
{
"sandbox": {
"enabled": true,
"credentials": {
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
],
"envVars": [
{ "name": "GITHUB_TOKEN", "mode": "deny" },
{ "name": "NPM_TOKEN", "mode": "deny" }
]
}
}
}
The example protects home-directory credentials, not a .env in the project. For a project file, keep the Read deny rule as the main control and use sandbox.filesystem.denyRead if you need the sandboxed commands blocked from it too.
Keep secrets out of the agent’s reach
Rules only help for the files they name. The permissions docs say read-only file reads and Grep run without approval inside the working directory, so any secret left in the project is readable by default. These steps are practice, not something the docs state:
- Keep production credentials out of the project folder. Load them from a secrets store at run time, and do not commit them.
- Scope each token to the work it needs, and rotate it on a schedule.
- Put the rules that must hold on every machine in managed settings. The permissions docs say no lower level, including command-line arguments, can override a managed permission rule. The managed settings post covers that setup.
Check that the rules loaded
Run /status inside Claude Code to see which settings files loaded. The settings docs say claude doctor lists entries Claude Code rejected, so run it in a shell if a rule seems to have no effect. To test the setup, create a scratch project with a dummy .env, then ask Claude Code to read it. The Read call should be refused before it runs, and so should cat .env through Bash.
Next steps
Add the Read deny rules and commit them, then add the hook for the Read tool. Turn on sandboxing if your team runs shell commands that read files indirectly. For the wider set of controls that settings files cannot provide, see governance for AI coding agents. For the Bash side of the same problem, read how to block destructive commands in Claude Code, and for the MCP side, how to restrict which MCP servers Claude Code can use.
Enforcing this with AIControls
Settings files and hooks run on each developer machine. AIControls adds a gateway layer. It supports Claude Code through its gateway for both the CLI and VS Code, and it provides per-user and per-session cost and budget limits. Those controls sit beside the file rules above, not in place of them. Download AIControls for the details.
Frequently asked questions
Does a Read deny rule stop Claude Code from running cat .env?
Yes, when Claude Code recognizes the command as a file command. The permissions docs say Read and Edit deny rules apply to file commands such as cat, head, tail, sed and tee. They do not apply to grep -r run from the folder that holds the file, or to a Python or Node script that opens the file itself. Sandboxing covers those.
Is a .claudeignore file enough to hide .env files from Claude Code?
No. The permissions docs say a .claudeignore file has no effect, so it does not stop Claude Code from reading anything. Move its entries into Read deny rules, such as Read(./.env), in .claude/settings.json or another settings file. Those rules cover the Read tool, recognized Bash file commands, and (on a best-effort basis) Grep and Glob.
Does sandboxing protect .env files from Claude Code?
Only when sandboxing is on, and only for shell commands and the processes they start. The sandbox is off by default and runs on macOS, Linux and WSL2. The Read tool runs outside the sandbox, so block the file's path with a Read deny rule as well.
Can a PreToolUse hook stop Claude Code reading a secret file?
Yes. Register a PreToolUse hook with the matcher Read, and exit with code 2 when tool_input.file_path names a secret file. The hook runs before the permission prompt, and a blocking exit code overrides any allow rule, so the read does not happen.
Do Read deny rules still apply when a file is reached through a symlink?
Yes. The permissions docs say a deny rule applies when either the requested path or the file it resolves to matches. A symlink in the project that points to a denied file is blocked, because the target matches the deny rule even though the link itself sits inside an allowed folder.