Credential Protection
- Identify the credential exposure paths that sandboxing alone does not close by default
- Configure sandbox.credentials to deny or mask specific files and environment variables for sandboxed Bash commands
- Apply CLAUDE_CODE_SUBPROCESS_ENV_SCRUB to strip cloud provider credentials from all subprocesses regardless of sandbox state
The Credential Gap the Sandbox Leaves Open
Enabling the sandbox dramatically reduces what Bash commands can access. But two credential exposure paths remain open by default that most developers do not realize:
- Filesystem reads: The sandbox default read policy covers the entire filesystem. That means sandboxed commands can read
~/.aws/credentials,~/.ssh/id_rsa,~/.config/gcloud/credentials.json, and any other credential file on disk — unless you explicitly block them. - Environment variable inheritance: Sandboxed Bash commands inherit the parent process's environment, including any tokens or passwords set as environment variables (
GITHUB_TOKEN,NPM_TOKEN,DATABASE_URL, and so on). The sandbox does not strip them.
A prompt injection that convinces a sandboxed process to cat a credential file or echo an environment variable and send it to an allowed domain can exfiltrate secrets despite sandbox being enabled. Explicit credential protection is a separate configuration step.
The sandbox.credentials Block
The sandbox.credentials settings block is the purpose-built mechanism for protecting credentials from sandboxed commands. It has two sub-arrays: files for filesystem paths and envVars for environment variables.
A minimal configuration protecting common credential locations:
{ "sandbox": { "credentials": { "files": [ { "path": "~/.aws", "mode": "deny" }, { "path": "~/.ssh", "mode": "deny" }, { "path": "~/.config/gcloud", "mode": "deny" } ], "envVars": [ { "name": "GITHUB_TOKEN", "mode": "deny" }, { "name": "NPM_TOKEN", "mode": "deny" }, { "name": "AWS_SECRET_ACCESS_KEY", "mode": "deny" } ] } } }
Deny Mode
For file entries, "mode": "deny" blocks reads of that path inside the sandbox — the same restriction that sandbox.filesystem.denyRead applies, but grouped separately for clarity. For environment variable entries, "mode": "deny" unsets the variable before each sandboxed command runs. The command and anything it logs never hold the real value.
The downside: if a tool needs the variable to authenticate (gh, npm, terraform), denying it also breaks the tool. For that use case, mask mode is the answer.
Mask Mode
Mask mode lets sandboxed tools continue to authenticate while keeping the real credential out of the process environment and out of logs. Instead of the real token, the sandboxed command sees a per-session sentinel value. When a request leaves the sandbox for one of the credential's injectHosts, the sandbox proxy replaces the sentinel with the real value at the network layer — the tool sends a valid credential, but the process itself never held it.
{ "name": "GITHUB_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"] }
Mask mode requires the proxy to terminate TLS so it can inspect and rewrite request contents. Set sandbox.network.tlsTerminate: {} in your configuration. Without it, masking fails closed: the sentinel reaches the server unchanged, authentication fails, and Claude Code reports the misconfiguration at startup and in /doctor. This is the right failure mode — the alternative (silently passing the sentinel through) would look like it's working while leaking the unsubstituted sentinel to the server.
Mask entries are honored only from user settings, managed settings, or the --settings CLI flag. A project's .claude/settings.json or .claude/settings.local.json cannot configure masking — this prevents a repository from setting up credential injection into arbitrary hosts.
Stripping Credentials from All Subprocesses
The sandbox.credentials block applies only to sandboxed Bash commands. For environments where you want credentials stripped from every subprocess regardless of sandboxing, set the environment variable:
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1
When set, Claude Code removes Anthropic API keys and common cloud provider credential variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, GOOGLE_CLOUD_PROJECT, and others) from the environment of all subprocesses it spawns. This is a blunt instrument — it does not support per-host injection like mask mode — but it works without sandboxing being enabled and with no additional configuration.
Credential Storage for API Keys
Claude Code stores your Anthropic API key in the macOS Keychain when available, and protects it with file permissions on Windows and Linux. This means the key is not stored in a plaintext configuration file where an accidental cat ~/.claude/settings.json would expose it. If you are configuring Claude Code in environments that cannot use the Keychain (headless servers, CI containers), be deliberate about how the key is injected — environment variables with the CLAUDE_CODE_SUBPROCESS_ENV_SCRUB approach, or masked via the credentials block.
A Practical Credential Policy
A reasonable starting configuration for a developer machine:
- Deny reads of
~/.aws,~/.ssh,~/.config/gcloud,~/.azure - Mask
GITHUB_TOKENwithinjectHosts: ["api.github.com"]if you need gh to work inside the sandbox - Deny
NPM_TOKENif your workflow does not need npm publish inside the sandbox, or mask it forregistry.npmjs.orgif it does - Set
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1as a belt-and-suspenders measure for unsandboxed subprocesses
There is no built-in deny list — only the files and variables you explicitly configure are protected. Review your common credential locations and sensitive tokens when setting this up, rather than assuming the defaults cover them.
- The sandbox default read policy covers the entire filesystem, meaning sandboxed commands can read ~/.aws/credentials and ~/.ssh/ unless you explicitly block them with sandbox.credentials.files deny entries.
- sandbox.credentials envVars with mode:deny unsets the variable before each sandboxed command — useful for tokens you do not need inside the sandbox, but it breaks tools that authenticate with that token.
- Mask mode substitutes a per-session sentinel for the real credential in the process environment and injects the real value only at the network layer for specified injectHosts — tools still authenticate but the process never holds the real secret.
- Mask mode requires network.tlsTerminate so the proxy can inspect and rewrite request contents — without it, masking fails closed with authentication failures rather than silently passing the sentinel through.
- CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 strips Anthropic and common cloud provider credentials from all subprocesses regardless of sandbox state — a blunt but broadly applicable backstop.