Claude Code can read any file in your project, run shell commands and stage commits. That's exactly what makes it useful, and exactly why a stray .env or private key can end up in a diff, a log, or a commit if you haven't set boundaries first.
.env while debugging a config issue and quotes a value back to you or into a file it writes.git add -A or git add . stages a credentials file you forgot was untracked..claude/settings.json instead of the shell environment.env or cat .env prints secrets straight into the transcript.None of this requires anything going wrong with the model. It's ordinary agent behavior meeting an ordinary unprotected file.
Add these to permissions.deny in .claude/settings.json (shared with your team) or ~/.claude/settings.json (just you):
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./**/*.pem)",
"Read(./**/*.key)",
"Read(./secrets/**)",
"Read(./.aws/**)"
]
}
}
Deny rules win over allow rules and can't be overridden by a broader match, so this is a solid first layer. It's worth adding .env.example as an explicit exception if you keep one, since it holds placeholder values you want Claude to see:
"allow": ["Read(./.env.example)"]
The gap: a deny rule on Read only stops Claude's own Read tool. Bash(cat ./.env) still works unless Bash is restricted too, and there's no single Bash pattern that reliably blocks every way to read a file (cat, less, python -c, a here-doc). Treat the deny rule as necessary, not sufficient — pair it with real command inspection. See the permissions guide for the full allow/ask/deny syntax.
Claude Code doesn't ship a built-in secret scanner. A PreToolUse hook on Bash can add one: when the command is a git commit, scan the staged diff for common secret shapes and block the commit if it finds one.
#!/usr/bin/env python3
# .claude/hooks/scan-secrets.py -- PreToolUse, matcher "Bash"
import json, re, subprocess, sys
data = json.load(sys.stdin)
cmd = data.get("tool_input", {}).get("command", "")
if "git commit" not in cmd:
sys.exit(0)
diff = subprocess.run(
["git", "diff", "--cached"], capture_output=True, text=True
).stdout
PATTERNS = [
(r"AKIA[0-9A-Z]{16}", "AWS access key"),
(r"ghp_[A-Za-z0-9]{36}", "GitHub token"),
(r"sk_live_[A-Za-z0-9]{16,}", "Stripe live key"),
(r"-----BEGIN [A-Z ]*PRIVATE KEY-----", "private key"),
]
hits = []
for line in diff.splitlines():
if not line.startswith("+"):
continue
for pattern, label in PATTERNS:
if re.search(pattern, line):
hits.append(label)
if hits:
print(f"Blocked commit: possible {', '.join(sorted(set(hits)))} in staged changes.",
file=sys.stderr)
sys.exit(2)
Register it in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [ { "type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scan-secrets.py" } ] }
]
}
}
Exit code 2 is what blocks the tool call; the text on stderr is what Claude sees as the reason, so keep it descriptive but never echo the matched secret itself. Test the hook directly before trusting it:
echo '{"tool_input":{"command":"git commit -m x"}}' | python3 .claude/hooks/scan-secrets.py; echo "exit=$?"
Widen the pattern list for your own providers, and don't expect regex matching to catch everything — it catches known shapes, not novel ones. See more hook examples, or build and test a rule set without writing the Python yourself using the free hook builder.
The env key in settings.json sets environment variables for every Claude Code session and the subprocesses it launches:
{
"env": {
"NODE_ENV": "development"
}
}
Use it for switches like this, not for credentials. Anything in env is visible to Claude and, if the file is committed, to everyone with repository access. Keep real secrets in your shell profile, a local .env loaded by your app's own tooling, or a secrets manager, and reference them by name in scripts rather than pasting values anywhere Claude or git can see them.
Read on .env, key files and any secrets directory, in a shared .claude/settings.json so the whole team gets it.env inside a settings file that gets committed..claude/settings.local.json out of git; that's where personal, uncommitted overrides belong. See settings.json explained.Keelwork bundles 10 workflow skills, 5 tested safety hooks (including a full guard-bash and a secret scanner), 3 subagents and 5 CLAUDE.md templates, with a one-command installer that safely merges into your settings.
Get Keelwork — $24 →