Issue 00 · Autumn 2026日本語 · RSS
AgentKoji

Keep Secrets Away from Claude Code

Claude Code API key management needs more than a deny rule: grep and Python can still read .env. We reproduced the leak paths with a dummy key.

The standard way to keep Claude Code out of .env is a Read(./.env) deny rule. Some paths stay open anyway. We reproduced the ones the official docs themselves list as out of scope, using a dummy key, and then describe how to keep keys out of the agent’s reach. For how permission rules are evaluated, see Permission Design for AI Agents.

Setup

Linux test box, Claude Code 2.1.301, 2026-10-16. A temp directory and a shell started with env -i (empty environment). Every key shown is the dummy sk-dummy-0000-NOT-REAL. We did not observe Claude Code’s own allow/deny decisions inside a session; which paths are blocked comes from the docs. What we verified is that the unblocked paths really print the contents.

What deny covers, per the docs

From https://code.claude.com/docs/en/permissions (checked 2026-10-16): Read and Edit deny rules apply to built-in file tools, to file commands Claude Code recognizes in Bash (cat, head, tail, sed, tee) and to redirection targets. They don’t apply to commands that read files without naming them, such as grep -r pattern ., or to subprocesses that open files themselves, like Python or Node scripts. For OS-level enforcement, the docs point to the sandbox.

Reproduced with a dummy key

d=$(mktemp -d) && cd "$d"
env -i PATH=/usr/bin:/bin HOME="$d" bash --norc --noprofile
printf 'DUMMY_API_KEY=sk-dummy-0000-NOT-REAL\n' > .env
PathCommandOutput
Search without naming the filegrep -r DUMMY_API_KEY ../.env:DUMMY_API_KEY=sk-dummy-0000-NOT-REAL
Python opens itpython3 -c "print(open('.env').read())"DUMMY_API_KEY=sk-dummy-0000-NOT-REAL
Node opens itnode -e "...readFileSync('.env')..."DUMMY_API_KEY=sk-dummy-0000-NOT-REAL
Inherited envbash -c 'env | grep DUMMY' after exportDUMMY_API_KEY=sk-dummy-0000-NOT-REAL
Shell tracebash -x -c 'curl -H "Authorization: Bearer $DUMMY_API_KEY" ...'+ curl ... 'Authorization: Bearer sk-dummy-0000-NOT-REAL' ...

set -x prints the expanded key into the trace, which lands in logs and in the agent’s context. Blocking a file and hiding a key are different jobs. Deny only does the first.

Environment variables reach subprocesses

Keys exported in the launching shell flow to Bash commands, hooks and stdio MCP servers. CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 strips credentials from those subprocesses, matching by variable name or value shape; GITHUB_TOKEN and secrets that don’t look like credentials remain, so the docs say to pair it with narrow permission rules (https://code.claude.com/docs/en/env-vars, checked 2026-10-16). Hook processes inherit the parent environment too (https://code.claude.com/docs/en/hooks, checked 2026-10-16).

When a variable you set does not take effect at all, see Claude Code Env Vars Not Working.

Close it at the OS level

The sandbox restricts file reads and network for Bash commands and their children; filesystem.denyRead covers credential files like ~/.ssh (Seatbelt on macOS, bubblewrap on Linux/WSL2). denyRead does not stop the built-in Read tool, so set both (https://code.claude.com/docs/en/sandboxing, checked 2026-10-16). A PreToolUse hook can block commands mentioning .env (Claude Code Hooks in Practice), but it can’t see grep -r ..

The trailing newline

Some commands that fetch a key from an OS credential store end with a newline. We stored one as a secret as is; the key saved with the newline, and a notification path failed every time without telling anyone. With the dummy:

get(){ printf 'sk-dummy-0000-NOT-REAL\n'; }
get | wc -c                          # 23 bytes, newline included
s="$(get)"; printf %s "$s" | wc -c   # 22 bytes

Piping keeps the newline; $(...) drops it. Check the byte count before you store a secret.

Show the agent aggregates only

When an agent inspects data with personal information, select non-personal columns and output counts and totals, never rows; transcripts keep everything. Before anything leaves the machine (web fetches, issue bodies, review requests to another model), check the files contain no secrets. Review requests are the path people forget.

Don’t put the key there

Best is no plaintext key where the agent runs. 1Password CLI’s op run, for example, resolves op:// references from a .env file at runtime and passes them as environment variables only to the subprocess it starts (https://developer.1password.com/docs/cli/secrets-environment-variables/, checked 2026-10-16). The same page warns that processes run by the same user can read each other’s environment. Inside that process, the key is an ordinary variable. For keeping a bot token out of the LLM’s context, see Run an AI Agent 24/7 on a VPS.

Summary