Rules / CFG041
CFG041 permissions.deny does not restrict access to .env files
Severity: error
OWASP: LLM02:2026 – Sensitive Information Disclosure
What cfgaudit checks
.env files routinely hold database passwords, API keys, and other credentials. permissions.deny is Claude Code's documented, harness-enforced mechanism for blocking file access. cfgaudit flags a permissions.deny block that exists but contains no pattern covering .env files.
A pattern counts as sufficient coverage if it targets .env:
.env
.env.*
*.env
**/.env
**/.env.*
A completely absent permissions.deny block is reported by CFG006, not here.
A deny-all entry also counts as coverage and suppresses this finding: Read(*) / Read(**) / Read(**/*) (blocks every read, any version), or the bare "*" deny-all-tools glob — but "*" only on Claude Code ≥ 2.1.166, where that glob was introduced. On older releases "*" is an unknown tool name and a no-op, so the finding still fires (add an explicit .env pattern or upgrade). When the Claude Code version is unknown, "*" is treated as covering.
An exception rule — Read(!<glob>) or Read(./!<glob>) (also under Edit and, for cfgaudit, any tool) — denies nothing; it removes the paths it matches from the deny rules listed before it. cfgaudit therefore does not count an exception as coverage, and it un-covers the class when the exception re-exposes the secret: a deny-all with Read(!**/.env) after it leaves .env readable, so the finding fires. A narrower carve that names a safe sibling, such as Read(!**/.env.example) beside a Read(**/.env*) deny, does not un-cover .env and stays quiet.
Trigger example
{
"permissions": {
"deny": ["Bash(rm *)"]
}
}
→ error (deny exists but no .env coverage).
Safe alternative
{
"permissions": {
"deny": ["Read(**/.env)", "Read(**/.env.*)"]
}
}
References
- OWASP LLM02:2026 – Sensitive Information Disclosure
- Claude Code docs: Permissions
- CFG006 (deny block absent/empty) · CFG007 (hardcoded secrets in
env)
Documentation for cfgaudit 1.18.1. Source of this page · Rule implementation