Claude Code's sandbox allowlist, sandbox.network.allowedDomains, stops a shell command and every process it starts from reaching a host you didn't list. It does nothing for WebFetch, for Monitor's WebSocket watches, for the files Claude writes, or for MCP tool calls. Turn the allowlist on in strict mode, then add two fail-closed PreToolUse hooks for those paths. At the end, shell commands, WebFetch and WebSocket watches reach only hosts you approved, and writes carrying your denylisted values are blocked.
Some gaps remain, and you should know them before you rely on this. Local MCP servers, hooks, plugin monitors, LSP servers, your status line command and apiKeyHelper run outside the sandbox with your full access, and WebSearch follows permission rules, not the allowlist. With the policy in user settings alone, a repository's own .claude/settings.json can switch the hooks off or take commands out of the sandbox, and its own hooks and .mcp.json servers run outside the sandbox entirely. Step 1 shows the flags that keep a repository's settings files and .mcp.json out of a session, and what you still have to check by hand.
If you run Claude Code 2.1.295 or later on macOS, Linux or WSL2 and want the egress controls our post on the User-Agent email leak recommended set up and proven rather than sketched, start here. Every setting below was checked against the current official docs, and every expected result in Steps 2 to 6 is output we captured, with our scratch paths rewritten to /Users/you/.claude. Tested on macOS 27.0.1 (arm64) with Claude Code 2.1.322, bash 3.2.57, jq 1.7.1 and Python 3.15.1, November 2026.
Prerequisites
- Claude Code 2.1.295 or later. Check with
claude --version; ours printed2.1.322 (Claude Code). The floor comes fromonFailureon hooks (2.1.295).strictAllowlistneeds 2.1.219, and the settings locks this guide relies on need 2.1.285. - macOS, Linux or WSL2. On native Windows, Claude Code runs commands unsandboxed, so the network layer below doesn't exist there; run Claude Code inside WSL2 instead. On Linux and WSL2, install the sandbox dependencies with
sudo apt-get install bubblewrap socat(Ubuntu/Debian) orsudo dnf install bubblewrap socat(Fedora). macOS uses the built-in Seatbelt framework and needs nothing extra. - jq, which both hooks use. Check with
jq --version. - A copy of your user settings you can restore:
cp ~/.claude/settings.json ~/.claude/settings.json.bak(skip it if the file doesn't exist yet; Rollback then means deleting it). - The list of hosts your work actually needs: your package registry, your git host, the APIs your tests call. Start short. Step 3 shows how to find the ones you missed.
Step 1. Turn on the sandbox with a strict allowlist in user settings
Put the network policy in ~/.claude/settings.json, because strictAllowlist only counts in user, managed or --settings settings. The sandboxing docs say setting it in a repository's .claude/settings.json or .claude/settings.local.json has no effect. Save this as ~/.claude/sandbox.json, replacing the three hosts with your own:
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"network": {
"allowedDomains": ["github.com", "*.github.com", "registry.npmjs.org"],
"strictAllowlist": true
}
}
}Each key closes a specific hole:
enabled: the sandbox is off by default. Once on, it covers Bash, PowerShell and Monitor commands and the processes they start.failIfUnavailable: without it, a missing dependency or an unsupported platform makes Claude Code run commands unsandboxed. With it, Claude Code exits at startup instead.allowUnsandboxedCommands: false: by default Claude can retry a blocked command outside the sandbox with thedangerouslyDisableSandboxparameter, and inbypassPermissionsmode that retry runs without a prompt.falsemakes Claude Code ignore the parameter. On 2.1.285 and later afalsein user settings holds even when a project setstrue, but it doesn't make the sandbox admin-required, so a project's other sandbox settings still apply.allowedDomains: hosts sandboxed commands may reach.*.github.commatches subdomains, and an entry without:portmatches every port on that host. The list starts empty.strictAllowlist: without it, an unlisted host gets a prompt in Manual mode and goes through with no prompt inbypassPermissionsmode. With it, the proxy refuses unlisted hosts in every permission mode.
allowedDomains isn't the whole allowlist. Claude Code adds the host of every WebFetch(domain:...) allow rule to it, and strict mode keeps the ones in your own settings. List the ones you already have and remove any host you don't want shell commands to reach:
jq '.permissions.allow // [] | map(select(startswith("WebFetch(domain:")))' ~/.claude/settings.jsonIf ~/.claude/settings.json doesn't exist yet, jq reports that it can't open the file, which means there are no rules to remove.
Merge the sandbox block into your existing settings so other keys survive, then print the result:
cd ~/.claude
[ -f settings.json ] || echo '{}' > settings.json
jq -s '.[0] * .[1]' settings.json sandbox.json > settings.json.new && mv settings.json.new settings.json
jq '.sandbox' settings.jsonYou should see your sandbox block printed with "strictAllowlist": true under network. The * merge keeps sibling keys such as sandbox.filesystem, but an existing allowedDomains array is replaced by the new one, so put every host you need in sandbox.json. Start a new Claude Code session to pick the policy up.
If Claude Code now exits at startup on Linux, failIfUnavailable is doing its job: install bubblewrap and socat, restart, and run /sandbox to see which dependency its Dependencies tab still lists. On WSL1 the message is Sandboxing requires WSL2, and the fix is upgrading the distribution.
User settings alone don't stop a repository from loosening this. Project settings outrank user settings, and because the sandbox isn't admin-required, a repository's .claude/settings.json or .claude/settings.local.json can still do three things. Strict mode only stops it from adding hosts and proxy ports:
- add
excludedCommandsentries, which run with no filesystem restrictions and no network proxy - switch
sandbox.enabledorfailIfUnavailabletofalse - set
"disableAllHooks": true, which turns off every user, project, local and plugin hook, including the two from Steps 4 to 6
We saw the third one happen. With a project file holding "disableAllHooks": true, a WebFetch that the Step 4 hook otherwise blocks came back with The page title is **Example Domain**. The real lock is managed settings with allowUnsandboxedCommands set to false, which makes the sandbox admin-required for every developer. For one session, or an unattended claude -p run, pass the same through --settings. That also makes the sandbox admin-required, and its keys outrank any project file, so it keeps your hooks on:
claude --settings '{"sandbox":{"allowUnsandboxedCommands":false},"disableAllHooks":false}'Our test used a settings file holding the full sandbox block, both hooks and "disableAllHooks": false, not this two-key form on top of user settings. That file made Claude Code ignore the repository's "excludedCommands": ["curl *"] and brought the hooks back.
The flag doesn't touch what the repository runs itself. Its own settings-file hooks and its .mcp.json servers run outside the sandbox with your full access, and claude -p never shows the trust dialog, so they run and connect without asking. We gave a test repository a PreToolUse hook and a .mcp.json server that each curl example.com. Under the --settings lock both reached it and recorded 200. For an unattended run in a repository you didn't write, also pass --setting-sources user, so Claude Code reads neither the project's settings files nor its .mcp.json:
claude -p "<prompt>" --setting-sources user \
--settings '{"sandbox":{"allowUnsandboxedCommands":false},"disableAllHooks":false}'With --setting-sources user added, the same repository's hook and server never ran, and your user settings, sandbox and hooks still load.
The docs only promise that this flag skips the project's settings files and .mcp.json. Project skills in .claude/skills can carry their own hooks, and those run with your full access. On 2.1.322 the flag also kept our test repository's skill from loading (Unknown skill: repo-probe), while the same skill's hook reached example.com once project settings were read. That is observed behaviour, not a documented guarantee, so check .claude/skills before an unattended run too. Without managed settings or these flags, read a repository's settings before you open it in Claude Code:
jq '{disableAllHooks, sandbox, hooks}' .claude/settings.json .claude/settings.local.json
ls -l .mcp.json
ls .claude/skillsTreat any disableAllHooks, excludedCommands, "enabled": false or hooks entry, an .mcp.json file or a .claude/skills folder as a reason to start that session with --setting-sources user and the lock above, and read each skill's frontmatter for a hooks: block. jq and ls report a file they can't open or find, which only means the repository doesn't have it.
Step 2. Prove the sandbox refuses an unlisted host
Ask Claude to run the probes, because a command you type at the ! prompt usually runs outside the sandbox and proves nothing. In a new session, give Claude this prompt:
Run these three Bash commands one at a time, exactly as written, and do not retry any of them another way:
curl -sS -o /dev/null -w '%{http_code}\n' https://api.github.com/zen
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/
curl --noproxy '*' -sS https://example.com/You should see the listed host answer, the unlisted host refused by the proxy, and the proxy bypass fail to resolve. Ours, on 2.1.322:
200
Exit code 56
curl: (56) CONNECT tunnel failed, response 403
000
<sandbox_violations>
deny network-outbound example.com:443 (host is not on the allow list)
</sandbox_violations>
Exit code 6
curl: (6) Could not resolve host: example.comThe signal that matters is host is not on the allow list with no approval prompt, and Could not resolve host for the third command: with the proxy skipped, the command has no route out at all. If example.com returns 200, the sandbox isn't on for this session. Run /sandbox and read its Config tab, check the keys landed in ~/.claude/settings.json, and check the project's files for "enabled": false as Step 1 describes. If Claude offers to retry outside the sandbox, allowUnsandboxedCommands isn't false in a file the session loaded; decline the retry.
Step 3. Add the hosts your toolchain needs
Run your normal install, build and test commands through Claude and collect every refusal. Under a strict allowlist there is no approval prompt to click through, so the denied host shows up in the command's result as a <sandbox_violations> line like this one. Add each host you trust to allowedDomains in ~/.claude/settings.json and start a new session.
deny network-outbound example.com:443 (host is not on the allow list)For example, to let a sandboxed command reach one API host on port 443 only and a local dev server on port 8080, extend the list like this:
"allowedDomains": [
"github.com",
"*.github.com",
- "registry.npmjs.org"
+ "registry.npmjs.org",
+ "api.example.com:443",
+ "127.0.0.1:8080"
],You should reach a state where your usual commands finish with no violation lines. Three failures look like allowlist problems and aren't:
- Plain
sshand most database drivers ignore the proxy variables, so they can't connect even to an allowed host. UDP, HTTP/3 over QUIC andpingcan't leave the sandbox at all. - A hostname that resolves only to a local address, including the
169.254.169.254cloud metadata endpoint, is refused. The fix for a local dev server: add the address itself, such as"127.0.0.1:8080". Issue #28018 asks for localhost access and has 81 reactions. - Go-based CLIs such as
ghand terraform can fail TLS verification inside the macOS sandbox. The docs' fix is anexcludedCommandsentry such asgh *, which runs that tool with your full access, its stored credentials and no proxy. That is the hole the next paragraph warns about, so add it per tool and only for tools you trust with an open network.
Keep the list narrow. The docs warn that broad domains such as github.com can become exfiltration paths, because the proxy decides from the client-supplied hostname and doesn't inspect TLS, so code inside the sandbox can try domain fronting. Prefer the exact API host over a wildcard, and use deniedDomains to carve a host out of a wildcard you must keep. Resist excludedCommands: an excluded command runs with no filesystem restrictions and no network proxy.
Step 4. Add a hook that holds WebFetch and WebSockets to the same allowlist
WebFetch runs inside Claude Code, outside the sandbox, so allowedDomains never applies to it. The tools reference says WebFetch never reads the sandbox allowlist. In auto and bypassPermissions modes it fetches with no prompt at all. We confirmed it: with Step 1's settings in place but no hooks, plus a WebFetch allow rule standing in for those modes, a fetch of https://example.com/ came back with The page title is "Example Domain.". No use for WebFetch? Set CLAUDE_CODE_DISABLE_WEB_FETCH=1 (2.1.285+) instead.
Monitor has the same gap through its WebSocket watches, which take a ws input with a ws:// or wss:// URL. For those, the docs list checks against private addresses and deniedDomains, and the managed-only allowlist, but not your user allowlist. In our strict-mode session with no hooks and a Monitor allow rule, a watch on wss://stream.binance.com:9443/ws/btcusdt@trade, a host on no list, started and delivered live trade messages.
This hook reads the URL from a WebFetch call or a Monitor WebSocket watch and applies your sandbox's two domain lists to it. It lets Monitor commands through, since the sandbox already covers them. Save it as ~/.claude/hooks/webfetch-host-allowlist.sh:
#!/usr/bin/env bash
settings="${1:-$HOME/.claude/settings.json}"
input=$(cat)
if [ "$(jq -r '.tool_name' <<< "$input")" = "Monitor" ] && [ "$(jq -r '.tool_input.ws.url // empty' <<< "$input")" = "" ]; then
exit 0
fi
url=$(jq -r '.tool_input.url // .tool_input.ws.url // empty' <<< "$input")
host=$(printf '%s' "$url" | sed -E 's#^[A-Za-z]+://##; s#[/?\#].*$##; s#^.*@##; s#:[0-9]+$##' | tr '[:upper:]' '[:lower:]')
if [ -z "$host" ]; then
echo "Blocked: no host found in the URL." >&2
exit 2
fi
matches() {
local entry
while IFS= read -r entry; do
entry=$(printf '%s' "$entry" | tr '[:upper:]' '[:lower:]' | sed -E 's#:[0-9]+$##')
case "$entry" in
"") ;;
"*") return 0 ;;
"*."*) [[ "$host" == *".${entry#\*.}" ]] && return 0 ;;
*) [[ "$host" == "$entry" ]] && return 0 ;;
esac
done < <(jq -r ".sandbox.network.$1[]?" "$settings")
return 1
}
if matches deniedDomains; then
echo "Blocked: $host is in sandbox.network.deniedDomains ($settings)." >&2
exit 2
fi
if matches allowedDomains; then
exit 0
fi
echo "Blocked: $host is not in sandbox.network.allowedDomains ($settings)." >&2
exit 2It checks deniedDomains first, then allowedDomains. An entry is an exact host or a leading *. for subdomains, as in the sandbox lists; a bare * allowing everything is this hook's own rule. Unlike the sandbox, the hook ignores ports, so api.example.com:443 lets WebFetch reach that host on any port. WebFetch doesn't follow a redirect to another host: it hands the target back to Claude, which fetches it with a second WebFetch call, so the hook checks the new host too. Exit code 2 is what blocks a PreToolUse call; exit 1 is only a non-blocking error. Make it executable and feed it three test calls:
chmod +x ~/.claude/hooks/webfetch-host-allowlist.sh
echo '{"tool_name":"WebFetch","tool_input":{"url":"https://api.github.com/repos/x","prompt":"x"}}' \
| ~/.claude/hooks/webfetch-host-allowlist.sh; echo "exit=$?"
echo '{"tool_name":"WebFetch","tool_input":{"url":"https://evilgithub.com/","prompt":"x"}}' \
| ~/.claude/hooks/webfetch-host-allowlist.sh; echo "exit=$?"
echo '{"tool_name":"Monitor","tool_input":{"ws":{"url":"wss://ws.postman-echo.com/raw"},"description":"x"}}' \
| ~/.claude/hooks/webfetch-host-allowlist.sh; echo "exit=$?"You should see:
exit=0
Blocked: evilgithub.com is not in sandbox.network.allowedDomains (/Users/you/.claude/settings.json).
exit=2
Blocked: ws.postman-echo.com is not in sandbox.network.allowedDomains (/Users/you/.claude/settings.json).
exit=2evilgithub.com is the case worth testing: a suffix check without the leading dot would let it through. If api.github.com also exits 2, the hook read a file with no allowedDomains, or no file at all, so it refuses everything; pass the right settings path as the first argument. If evilgithub.com exits 0, your list contains a bare *.
Step 5. Add a content guard for writes, edits and MCP inputs
This hook refuses any tool call whose input contains a value you never want sent: your email, your name, an internal hostname. It matters most for Write and Edit. In the leak post, a script written to disk and run with python3 carried the address past the built-in check. If Claude writes the address into a script, this guard blocks the Write before the file exists; a script that reads the address at run time is the sandbox's job. The guard also sees MCP tool inputs, which the sandbox never touches. Save it as ~/.claude/hooks/egress-content-guard.sh:
#!/usr/bin/env bash
denylist="${1:-$HOME/.claude/hooks/egress-denylist.txt}"
input=$(jq -c '.tool_input' | tr '[:upper:]' '[:lower:]')
needles=$(
git config --global user.email 2>/dev/null
[ -f "$denylist" ] && cat "$denylist"
)
while IFS= read -r needle; do
needle=$(printf '%s' "$needle" | tr '[:upper:]' '[:lower:]')
[ -z "$needle" ] && continue
encoded=${needle//@/%40}
if [[ "$input" == *"$needle"* || "$input" == *"$encoded"* ]]; then
echo "Blocked: this call contains a value from the egress denylist. Leave it out or ask the user first." >&2
exit 2
fi
done <<< "$needles"
exit 0Your global git email is added for you. Matching ignores case and also catches a value percent-encoded once (%40). Write the denylist with one value per line, then test it:
printf '%s\n' '[email protected]' 'Jo Developer' > ~/.claude/hooks/egress-denylist.txt
chmod +x ~/.claude/hooks/egress-content-guard.sh
echo '{"tool_name":"Write","tool_input":{"file_path":"/tmp/p.py","content":"UA = \"[email protected]\""}}' \
| ~/.claude/hooks/egress-content-guard.sh; echo "exit=$?"
echo '{"tool_name":"Bash","tool_input":{"command":"npm test"}}' \
| ~/.claude/hooks/egress-content-guard.sh; echo "exit=$?"You should see:
Blocked: this call contains a value from the egress denylist. Leave it out or ask the user first.
exit=2
exit=0If npm test or an ordinary edit exits 2, a denylist line is too short or too common, such as a first name on its own. Expect the guard to stop legitimate edits that contain your git email too, like a package.json author field or a CHANGELOG entry; make those edits yourself or remove the line while you do.
This is a string match, and it misses anything the tool input doesn't spell out literally. Our tests of a command containing $(git config user.email), an address assembled from a shell variable and one decoded from base64 all exited 0. The sandbox from Step 1 sits under it for those cases, and it checks the destination instead of the command text.
Step 6. Register both hooks so they fail closed
Register the hooks in user settings with exec form and "onFailure": "block". Without that field, a hook with a typo in its path, a crash or a timeout is a non-blocking error, and the tool call goes ahead. Save this as ~/.claude/hooks.json, with /Users/you replaced by your home directory:
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(Bash|PowerShell|Monitor|Write|Edit|NotebookEdit|WebFetch|WebSearch)$|^mcp__.*",
"hooks": [
{
"type": "command",
"command": "/Users/you/.claude/hooks/egress-content-guard.sh",
"args": [],
"onFailure": "block"
}
]
},
{
"matcher": "^(WebFetch|Monitor)$",
"hooks": [
{
"type": "command",
"command": "/Users/you/.claude/hooks/webfetch-host-allowlist.sh",
"args": [],
"onFailure": "block"
}
]
}
]
}
}Both matchers contain regex characters, so Claude Code treats them as unanchored JavaScript regular expressions. The ^ and $ keep them to exact tool names, PowerShell covers Windows hosts where shell commands route through that tool, and ^mcp__.* catches every MCP tool, since they are named mcp__<server>__<tool>. A matcher that names Monitor is required for WebSocket watches: a hook with a plain Bash matcher doesn't run for them. Hook entries merge across settings levels, but inside one file the PreToolUse array is yours to keep, so append to it rather than replacing it:
cd ~/.claude
jq -s '.[0] as $old | .[1] as $new | ($old * $new)
| .hooks.PreToolUse = (($old.hooks.PreToolUse // []) + $new.hooks.PreToolUse)' \
settings.json hooks.json > settings.json.new && mv settings.json.new settings.json
jq '[.hooks.PreToolUse[].matcher]' settings.jsonYou should see your existing matchers followed by the two new ones. The merge appends every time, so run it once; a second run registers both hooks twice. Start a new session and ask Claude to write a file containing a denylisted value, to fetch https://example.com/, and to open a WebSocket watch on a host outside your list. All three calls should be refused before they run:
PreToolUse:Write hook error: [/Users/you/.claude/hooks/egress-content-guard.sh]: Blocked: this call contains a value from the egress denylist. Leave it out or ask the user first.
PreToolUse:WebFetch hook error: [/Users/you/.claude/hooks/webfetch-host-allowlist.sh]: Blocked: example.com is not in sandbox.network.allowedDomains (/Users/you/.claude/settings.json).
PreToolUse:Monitor hook error: [/Users/you/.claude/hooks/webfetch-host-allowlist.sh]: Blocked: stream.binance.com is not in sandbox.network.allowedDomains (/Users/you/.claude/settings.json).In our run the file was never created and the watch delivered nothing. To check the fail-closed path, point one hook at a script that doesn't exist and ask for any Bash command. Every call is refused until you fix the path:
PreToolUse:Bash hook error: [/Users/you/.claude/hooks/missing-guard.sh]: failed; blocking because onFailure is "block"
Error occurred while executing hook command: ENOENT: no such file or directory, posix_spawn '/Users/you/.claude/hooks/missing-guard.sh'If instead you see Failed with non-blocking status code: and the tool ran, onFailure didn't apply: check the Claude Code version is 2.1.295 or later and the field sits on the hook handler, next to "type", not on the matcher group.
Verify
The check that matters most is a script, because that's the path a prompt or a string match can't see. Ask Claude to write this as probe.py and run python3 probe.py:
import socket, urllib.request
try:
urllib.request.urlopen("https://example.com/", timeout=5)
print("urllib: connected")
except Exception as e:
print("urllib:", type(e).__name__, e)
try:
socket.create_connection(("1.1.1.1", 443), timeout=5)
print("raw socket: connected")
except Exception as e:
print("raw socket:", type(e).__name__, e)You should see both connections refused. The first respects the proxy and is told why; the second ignores it and gets no route at all:
urllib: URLError <urlopen error Tunnel connection failed: 403 host is not on the allow list>
raw socket: PermissionError [Errno 1] Operation not permittedThat second line settles a question the leak post left open: a child process that opens its own socket stays inside the boundary. With every step in place, our probes on 2.1.322 ended like this:
| Probe | What stopped it | Result |
|---|---|---|
curl to api.github.com | Nothing, host is listed | 200 |
curl to example.com | Sandbox proxy, strict allowlist | CONNECT tunnel failed, response 403 |
curl --noproxy '*' | Sandbox, no direct route | Could not resolve host |
| Python urllib and raw socket | Sandbox, child process covered | 403 host is not on the allow list / Operation not permitted |
| Write a file containing the email | Content guard hook | Blocked, file not created |
| WebFetch example.com | Host hook | Blocked |
| Monitor WebSocket to an unlisted host | Host hook | Blocked, no messages |
| WebFetch github.com/anthropics/claude-code | Nothing, host is listed | Page fetched |
| Hook script missing | onFailure: "block" | Every Bash call blocked |
Repository sets "disableAllHooks": true | "disableAllHooks": false in --settings | Hooks off without it, back on with it |
Repository hook and .mcp.json server curl example.com | --setting-sources user | 200 under the --settings lock alone; never ran with the flag |
| Project skill with a hook that curls example.com | --setting-sources user (observed, not documented) | 200 with project settings read; Unknown skill with the flag |
We ran these probes with claude -p, the JSON passed through --settings and --setting-sources project from a directory of our own, so our personal settings stayed out of the result. --settings with allowUnsandboxedCommands set to false makes the sandbox admin-required, which also makes Claude Code ignore a repository's loosening settings. A setup in ~/.claude/settings.json alone doesn't get that protection, as Step 1 explains. We did not run the user-settings path itself, the Linux or WSL2 path, the interactive /sandbox panel, PowerShell or a live MCP server; those follow the docs.
Rollback
Nothing here is irreversible. The guide edits one settings file, adds two staging files next to it and three files under ~/.claude/hooks. To back out, restore the copy from Prerequisites (if you had no settings.json before Step 1, delete it instead), delete those files and start a new session:
cp ~/.claude/settings.json.bak ~/.claude/settings.json
rm ~/.claude/hooks/egress-content-guard.sh ~/.claude/hooks/webfetch-host-allowlist.sh ~/.claude/hooks/egress-denylist.txt
rm ~/.claude/sandbox.json ~/.claude/hooks.json
jq '.permissions.allow // [] | map(select(startswith("WebFetch(domain:")))' .claude/settings.local.jsonThe last command, run from a project root, lists WebFetch(domain:...) allow rules in that repository's local settings. Answering "Yes, and don't ask again" to a network or WebFetch prompt saves one there, and WebFetch keeps honoring it after the hooks are gone. Restoring your user settings doesn't remove them, so review each repository you worked in. In a repository with no local settings file, jq reports that the file can't be opened, which means there is nothing to review.
Rollback can't recall anything that left before the controls were in place. It also doesn't touch what the policy never covered: local MCP servers, hooks, plugin monitors, LSP servers, the status line command and apiKeyHelper run with your full access whether the sandbox is on or off. There is no data-loss risk in either direction.
What's next
For a team, move the same keys into managed settings and add allowManagedDomainsOnly so developers can't widen the list. Managed allowUnsandboxedCommands set to false also stops repositories from loosening the sandbox, which matters most for unattended pipelines like our four-agent Claude Code team. Subagents already share the parent's sandbox configuration and fire the same settings hooks. The gap left is the processes the sandbox never wraps, starting with local Model Context Protocol servers. To put those behind a boundary, run the whole Claude Code process inside the sandbox runtime or the dev container with its firewall script, as the sandboxing docs describe.
