Sandbox blocking npm install in Claude Code, Cursor and Codex
In short: when an agent's npm install, pip install or curl fails but the same command works in your own terminal, the sandbox is usually the reason. Each tool's sandbox lets commands reach only the hosts you allow and write only inside the project. Allow the registries you use by name, allow the package cache folder, and leave everything else closed. Turning the sandbox off isn't the fix.
Is it the sandbox? Read the error
Agents often misreport these failures as a bad token, a broken registry or a flaky network. The error text usually tells you which limit you hit:
| What you see | What it usually means |
|---|---|
Could not resolve host, Temporary failure in name resolution, Network is unreachable | The command has no network route at all: Codex with network off, or a tool that ignores the proxy variables in Claude Code's sandbox |
403 Forbidden, Tunnel connection failed: 403, or a prompt asking about a host | The sandbox proxy is working and this host isn't on the allowlist |
EROFS, Read-only file system, Operation not permitted | The command wrote outside the folders the sandbox allows, often a cache in your home folder |
Works for curl, fails for ssh, psql or a database driver | The tool doesn't use the HTTP proxy, so an allowed host still can't be reached |
To check rather than guess, ask the agent to run the probe script below. It prints what the sandbox lets that command reach and write.
The three sandboxes side by side
| Claude Code | Cursor | Codex | |
|---|---|---|---|
| On by default? | No. Turn it on with /sandbox or sandbox.enabled | Yes in Auto-review (the default mode); optional in Allowlist; none in Run Everything | Yes. The default workspace-write sandbox has no network |
| Config file | .claude/settings.json or ~/.claude/settings.json, key sandbox | .cursor/sandbox.json or ~/.cursor/sandbox.json | ~/.codex/config.toml, or .codex/config.toml in a trusted project |
| Hosts allowed out of the box | None. You get asked (in Manual mode) the first time | Cursor's default list: npm, PyPI, crates.io, GitHub, Docker Hub and many more | None until you set network_access, and then every host unless the network proxy is on |
| Allow a host | network.allowedDomains | networkPolicy.allow | [features.network_proxy] domains |
| Allow a folder outside the project | filesystem.allowWrite | additionalReadwritePaths | writable_roots |
| What it covers | Bash, PowerShell and Monitor commands only | Terminal commands | Commands Codex runs; not web search or MCP |
Claude Code
1. Know what's blocked
The sandbox is off until you run /sandbox or set sandbox.enabled. Once it's on, a sandboxed command can write only to the working directory, a per-user temp directory and folders you've added. It has no direct network route. Traffic goes through a proxy on your machine that checks each host against your allowed domains, and that list starts empty.
What happens with a host that isn't on the list depends on your permission mode: Manual and acceptEdits prompt you, auto mode refuses unless the command named the host and the classifier approved it (v2.1.271 or later), dontAsk refuses, and bypassPermissions lets it through. Answering Yes allows the host for the session. Yes, and don't ask again saves a WebFetch(domain:...) allow rule to your local settings, and the sandbox picks those rules up as allowed domains.
2. Allow the hosts and the cache folder
In .claude/settings.json (commit it) or ~/.claude/settings.json (just you):
{
"sandbox": {
"enabled": true,
"network": {
"allowedDomains": ["registry.npmjs.org", "pypi.org", "files.pythonhosted.org"]
},
"filesystem": {
"allowWrite": ["~/.npm"]
}
}
}
- Use exact hosts where you can. The docs' examples use a leading
*.wildcard such as*.npmjs.org, and they warn that a broad entry likegithub.comcan be used to send data out. allowWriteis for tools that write outside the project. The docs name npm among them: its cache lives in~/.npm. Path rules use~/and absolute paths here, not the//prefix that permission rules use.- Filesystem path lists from different settings files are merged, not replaced.
- To refuse every other host instead of prompting, set
network.strictAllowlisttotruein your user settings (v2.1.219 or later). A repo's settings file can't turn it on.
3. The commands that still fail
- Tools that ignore the proxy (plain
ssh, most database drivers, anything over UDP) can't connect, even to an allowed host. Take that exact command out of the sandbox withsandbox.excludedCommands, and add an ask rule for it so you approve each run. - Docker doesn't work inside the sandbox. The docs suggest a narrow exclusion such as
docker compose *. An excluded command runs with your full access. - A server on localhost: on Linux and WSL2, each sandboxed command has its own private localhost, so it can't reach your dev server or a database in a container. Use
excludedCommandsfor the command that needs it. On macOS, setnetwork.allowLocalBindingtotrue. - git over SSH fails on macOS even with the host allowed. Switch the remote to HTTPS, or exclude
git fetch *,git pull *andgit push *. - jest hangs because watchman doesn't work in the sandbox. Run
jest --no-watchman.
When a command fails in the sandbox, Claude may offer to retry it unsandboxed. Your mode decides whether that retry prompts. If you'd rather it never happens, set allowUnsandboxedCommands to false. Also note that if the sandbox can't start (missing bubblewrap or socat on Linux), Claude Code runs commands unsandboxed unless you set failIfUnavailable.
Cursor
1. Know when the sandbox is in play
In Auto-review, the default Run Mode, allowlisted calls run as they are and other shell commands run in the sandbox when they can. If a sandboxed command fails on a sandbox limit, the agent can rerun it outside the sandbox, and the Auto-review classifier checks that rerun. In Allowlist mode the sandbox is optional. Run Everything has no sandbox at all. Cloud Agents don't use Run Modes; they run on their own machine.
Cursor's sandbox starts more open than the other two. In the default network mode, sandbox.json + Defaults, a long built-in list is already allowed, including registry.npmjs.org, pypi.org, files.pythonhosted.org, github.com, crates.io and Docker Hub. So when npm install fails in Cursor, look for a host that isn't on that list: a private registry, a Git dependency on another host, or an API your tests call.
2. Add the hosts it's missing
In .cursor/sandbox.json (one repo) or ~/.cursor/sandbox.json (all projects):
{
"networkPolicy": {
"default": "deny",
"allow": ["npm.pkg.github.com", "api.stripe.com"]
},
"enableSharedBuildCache": true
}
- Entries can be exact hosts, wildcards or CIDR ranges. In Cursor,
*.example.comalso matchesexample.comitself. URL paths are ignored, and deny always beats allow. - The two files are merged: allow lists are combined, and a
"deny"default wins over"allow". A team admin allowlist replaces the combined list. - The network mode setting has two other options. sandbox.json Only drops Cursor's defaults, so you have to list your registries yourself. Allow All opens every host inside the sandbox.
enableSharedBuildCachesends npm, pip, cargo and similar caches to a shared temp folder, so sandboxed and unsandboxed runs use the same caches.additionalReadwritePathsgrants other folders outside the project.
3. The commands that still fail
- Local and private addresses (
127.x,10.x,172.16.x,192.168.xand the cloud metadata address) are blocked by default. The reference lists CIDR ranges as allow patterns, but it doesn't say whether one re-opens a blocked private range, and we haven't tested it. Expect a command that talks to your local dev server or database to need a run outside the sandbox. - Docker on Linux: inside the sandbox,
id -ureturns 0. Scripts that pass your user ID todocker run --usershould readCURSOR_ORIG_UIDandCURSOR_ORIG_GIDinstead. - Linux needs kernel 6.2 or later with Landlock v3 and unprivileged user namespaces. Without them, Cursor asks before running commands instead of sandboxing them.
- Some paths stay read-only whatever you write in
sandbox.json, including.cursor/*.json,.claude/*.json,.vscode/,.git/hooks/and.git/config.
Codex
1. Know what the default blocks
Codex's default workspace-write sandbox lets commands write in the workspace and temp folders, with no network. In our test, every host failed with Temporary failure in name resolution, which is easy to read as a DNS problem. Your home folder is read-only, and so are .git, .agents, .codex and .aws inside the workspace. A git commit inside the sandbox failed on .git/index.lock, so with the default on-request approvals Codex has to ask to run it outside the sandbox, or you commit yourself.
2. Open the network to your registries only
# ~/.codex/config.toml, or .codex/config.toml in a trusted project
sandbox_mode = "workspace-write"
approval_policy = "on-request"
[sandbox_workspace_write]
network_access = true # off by default
writable_roots = ["~/.npm"] # npm's cache lives outside the workspace
[features.network_proxy]
enabled = true # without the proxy, network_access means every host
domains = { "registry.npmjs.org" = "allow", "pypi.org" = "allow", "files.pythonhosted.org" = "allow" }
Two settings, and both matter:
network_access = truealone opens every host. In our test,example.comwas reachable with only that line. The docs say the same thing: network on with the proxy off means "unrestricted direct outbound access".[features.network_proxy]routes commands through a local proxy that enforcesdomains. Domain rules do nothing until the proxy is on, and the proxy does nothing while the network is off. With both on,registry.npmjs.orgworked andexample.comgot403 Forbiddenwith the headerx-proxy-error: blocked-by-allowlist.- Codex wildcards differ from Cursor's:
*.example.commatches subdomains only, and**.example.commatches the domain and its subdomains."*"allows every public host. - The proxy also refuses hostnames that resolve to non-public addresses, even when they're on your list. In our test, our machine's DNS answered a reserved address for
pypi.org, and the proxy blocked it. If an internal registry resolves to a private IP, that's why.
For one session, pass the same settings as flags:
codex -c 'sandbox_workspace_write.network_access=true' \
-c 'features.network_proxy.enabled=true' \
-c 'features.network_proxy.domains={ "registry.npmjs.org" = "allow" }'
The proxy only covers commands Codex runs. Web search, MCP servers and connectors have their own settings.
3. Let installs write their cache
With the network open, npm install still failed in our test:
npm error code EROFS
npm error path /home/box/.npm/_cacache/tmp/***
npm keeps its cache in ~/.npm, outside the workspace. Adding it to writable_roots, as in the file above, fixed it. The docs' example uses an absolute path; "~/.npm" also worked in codex-cli 0.166.0. Do the same for pip (~/.cache/pip) or cargo if you use them.
4. Reach localhost on purpose
With the proxy on, local and private addresses are blocked. Our test server on 127.0.0.1:8765 returned 403 until we added both literals:
domains = { "registry.npmjs.org" = "allow", "localhost" = "allow", "127.0.0.1" = "allow" }
Wildcards don't count as local exceptions. allow_local_binding = true opens local and private addresses more broadly, so use it only if a hostname you need resolves to one.
Codex also has newer permission profiles (marked beta) that describe the same filesystem and network rules as one named profile. Don't mix them with sandbox_mode: if sandbox_mode appears in any loaded config, Codex uses the older settings and ignores the profile.
Wildcards don't mean the same thing
| Pattern | Claude Code | Cursor | Codex |
|---|---|---|---|
example.com | That host | That host | That host |
*.example.com | Leading *. is supported; the docs don't say whether it includes example.com | Subdomains and example.com | Subdomains only |
**.example.com | Not documented | Not documented | Subdomains and example.com |
| Port | "127.0.0.1:8080"; no port means every port | Not documented | Ports are stripped |
If you keep one list of hosts for all three tools, write exact hosts. They mean the same thing everywhere.
A probe to run inside the sandbox
Ask the agent to run this with the hosts your project needs, for example python3 sandbox_probe.py registry.npmjs.org npm.pkg.github.com. It has to be the agent that runs it. A command you type in your own terminal isn't sandboxed.
#!/usr/bin/env python3
"""Ask the agent to run this inside its sandbox to see what the sandbox blocks.
Usage: python3 sandbox_probe.py [host ...] (defaults: registry.npmjs.org pypi.org github.com)
Exit code 1 if a host or an existing cache folder is blocked. Standard library only."""
import os, sys, pathlib, tempfile, urllib.request, urllib.error
HOSTS = sys.argv[1:] or ["registry.npmjs.org", "pypi.org", "github.com"]
FOLDERS = [".", ".git", "~", "~/.npm", "~/.cache/pip", "~/.cargo/registry", "~/go/pkg/mod", "~/.gradle"]
def can_write(folder):
path = pathlib.Path(folder).expanduser()
if not path.is_dir():
return None
try:
with tempfile.NamedTemporaryFile(dir=path, prefix=".sandbox-probe-"):
return True
except OSError:
return False
def reach(host):
req = urllib.request.Request(f"https://{host}/", method="HEAD")
try:
urllib.request.urlopen(req, timeout=8)
return True, "reachable"
except urllib.error.HTTPError as e: # the server answered, so the network path works
return True, f"reachable (HTTP {e.code})"
except urllib.error.URLError as e:
reason = str(e.reason)
if "Tunnel connection failed" in reason:
return False, f"BLOCKED by the sandbox proxy ({reason})"
return False, f"NO ROUTE ({reason})"
except OSError as e: # timeouts and resets
return False, f"NO ROUTE ({e})"
proxies = sorted(k for k in os.environ if k.lower() in ("http_proxy", "https_proxy", "all_proxy"))
print("Sandbox markers:")
print(" CURSOR_SANDBOX =", os.environ.get("CURSOR_SANDBOX", "(not set)"))
print(" proxy variables:", ", ".join(proxies) if proxies else "none (direct network, or no network)")
failed = False
print("Write access:")
for folder in FOLDERS:
ok = can_write(folder)
if ok is None:
continue
print(f" {'ok ' if ok else 'BLOCKED'} {folder}")
failed |= (not ok) and folder.startswith("~/.") # a read-only package cache breaks installs
print("Network (HTTPS):")
for host in HOSTS:
ok, why = reach(host)
print(f" {host}: {why}")
failed |= not ok
sys.exit(1 if failed else 0)
"BLOCKED by the sandbox proxy" means add the host to the allowlist. "NO ROUTE" means there's no network at all, or the host doesn't resolve. A blocked ~/.npm or ~/.cache/pip means installs will fail even with the network open. A blocked .git is expected in Codex. In Claude Code's Manual mode, a host that isn't allowed may bring up a prompt while the probe waits.
We ran it in Codex's sandbox (codex-cli 0.166.0 on Linux) with the network off, with only network_access, and with the config above, and outside any sandbox. The results matched what's described in the Codex section. We didn't run it inside Claude Code or Cursor, which weren't installed on the test machine; it only reads standard proxy variables and CURSOR_SANDBOX, which Cursor sets in sandboxed commands.
When to stop allowlisting
If the list of exceptions keeps growing (Docker, databases, SSH, local servers), run the agent inside a dev container and make the container the boundary. Claude Code's docs describe a reference container with an outbound firewall, running as a non-root user, and the CLI refuses --dangerously-skip-permissions when it runs as root. Codex's docs suggest --sandbox danger-full-access inside a container when the container is the boundary, so Codex doesn't try to build a second sandbox. A container still lets a command send out anything inside it, so keep host secrets such as ~/.ssh out of it.
For fewer prompts without widening the sandbox, see how to stop Claude Code, Cursor and Codex asking permission for every command. To keep secrets out of reach, see how to stop agents reading your .env file.
A ten-minute setup
- List the hosts your installs and tests need: the package registries, any private registry, Git hosts for Git dependencies, and APIs your tests call.
- Claude Code:
/sandbox, then add those hosts toallowedDomainsand the cache folder toallowWrite. - Cursor: leave the network mode on sandbox.json + Defaults and add only the hosts that aren't on Cursor's default list.
- Codex: turn on
network_accessand[features.network_proxy]with adomainstable, and add the cache folder towritable_roots. - Have the agent run the probe. Fix what it reports, commit the project files, and keep personal hosts in your home-folder config.
FAQ
Why does npm install fail when my AI agent runs it?
Usually the agent's sandbox, not npm. Codex's default workspace-write sandbox has no network, Claude Code's sandbox only reaches hosts on its allowlist (empty at first), and Cursor's sandbox, in its default network mode, only reaches its built-in list plus your sandbox.json. Even with the network open, npm writes its cache to ~/.npm, which is outside the workspace, so the install can still fail with EROFS until you allow that folder.
How do I allow a domain in the Claude Code sandbox?
Add it to sandbox.network.allowedDomains in .claude/settings.json or ~/.claude/settings.json, or answer "Yes, and don't ask again" when Claude Code asks, which saves a WebFetch(domain:...) allow rule to your local settings. Sandboxed commands also get every domain from WebFetch(domain:...) allow rules.
Where does Cursor's sandbox.json go?
In ~/.cursor/sandbox.json for every project, or .cursor/sandbox.json in the repo for one project. Both files are merged and the project file wins. Network allow lists from both are combined, and "deny" as the default wins over "allow".
How do I give Codex internet access for package installs?
Set network_access = true under [sandbox_workspace_write] in config.toml. On its own that opens every host, so also turn on [features.network_proxy] with a domains table that allows only your registries. Add writable_roots for the package manager's cache folder if installs fail with a read-only error.
Why can't the agent reach my dev server on localhost?
All three sandboxes treat local addresses as special. Codex blocks them until you allow the exact literal ("localhost" or "127.0.0.1"). Cursor blocks 127.x and private ranges by default. Claude Code on Linux gives each sandboxed command its own localhost, so it can't see servers on your machine; on macOS, set network.allowLocalBinding to true.
Sources
- Claude Code docs: Configure the sandboxed Bash tool (network, filesystem, troubleshooting)
- Claude Code docs: Development containers
- Cursor docs: sandbox.json reference
- Cursor docs: Run Modes (sandboxing, network access modes, default domains)
- Codex docs (ChatGPT Learn): Agent approvals & security (network access, network proxy)
- Codex docs (ChatGPT Learn): Permissions (permission profiles)
- Codex docs (ChatGPT Learn): Advanced configuration (writable_roots, one-off overrides)