Skip to main content
You’ll learn: which settings keep a secret out of Claude’s context, which door each one closes, and the minimal config that closed all of them in our tests.
The short answer: a Read deny rule stops Claude’s file tools. The sandbox stops the shell. sandbox.credentials stops environment variables. You need all three — each one leaked in our tests where the others didn’t. .claudeignore does nothing. We put fake “canary” secrets in a toy project: a .env, a service-account.json, and an environment variable. Then we asked Claude Code to leak them 12 different ways, under 20 setups, twice each: 258 runs. One question per run: did the canary show up anywhere the model could see it? Every leak also ended up in Claude’s final answer.
Why this matters: once a secret is in the context, it’s on the wire to the model provider and saved in plain text in your local session history. Rotating the key is then the only fix.

The Results

Tested on Claude Code 2.1.287, macOS, in bypassPermissions mode — the worst case, because nobody is there to say no. 2 runs per cell, so a unanimous cell is not a measured rate. The sandbox.credentials row combines two setups: file prompts ran with all three layers plus the scrub variable, env prompts ran with the three layers alone. The full breakdown is in RESULTS.md. Two things jump out:
  • Each layer closes a different door. Deny rules catch Claude’s own file tools and the shell commands Claude Code recognizes, like cat. They miss a script that opens the file itself. The sandbox catches every shell command — but it never touches environment variables.
  • The environment is the door nobody closes. Every setup without an environment control leaked the variable, including the full sandbox runtime.

The .claudeignore Myth

The .claudeignore “HELD” cells are not protection. Claude opened the .claudeignore file, read it as a request, and chose to honour it. Asked to use the Read tool on service-account.json — which the same .claudeignore lists — it leaked 2 out of 2 times. The official docs say it plainly: a .claudeignore file “has no effect”.

The Minimal Config

Three doors lead into Claude’s context: its file tools, any shell command, and environment variables. Each layer below closes one of them. Copy what you need — each block is a layer the tests confirmed.

Layer 1: Deny Claude’s File Tools

In .claude/settings.json (project) or ~/.claude/settings.json (all projects):
This stopped the Read tool and cat .env in every run. ./.env and bare names like Read(.env) are relative to the folder you start Claude in, so they also work from user settings. Watch out for a single leading slash. In user settings, Read(/secrets/**) points inside ~/.claude, not your project.

Layer 2: Put the Shell in a Sandbox

  • failIfUnavailable — if the sandbox can’t start, Claude Code stops instead of quietly running unsandboxed.
  • allowUnsandboxedCommands: false — Claude can’t retry a blocked command outside the sandbox. In bypassPermissions mode that retry would run with no prompt.
With this on, the Python script and grep -r got Operation not permitted from the operating system. In user settings, ./ points to ~/.claude, so write paths like ~/**/.env instead.

Layer 3: Hide the Environment Variables

List every secret variable by name:
The variable was unset inside every sandboxed command, with this block in project settings, user settings, or a --settings file. There is no built-in list — only what you name is hidden. We tested the envVars entry; the two files entries follow the docs’ own example and were not tested. Need the command to still authenticate? Use "mode": "mask" instead. Commands see a placeholder (fake_value_<uuid> in our runs) and the sandbox proxy swaps in the real value on outbound requests. It needs "network": { "tlsTerminate": {} } too.
Two catches with mask mode:
  • It works only from user settings, managed settings, or --settings. In a project’s .claude/settings.json it is ignored — the real value leaked 2 out of 2 times.
  • It requires Claude Code v2.1.199 or later.

What Each Layer Doesn’t Cover

Not tested: MCP servers, hooks, Linux, interactive sessions, and other ways a file reaches context, like @file mentions or IDE selections.
Plan type changes how long a leak lives. Per Anthropic’s data-usage page: Team, Enterprise and API keep data 30 days by default. Free, Pro and Max keep it 5 years if you allow your data to improve models, 30 days if you don’t. Your local session files are kept 30 days by default (cleanupPeriodDays).

On a Personal Plan?

Everything above works without managed settings. Put the three layers in ~/.claude/settings.json and they apply to every project. Our tests ran deny rules and the sandbox from project settings, and credentials from user settings too. What managed settings add is a lock, not a new layer: a repository’s own settings can’t loosen them. On a personal machine, you are the admin. Re-check a repo’s .claude/settings.json before you run Claude in it.

Hooks: Not the Tool for This

Hooks are a good tripwire — the Hooks Playbook covers how to compose them — but they are a text match on the command. The official hooks guide’s file-protection example guards edits (Edit|Write), not reads. We did not invent a read-guard hook to test. One community project, wardenv (not tested here), does block .env reads with hooks and redacts output. By its own README it is designed to “fail open” so it never breaks the session. Treat a hook as an extra layer on top of the three above, never in place of them.

Better: Don’t Give It a Long-Lived Key at All

The safest secret is one that expires before anyone can use it. Documented, not tested in this POC. For Google Cloud, skip the service-account key file. Use your own login to borrow the service account’s identity:
Google’s docs say impersonation is “more secure than using a service account key”, because the credentials it creates “do not persist”. Short-lived access tokens last 1 hour by default (up to 12 hours if your org allows it). You need the Service Account Token Creator role (roles/iam.serviceAccountTokenCreator) on the account. That command still writes a credentials file at ~/.config/gcloud/application_default_credentials.json. Add ~/.config/gcloud to sandbox.credentials.files with deny, as in Layer 3.

Run It Yourself

The proof of concept is public. The test harness, the 20 setups (arms/), the toy project with fake secrets, and the full RESULTS.md all live in this repo. The raw run logs aren’t published — RESULTS.md carries the tables generated from them.

poc/secrets-safety

Harness, setups, toy project and full results for every cell on this page.
You need uv, the Claude Code CLI, and POC_GATEWAY: the address of a local Anthropic-compatible proxy that holds your API key (LiteLLM, for example). The harness never passes a real key to Claude Code — the child process only gets a dummy bearer. That’s on purpose: some of the attack prompts dump the environment, so a real key must never be in it.
A ready-made gateway is coming. The Agentic Kit will soon ship its gateway publicly. Until then, any Anthropic-compatible proxy you run locally works.
From poc/secrets-safety/, this reruns one cell — the sandbox setup against the printenv prompt (the port is an example):

Further Reading

  • Sandboxing and Sandbox environments: what the sandbox covers, and the sandbox runtime
  • Permissions: Read and Edit: what deny rules cover
  • Prior art, not tested here: promptfoo’s coding-agent:secret-env-read and coding-agent:secret-file-read red-team plugins (the same canary-string method), anthropics/sandbox-runtime, and claude-code issues #44868 and #58173 (user reports of .env leaks despite CLAUDE.md rules)
  • gitleaks and trufflehog find secrets committed to git. They don’t stop a live session from reading your working copy.

Running agents unattended? See Permission Modes for when each mode fits, and the Hooks Playbook for tripwires on top of the three layers above.