Originally published on KitQuarry. Written by Eliran Kotzero for KitQuarry.

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; if auto mode is missing or its classifier keeps failing, see Claude Code auto mode unavailable or classifier blocked), 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 like github.com can be used to send data out.
  • allowWrite is 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.strictAllowlist to true in 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 with sandbox.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 excludedCommands for the command that needs it. On macOS, set network.allowLocalBinding to true.
  • git over SSH fails on macOS even with the host allowed. Switch the remote to HTTPS, or exclude git fetch *, git pull * and git 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.com also matches example.com itself. 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.
  • enableSharedBuildCache sends npm, pip, cargo and similar caches to a shared temp folder, so sandboxed and unsandboxed runs use the same caches. additionalReadwritePaths grants other folders outside the project.

3. The commands that still fail

  • Local and private addresses (127.x, 10.x, 172.16.x, 192.168.x and 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 -u returns 0. Scripts that pass your user ID to docker run --user should read CURSOR_ORIG_UID and CURSOR_ORIG_GID instead.
  • 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 = true alone opens every host. In our test, example.com was 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 enforces domains. Domain rules do nothing until the proxy is on, and the proxy does nothing while the network is off. With both on, registry.npmjs.org worked and example.com got 403 Forbidden with the header x-proxy-error: blocked-by-allowlist.
  • Codex wildcards differ from Cursor's: *.example.com matches subdomains only, and **.example.com matches 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

  1. List the hosts your installs and tests need: the package registries, any private registry, Git hosts for Git dependencies, and APIs your tests call.
  2. Claude Code: /sandbox, then add those hosts to allowedDomains and the cache folder to allowWrite.
  3. Cursor: leave the network mode on sandbox.json + Defaults and add only the hosts that aren't on Cursor's default list.
  4. Codex: turn on network_access and [features.network_proxy] with a domains table, and add the cache folder to writable_roots.
  5. 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

Official docs, checked 2026-11-05.

If you want a starting point, KitQuarry's free sampler is on GitHub: ai-coding-rules-sampler. It has a short CLAUDE.md/AGENTS.md, security rules and three skills (plan-feature, review-changes, record-decisions) for Claude Code, Cursor, Codex and Windsurf. MIT, free. (I made it.)