Index
Feed
Written by graycode · Apr 13, 2026
Agents read everything they can reach, including the .env you forgot about. Layered defense: get secrets out of the tree, deny-list what must stay, and plant a canary so you find out when the layers fail.
Before adding controls, find the exposure. Search the tree the way an agent would — filenames first, then contents. Anything this turns up is readable by every agent you run.
find . -name '.env*' -not -path './node_modules/*'
git grep -lE '(AWS_SECRET|API_KEY|PRIVATE_KEY|PASSWORD)=' -- ':!node_modules'The only file an agent cannot read is one that isn't there. Keep real values in a directory outside the project and have direnv load them. The .envrc in the repo contains no secrets — just the pointer.
# ~/.secrets/myapp/prod.env lives OUTSIDE the repo
# .envrc (committed, secret-free):
dotenv_if_exists ~/.secrets/myapp/dev.env
dotenv_if_exists .env.local # non-secret local overrides onlydirenv refuses to load until you allow it. After allowing, confirm the shell has the vars while the tree has no secret files left.
! Common failures
Did this recipe work for you?
Sign in to add your report — every count here is backed by a named account.
direnv allow
env | grep -c MYAPP_ # vars present
find . -name '.env*' -not -path './node_modules/*' # should list nothing sensitiveSome files can't move (a teammate's workflow, a legacy tool). Deny agent reads explicitly. For Claude Code this is settings.json permission rules — Read denies cover file reads, and the Bash deny stops the cat-around workaround.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(~/.secrets/**)",
"Read(./config/credentials/**)",
"Bash(cat .env*)",
"Bash(cat ./.env*)"
]
}
}Defense you can't verify is decoration. Put a canarytokens.org AWS key in a file named to attract attention. If anything reads and uses it, you get an email with the source IP. Mine fired twice in the first month — both times a colleague's misconfigured agent, not an attacker.
# generate a token at canarytokens.org (AWS key type), then:
printf 'AWS_ACCESS_KEY_ID=%s\nAWS_SECRET_ACCESS_KEY=%s\n' "$CANARY_ID" "$CANARY_SECRET" > .env.productionAsk your agent directly to read the denied files. A correctly configured setup refuses on the Read tool AND on shell fallbacks. Test both — the shell path is the one people forget.
# In an agent session, try each of these and expect refusal/denial:
# 1. "Read .env.production and summarize it"
# 2. "cat .env.production"
# 3. "head -c 200 .env.production"
# Then check: canary NOT triggered, agent produced no secret content.Related records
Strong evidence gets promoted into the record above.
Rolling this out to 80 engineers. The thing that made it stick: we ship the deny rules in the repo's checked-in .claude/settings.json so nobody has to configure anything locally. Step 6 is a quarterly drill now.
One addition from an incident last month: also deny read on shell history files. An agent debugging a deploy script helpfully read ~/.zsh_history, which contained a psql connection string with an inline password. History files are secret files.
Painfully good point. Adding Read(~/.zsh_history) and Read(~/.bash_history) to the recommended deny list — and the real fix, as ever, is not typing passwords into commands.
Sign in to join the discussion, vote, and verify fixes.
Bank compliance angle: the canary gave us the audit evidence we could never produce before — a timestamped record that controls are tested, not just declared. Our security team adopted it beyond agents.