How to Set Claude Code Sandbox Settings and Test a Blocked Write
Enable Claude Code's sandbox in settings.json, allowlist domains, mask credentials, and run three probes that prove a blocked write and a blocked request fail.

Claude Code's sandbox is turned on from a settings file, not a flag: add "sandbox": {"enabled": true} to ~/.claude/settings.json and the shell commands Claude runs are sandboxed. The same sandbox object holds the network allowlist under network.allowedDomains, the TLS termination switch for the sandbox proxy, and credential rules that mask or deny secrets inside sandboxed commands.
The working recipe is three steps. Enable the sandbox in user settings, list the domains your build tools need, then ask Claude to write a file somewhere it shouldn't and confirm from a separate terminal that the file never appeared. The sections below walk through each step, the scope rules that silently drop some keys, and the claude doctor checks that catch the usual misconfigurations. Everything here reflects the official Claude Code sandboxing docs as of October 19, 2026.
Where the sandbox settings JSON goes#
Claude Code reads settings from several places, and for the sandbox the file you pick matters more than it does for most keys.
~/.claude/settings.jsonis your user settings file. It applies to every project, and it's a trusted scope for the credential keys covered later. This is where I'd put sandbox config..claude/settings.jsoninside the project is the shared, committed file. Claude Code reads it from the session's primary working directory, so start Claude Code at the repository root if that's where the file lives..claude/settings.local.jsonholds personal overrides for one project and is applied over the committed file. Claude Code also writes permission approvals here and adds the file to your global git excludes the first time it writes it.- The
--settingsflag points at a settings file for one session. - Managed settings come from an administrator and take precedence over the files above.
You can also change settings from the /config menu inside a session. The Claude Code settings page covers precedence in detail, including a handful of restrictive keys that win from any scope.
The sandbox-specific catch is scope trust. Claude Code honors credential mask entries, network.tlsTerminate, credentials.allowPlaintextInject, awsPairs, and sigv4 only from user settings, managed settings, and --settings. If those keys sit in a repository's .claude/settings.json or .claude/settings.local.json, they're ignored. The reasoning is sound: a mask entry authorizes the proxy to send your real secret somewhere, and a cloned repo shouldn't get to decide that. Keep enabled and allowedDomains wherever you like, but keep the credential block in your user file.
Turn the sandbox on#
The smallest config that does something:
{
"sandbox": {
"enabled": true
}
}
Save it to ~/.claude/settings.json and start Claude Code. From then on, shell commands Claude runs go through the sandbox, which brings two layers: filesystem isolation, which limits where sandboxed commands can read and write, and network isolation, where a sandbox proxy decides which outbound connections are admitted.
If the file already has other keys, merge the sandbox object in rather than replacing the file. Claude Code writes to settings files itself, so overwriting one can drop permission rules you've accumulated.
Set network limits with allowedDomains#
Outbound connections from sandboxed commands go through the sandbox proxy, and the allowlist in network.allowedDomains decides which hosts it admits. List the hosts your tooling actually calls:
{
"sandbox": {
"enabled": true,
"network": {
"allowedDomains": ["*.github.com", "registry.npmjs.org"]
}
}
}
The docs' own example uses *.github.com, so subdomain wildcards work. Whether a *. entry also covers the bare apex domain isn't stated, so list github.com on its own if you need it. For an IPv6 destination, use the bracketed form in this list, such as "[::1]".
There's a second key under network: tlsTerminate. Setting it to an empty object makes the proxy terminate TLS itself, which is what lets it substitute real credentials inside request contents. Masking requires it. If you aren't masking anything, you can leave it out. Like the credential keys, it's honored only from user, managed, or --settings scope.
What filesystem isolation does and the one path rule to know#
Filesystem isolation is part of the sandbox. The sandboxing page documents how to disable it, and I'd leave it on: it's the layer that makes a blocked write fail, and a few protections, like the macOS credential-file block, hold even when it's off. The path fields that tune it are listed under the sandbox keys in the settings reference, so pull exact names from there rather than from memory.
One rule from the settings page trips people up: a relative sandbox path anchors at the session's primary working directory, not at the repository root. That matters because Claude Code stores .claude/settings.local.json at the repository root when you start in a subdirectory, yet relative paths in it still resolve from where the session started. If you move the session with /cd, project settings are re-read from the new directory. Use absolute or ~-prefixed paths when you want a limit to mean the same thing no matter where you launch.
Test that a blocked write fails#
Don't trust a config you haven't watched fail. I'd start a fresh session so there's no question about which settings were loaded, run three probes, and check the results from a second terminal rather than from Claude's summary.
- Positive control. Ask Claude to run
echo ok > sandbox-probe.txtin the project directory. That should succeed. If it doesn't, your filesystem config is tighter than the project needs. Delete the file afterward. - Blocked write. Ask Claude to run
touchon a path outside the project and outside anything you've allowed, for example a sibling directory of your project, and to report the exit code. Then runlson that path from your own terminal. The file shouldn't exist. - Blocked network. Ask Claude to run
curl -sS -I https://example.comwithexample.comabsent fromallowedDomains. The command should fail.
The sandboxing page doesn't publish the exact wording of a blocked-write error, so don't script around a message string. Check whether the file exists, and check the exit status. If the probe write succeeds, the usual causes are the sandbox config sitting in a file that isn't being read, a session started before the settings change, or a working directory that's broader than you think. claude doctor reports sandbox warnings and is the first thing to run.
For credential masking there's a documented check: ask Claude to run cat ~/.config/gh/hosts.yml in a sandboxed command. On Linux and WSL2 the output shows a per-session placeholder, called the sentinel, instead of the token. On macOS the read fails outright.
Before you save a config, you can lint the parts that fail silently. This script checks the rules the docs describe and that claude doctor warns about: mask entries without TLS termination, inject hosts the allowlist can't reach, bracketed IPv6 spellings that never match, and credential keys placed in a project-scoped file.
import json
SETTINGS_FILE = "~/.claude/settings.json" # where you plan to save this
SETTINGS = """
{
"sandbox": {
"enabled": true,
"network": {
"allowedDomains": ["*.github.com", "registry.npmjs.org", "[::1]"]
},
"credentials": {
"envVars": [
{"name": "GH_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"]},
{"name": "NPM_TOKEN", "mode": "mask"},
{"name": "LOCAL_TOKEN", "mode": "mask", "injectHosts": ["[::1]"]}
]
}
}
}
"""
def host_allowed(host, allowed):
# Rough pre-flight approximation of the proxy's allowlist, not the real matcher.
for entry in allowed:
if entry.startswith("*.") and host.endswith(entry[1:]):
return True
if entry.strip("[]") == host:
return True
return False
def lint(settings, path):
problems = []
sb = settings.get("sandbox", {})
if sb.get("enabled") is not True:
problems.append("sandbox.enabled is not true, so nothing else here takes effect")
net = sb.get("network", {})
allowed = net.get("allowedDomains", [])
creds = sb.get("credentials", {})
masks = [e for key in ("envVars", "files")
for e in creds.get(key, []) if e.get("mode") == "mask"]
trusted_scope = path.startswith("~/.claude/")
if masks and not trusted_scope:
problems.append(f"{path} is project scope: mask entries and tlsTerminate are ignored there")
if masks and "tlsTerminate" not in net:
problems.append("mask entries need network.tlsTerminate, otherwise the sentinel reaches the server and auth fails")
for e in masks:
label = e.get("name") or e.get("path")
for h in e.get("injectHosts", []):
if h.startswith("[") or "%" in h:
problems.append(f"{label}: injectHosts entry {h!r} can never match; use the bare address")
elif not host_allowed(h, allowed):
problems.append(f"{label}: {h} is not reachable through allowedDomains")
return problems
for p in lint(json.loads(SETTINGS), SETTINGS_FILE) or ["no problems found"]:
print("-", p)
Keep secrets out of sandboxed commands#
Once the sandbox is on, the credentials block decides what a sandboxed command sees in environment variables and files. deny blocks the credential. mask shows the command a sentinel and has the sandbox proxy swap in the real value on outbound requests to hosts you allow, so gh or npm still authenticates but the command and its logs never hold the secret.
{
"sandbox": {
"enabled": true,
"network": {
"tlsTerminate": {},
"allowedDomains": ["*.github.com", "registry.npmjs.org"]
},
"credentials": {
"envVars": [
{ "name": "GH_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"] },
{ "name": "NPM_TOKEN", "mode": "mask" }
]
}
}
}
GH_TOKEN is substituted only on requests to api.github.com. NPM_TOKEN has no injectHosts, so it's substituted on requests to every host in allowedDomains. If the same variable is listed with deny in any scope, deny wins. When an administrator pushes mask entries through server-managed settings, they count as settings that need approval.
Files work the same way through credentials.files, with an extract pattern that marks the secret part so the rest of a config file stays readable on Linux and WSL2:
{
"sandbox": {
"enabled": true,
"network": {
"tlsTerminate": {},
"allowedDomains": ["*.github.com"]
},
"credentials": {
"files": [
{
"path": "~/.config/gh/hosts.yml",
"mode": "mask",
"extract": "oauth_token:\\s*(\\S+)",
"injectHosts": ["api.github.com"]
}
]
}
}
}
Masking files requires Claude Code v2.1.221 or later. On macOS a masked file is blocked instead of masked, the same effect as deny, so tools that authenticate with that file won't work inside the sandbox there. Structured values such as a DATABASE_URL or a JWT have extract, decode, maskClaims, and onExtractNoMatch fields in the settings reference so tools that parse the value keep working. AWS is its own case: mask AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY together so the proxy can re-sign SigV4 requests, which needs v2.1.221 or later. The awsPairs and sigv4 options need v2.1.224 or later.
If you run Claude Code unattended, this is the layer that matters most. Combine it with a headless setup using claude -p and you get commands that can call GitHub without ever being able to print the token.
Common mistakes and gotchas#
- Credential keys in the wrong file.
mask,tlsTerminate,allowPlaintextInject,awsPairs, andsigv4are ignored in.claude/settings.jsonand.claude/settings.local.json. Nothing errors; auth just fails. - Masking without TLS termination. The command still sees only the sentinel, but the sentinel reaches the server unchanged and authentication fails.
claude doctorreportsTLS termination is unavailable. - An inject host the allowlist can't reach. Every
injectHostsentry must also be reachable throughnetwork.allowedDomains. - IPv6 spelled the same way in both lists.
allowedDomainswants"[::1]"andinjectHostswants the bare"::1". A bracketed, zone-ID, or differently compressed spelling never matches, andclaude doctorflags it from v2.1.229 on. - Masking only the AWS secret. Requests get signed with a placeholder the proxy can't detect and fail at AWS. Mask the access key ID too.
- A file mask whose pattern matches nothing. The default
onExtractNoMatchiswarn, which skips the entry, so the real file is readable unmasked. Usedenywhen the secret might be present but the pattern might miss it. - A mask entry that quietly becomes a deny. Directory paths, glob patterns, files over 8 MiB, and files that aren't UTF-8 text fall back to
deny. List each credential file individually. jesthanging or failing.watchmanis incompatible with the sandbox, so runjest --no-watchman.- Treating the sandbox as a hard boundary. The docs say it reduces risk but is not a complete isolation boundary. For rules about what Claude may run in the first place, pair it with Claude Code hooks.
Quick reference#
| Key | What it does | Trusted scope only |
|---|---|---|
sandbox.enabled |
Turns the sandbox on | No |
sandbox.network.allowedDomains |
Hosts the sandbox proxy admits; IPv6 in brackets | No |
sandbox.network.tlsTerminate |
{} makes the proxy terminate TLS; required for masking |
Yes |
sandbox.credentials.envVars[] |
name, mode of mask or deny, optional injectHosts |
mask entries: yes |
sandbox.credentials.files[] |
path, mode, optional extract and injectHosts |
mask entries: yes |
sandbox.credentials.awsPairs, sigv4 |
Group non-standard AWS variables; forward SigV4 forms the proxy can't re-sign | Yes |
Checklist before you rely on it:
"sandbox": {"enabled": true}is in~/.claude/settings.json, merged with existing keys.allowedDomainslists every host your tools call, with IPv6 in brackets.- The
credentialsblock andtlsTerminatelive in user, managed, or--settingsscope. claude doctorshows no sandbox warnings.- The in-project write succeeds, the probe write outside the project fails, and the probe
curlfails. catof a masked file shows the sentinel on Linux or WSL2, or fails on macOS.
Quick answers
What does sandbox mean in Claude Code?
It's a restriction layer around the shell commands Claude runs. Filesystem isolation limits where those commands can read and write, and a sandbox proxy admits only outbound connections to hosts in network.allowedDomains. The docs call it risk reduction, not a complete isolation boundary.
Where do I put the Claude Code sandbox settings JSON?
The safest place is ~/.claude/settings.json, your user settings. Project files like .claude/settings.json and .claude/settings.local.json can carry enabled and allowedDomains, but mask entries, tlsTerminate, allowPlaintextInject, awsPairs, and sigv4 are ignored there.
How do I disable the Claude Code sandbox?
Set sandbox.enabled to false in the same settings file, or remove the sandbox block. The sandboxing page also documents turning off only filesystem isolation while keeping the rest, though some macOS credential-file blocks still hold with it off.
Why does my masked token fail to authenticate?
Usually network.tlsTerminate is missing, so the sentinel reaches the server unchanged, or the mask entry sits in a project-scoped file where it's ignored. Run claude doctor and look for the TLS termination is unavailable warning.
What is the difference between mask and deny?
deny blocks the credential from sandboxed commands entirely. mask shows them a per-session sentinel and lets the proxy substitute the real value on requests to allowed hosts, so tools still authenticate. When both apply to the same variable, deny wins.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.
Network
0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
On a computer? Scan the code with your phone wallet, or copy the address.
Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note