●SLACK — Claude Tag launches in beta on Slack: tag @Claude into channels to delegate tasks and connect tools, data, and codebases●SECURITY — Claude Code adds a sandbox.credentials setting to block sandboxed commands from reading credential files and secrets●FIX — Remote MCP tool calls that once hung for five minutes now abort with an error instead of blocking●MCP — Enterprise MCP connectors gain Okta provisioning, giving users zero-touch access on first login●MODEL — Claude Fable 5 offers a 1M-token context, always-on adaptive thinking, and 128K output●LINEUP — Opus 4.8, Sonnet 4.6, and Haiku 4.5 lead the lineup; pick the right one per task●SLACK — Claude Tag launches in beta on Slack: tag @Claude into channels to delegate tasks and connect tools, data, and codebases●SECURITY — Claude Code adds a sandbox.credentials setting to block sandboxed commands from reading credential files and secrets●FIX — Remote MCP tool calls that once hung for five minutes now abort with an error instead of blocking●MCP — Enterprise MCP connectors gain Okta provisioning, giving users zero-touch access on first login●MODEL — Claude Fable 5 offers a 1M-token context, always-on adaptive thinking, and 128K output●LINEUP — Opus 4.8, Sonnet 4.6, and Haiku 4.5 lead the lineup; pick the right one per task
Your Sandbox Can Run the Code but Shouldn't Read Your Credentials — Shrinking the Secret-Read Surface with sandbox.credentials
Claude Code's sandbox can still read ~/.aws/credentials and token env vars by default. Using sandbox.credentials (v2.1.187+), here is how I tightened the secret-read surface of unattended runs at the OS level, with config and verification you can reuse.
I run Claude Code unattended on a fixed daily schedule, letting it build and test code inside the sandbox. One day I was idly looking at the sandbox read scope in the /sandbox Config tab, and I stopped. Writes were scoped down to the working directory and a temp area, but reads were still open to the entire machine — and that scope included ~/.aws/credentials and ~/.ssh/. A child process of some npm test the agent piped in could, in principle, read those files.
I had naively assumed the sandbox was a mechanism for "running code safely," but the default read policy was far broader than I imagined. As an indie developer running the Dolice Labs sites unattended, that read scope sat closer to my secrets than I was comfortable with. This article shares the actual work of shrinking that read exposure at the OS-enforcement level using sandbox.credentials, added in v2.1.187. If you don't pin down exactly what it blocks and what it doesn't, you leave a hole while feeling reassured — so I'll center the piece on that boundary.
Why the default read policy is open to the whole machine
The sandbox's default behavior is asymmetric between writes and reads. Writes are scoped to the working directory and its subdirectories, plus the session temp directory that $TMPDIR points to. Reads, by contrast, are open to the "entire machine" except for a few denied directories.
There's a reason for the asymmetry. Builds and tests often can't run unless they can read outside the working directory — tool caches under your home directory, shared system libraries, global config files, and so on. Scoping reads down to only the working directory breaks most toolchains. So the default opens reads wide and closes only writes tightly.
The problem is that this "wide-open read" includes your credentials. The official docs say it plainly: the default read policy still allows reading ~/.aws/credentials and ~/.ssh/. In an unattended pipeline this matters in a quiet but real way. The commands an agent generates and runs aren't always the same, and the moment a command that reads a credential file slips through, the sandbox itself does nothing to stop it.
What sandbox.credentials blocks, and what it doesn't
sandbox.credentials declares the credential files and environment variables that sandboxed commands must not access. Its behavior splits into two channels.
One is files. Paths listed here are denied for reads inside the sandbox — the same read block that filesystem.denyRead applies. The other is envVars. Variables listed here are unset before each sandboxed command runs. The point is that you can write the credential-file read denial and the secret-environment-variable removal together in a single credentials block.
Conversely, you need to know precisely what it does not stop. The table below organizes the scope of sandbox.credentials.
Target
Effect of sandbox.credentials
Sandboxed Bash commands and their child processes
files are read-denied; envVars are unset before each run
Built-in Read / Edit / Write file tools
Out of scope (governed by the permission system, not the sandbox)
Access via MCP servers
Out of scope
Exfiltration over the network
Out of scope (controlled separately by domain allowlist and proxy)
Commands that run with the sandbox disabled
Out of scope (they follow the regular permission flow)
So sandbox.credentials is a part that plugs only the "read path of sandboxed Bash." Having the built-in Read tool open a credential file, or having contents shipped out to a broad domain you allowed, must be defended at a different layer. Conflate these and you close one side while feeling safe. If you want to design secret handling end to end, reading Claude Code Secret Management and Trust Boundary Design, which covers how to partition trust boundaries, makes it clearer which layer sandbox.credentials belongs to.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Understand structurally why the sandbox's default read policy is open to the whole machine, credential files included
✦Reuse a Before/After sandbox.credentials config that separates files (denyRead) from envVars (unset)
✦Take away the in-sandbox commands that verify the policy and a minimal watchdog that guards against regression
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
In this state, the following commands succeed inside the sandbox.
# Before: readable even though we're inside the sandboxcat ~/.aws/credentialsecho "$GITHUB_TOKEN"
The After adds a credentials block. It denies reads of the AWS credentials file and the SSH directory, and removes GITHUB_TOKEN and NPM_TOKEN from the environment of sandboxed commands.
The "mode": "deny" on each entry is currently the only supported value. The schema makes mode explicit to stay forward-compatible with future modes. Paths in files follow the same prefix conventions as sandbox.filesystem.* (~/ is home, / is absolute, no prefix is relative to where the settings live).
One thing to note: there is no built-in deny list. Only the files and variables you list are restricted. Writing ~/.aws/credentials does not automatically protect ~/.config/gcloud. You have to inventory the credentials in your own environment and enumerate them explicitly.
envVars unset vs. environment scrubbing
There are two similar-but-distinct mechanisms around environment variables. They're easy to conflate, so keep the decision axis separate.
sandbox.credentials.envVars unsets the named variables only from the environment of sandboxed Bash commands. It doesn't affect your normal interactive work or commands that run outside the sandbox. It's well suited to pinpoint-removing "specific secrets you don't want handed to the sandbox."
If, regardless of whether sandboxing is on, you want to strip Anthropic and cloud-provider credentials from all subprocesses, use the CLAUDE_CODE_SUBPROCESS_ENV_SCRUB environment variable. Its reach is broader and applies even to non-sandboxed child processes.
Here's how I frame the choice. App-specific tokens (individual secrets you want to enumerate, like GITHUB_TOKEN or NPM_TOKEN) get removed via sandbox.credentials.envVars. When you want to broadly strip generic Anthropic/cloud credentials, layer in CLAUDE_CODE_SUBPROCESS_ENV_SCRUB. The two aren't mutually exclusive — they're parts meant to be stacked.
denyRead vs. credentials: why split it into its own block
The read denial in files is functionally identical to sandbox.filesystem.denyRead. So why split it into a separate credentials block?
The practical reason is readability and clarity of intent. filesystem.denyRead is the home for general filesystem rules, where purposes are mixed — protecting build artifacts, hiding caches, and so on. Mixing credential protection in there makes it hard to tell, when you read the config later, "what was the intent behind blocking this path?" The credentials block gathers file read denial and environment-variable unset under the single intent of "protect secrets." As the config grows, you can inspect credential protection in one place.
Worth pinning down the shared behavior, too. Entries in each scope of credentials (user, project, local, managed) are merged. And since the only mode is deny, any scope can add a restriction but none can remove one. In other words, a credential an organization or project has blocked can't be reopened by a lower scope overriding it. For the purpose of protecting secrets, that's a desirable fail-safe property.
Verifying the policy is in effect
Once you've written the config, always confirm it's working. Touch the credential files and environment variables inside the sandbox and watch them get denied. Verify in two directions.
Confirm the read denial
First, confirm the read denial. Try to open a credential file from sandboxed Bash and see it fail.
# After: both should fail inside the sandboxcat ~/.aws/credentials # fails, read deniedls ~/.ssh # directory read is blocked
Confirm the env-var unset
Next, confirm the env-var unset. Check that the variables listed in envVars are empty when a sandboxed command runs.
# If the output is empty (unset), the unset is in effectprintf 'GITHUB_TOKEN=[%s]\n' "${GITHUB_TOKEN:-}"printf 'NPM_TOKEN=[%s]\n' "${NPM_TOKEN:-}"
The crucial part here is that the verification commands are running inside the sandbox. A command that ran with the sandbox disabled (one that fell back via dangerouslyDisableSandbox) follows the regular permission flow, so the credentials restrictions don't apply. When you read the results, always check "was the command that just ran inside the sandbox?" Confirming the resolved settings in the /sandbox Config tab — that the paths and variables you intended are reflected — makes it more certain.
Place a regression watchdog
For unattended operation, rather than verifying once, it's reassuring to place a minimal watchdog that guards against regression. A check light enough to count the entries in credentials.files and envVars from the settings file and alert when they drop below the expected values is plenty useful.
#!/usr/bin/env bash# Minimal watchdog: alert if credential protection drops below expectationsset -euo pipefailSETTINGS="${1:-.claude/settings.json}"EXPECT_FILES="${2:-2}"EXPECT_ENVS="${3:-2}"# files and envVars both carry "mode":"deny", so count per blockn=$(python3 -c 'import json,sysc=json.load(open(sys.argv[1]))["sandbox"]["credentials"]print(len(c.get("files",[])), len(c.get("envVars",[])))' "$SETTINGS")read -r got_files got_envs <<< "$n"if [ "$got_files" -lt "$EXPECT_FILES" ] || [ "$got_envs" -lt "$EXPECT_ENVS" ]; then echo "credential protection shrank: files=$got_files(>=${EXPECT_FILES}) envVars=$got_envs(>=${EXPECT_ENVS})" >&2 exit 1fiecho "OK: files=$got_files envVars=$got_envs"
Since settings merge in the additive direction, a count below expectations is a sign that the configuration broke unintentionally in some scope. When the watchdog trips, revisit the config.
Enforcing across an org, and the limits the setting can't touch
To make this take effect beyond personal use, across a team or organization, deliver it through managed settings. The standard move is to require the sandbox, refuse to start if dependencies are missing, forbid escaping outside the sandbox, and then add directories like ~/.aws and ~/.ssh and your secret environment variables to credentials. Precisely because the default read policy leaves these open, the org-distributed settings are where credentials earns its keep.
If you want to pin read paths to the org-approved values only, set allowManagedReadPathsOnly to true in managed settings. User, project, and local allowRead entries are ignored, so developers can't widen reads on their own. Combined with credentials being deny-only and thus "non-removable," it's easier to close the back doors from lower scopes.
Finally, let me be honest about a limit sandbox.credentials can't touch. This is a part that shrinks the read and environment exposure surface; it does not stop exfiltration over the network. If you've put a broad domain (a generic host, say) on the allowlist, the path for code inside the sandbox to send data there remains. Blocking reads doesn't complete your defense if the network side is loosely allowed. Bearing in mind that the filtering is hostname-based and doesn't inspect TLS contents, the key is to revisit the filesystem and network sides together.
If you have an unattended pipeline, the next step is concrete. Look once at your sandbox's read scope in the /sandbox Config tab, and inventory the credential files included there and the tokens riding in your environment. Enumerate them in sandbox.credentials, and visually confirm the denial with cat and echo inside the sandbox. Only after doing this does the sandbox move toward a state where it "can run the code but can't read your secrets."
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.