The annoyance
I put Read(./.env) and Read(./secrets/**) under permissions.deny in .claude/settings.json. Claude could not open .env with the Read tool. I asked for a short Python script that opened the file itself. The values came back.
A .claudeignore line for the same paths changed nothing.
What Anthropic actually says
From the official Configure permissions page, under Read and Edit:
“If your project has a .claudeignore file, it has no effect, so move its entries into Read deny rules.”
“Read and Edit deny rules apply to Claude's 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 such as > file and < file. They don't apply to a command that reads files without naming them, such as grep -r pattern . run from the directory that holds the file, or to arbitrary subprocesses that read or write files indirectly, like a Python or Node script that opens files itself. For OS-level enforcement that blocks all processes from accessing a path, enable the sandbox.”
The case that paragraph leaves out is grep -r pattern ., run from the directory that holds the file, and a Python or Node script that opens the file itself.
The same sentence points at the sandbox. It is off by default. While it is on, the limits apply to Bash, PowerShell, and Monitor commands and the processes they start. “The sandbox covers shell commands only. Claude's file tools, MCP servers, and hooks run outside it.”
“A denyRead entry doesn't stop the Read tool, and allowedDomains doesn't limit WebFetch”
The Read deny stops the Read tool. sandbox.filesystem.denyRead blocks a sandboxed command from reading a path. While the sandbox is on, a command can still read most of the machine, including credential files, until denyRead names the path.
Read rules and sandbox paths use different prefixes. Exclude sensitive files denies Read(./.env), Read(./.env.*), Read(./secrets/**), a credentials file, and Bash(curl *). This note keeps the three read rules. In the project's .claude/settings.json, ./ is the project root and a single leading / is absolute. Claude Code removes a trailing /**, so ./secrets is the directory. A * on denyRead works on every platform, so ./.env.* is the sandbox spelling of Read(./.env.*).
The settings reference also adds the Read deny paths when it builds the sandbox. “Claude Code merges entries across every settings scope the session loads, and adds the paths from your Read(...) deny permission rules.” I still write filesystem.denyRead in sandbox form. When filesystem.disabled is true, those entries are not enforced.
CLAUDE.md Never Skips Permission Prompts is a sentence that never decides the call. Bypass Permissions in Project Settings Does Nothing is a mode the project file cannot set. Local Allow Does Not Beat Ask is an allow that saved and still lost. This note is a deny that saved, and still does not cover a script that never names the file.
The fix
Put this in the project's .claude/settings.json. permissions.deny is the Read layer. sandbox.enabled turns the sandbox on. sandbox.filesystem.denyRead is the read block, in sandbox path form, for the same files.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
]
},
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": [
"./.env",
"./.env.*",
"./secrets"
]
}
}
}A command in sandbox.excludedCommands runs outside the sandbox, with no filesystem restriction. “If Claude asks to retry a failed command outside the sandbox, decline the retry.”
Prove it
Open /permissions first. The three Read rows should name .claude/settings.json. The Read tool on .env is the tool-layer check. It does not prove the sandbox, because a denyRead entry does not stop the Read tool.
# /permissions
# deny Read(./.env) .claude/settings.json
# deny Read(./.env.*) .claude/settings.json
# deny Read(./secrets/**) .claude/settings.json
# Ask Claude to read .env with the Read tool.
# The deny blocks it. denyRead alone would not.
# Ask Claude to run this from the directory that holds the file.
# Do not type it at the ! prompt.
grep -r pattern .
# A Python or Node script that opens the file is the other case.
# If Claude offers an unsandboxed retry, decline it.
# The contents must not come back.
# /sandbox, Config tab: sandbox on.
# denyRead lists ./.env, ./.env.*, and ./secrets.Do not type that command at the ! prompt. In most sessions the ! prompt runs outside the sandbox, so it does not test this. Open /sandbox and read the Config tab. The sandbox is on, and denyRead lists ./.env, ./.env.*, and ./secrets.
Checklist
permissions.denyholdsRead(./.env),Read(./.env.*), andRead(./secrets/**). A.claudeignorefile does not stand in for them.sandbox.enabledistrue, in the project's.claude/settings.json, where./is the project root.sandbox.filesystem.denyReadlists./.env,./.env.*, and./secrets.filesystem.disabledis nottrue.- Ask Claude to read
.env. The Read tool is denied.cat .envis a command Claude Code recognises, so the Read deny covers it. - Ask Claude to run
grep -r pattern .from the directory that holds the file, or a Python or Node script that opens it. Decline an unsandboxed retry. The contents do not come back. - On native Windows, commands run unsandboxed. Use WSL2 for the sandbox.
Why it fails
I had ranked the deny rule as a property of the file. The permissions page ranks it as a property of the call: the file tools, the Bash commands Claude Code recognises, and a redirect target. A script that opens the file is a process the command starts. The operating system blocks that read when the sandbox is on and denyRead covers the path.
Same family as CLAUDE.md Never Skips Permission Prompts and Local Allow Does Not Beat Ask: a line that looks like the decision, and a saved rule that still loses. This one is the Read deny that did save.
I keep the operator notes for this series in the NavAIgate classroom at https://www.skool.com/navaigate.
Sources
From idea to operation
Make the next AI decision concrete.
NavAIgate helps leadership teams identify high-value AI opportunities, prove them safely and turn the winners into working systems.
