Here's the failure the approval prompt is built for, and the one it can't handle. When you're watching the terminal and your agent's sandboxed command reaches a host it hasn't seen, it asks: allow this? You glance, you approve, you move on. Now run that same agent in CI, or on a schedule, or as a background job you kicked off before lunch. The prompt fires into a room with no one in it. The run hangs until it times out. The control that felt like safety was a control that only works when you don't need it.
Deny-by-default egress is the fix: decide the network boundary in advance, allow the short list of hosts a run legitimately needs, and refuse everything else without a prompt. Claude Code shipped exactly this as sandbox.network.strictAllowlist in 2.1.219 (changelog) — it "denies non-allowlisted hosts for sandboxed commands without prompting." Here's how to set it up whether or not your agent has that exact setting.
1. Enumerate what a run actually touches#
You can't allowlist hosts you haven't named. Run one representative task with the old prompting behavior on, and write down every host it asks about. For a coding agent, the honest list is short:
registry.npmjs.org # or pypi.org, crates.io, proxy.golang.org
github.com # your Git host
api.anthropic.com # the model/API endpoint your run calls
Most runs need nothing beyond a package registry, a Git host, and one API endpoint. The open internet is not on the list. If a task genuinely needs one more host, it'll show up here — add it, don't guess.
2. Turn on deny-by-default#
If your agent has a strict-allowlist setting, this is one line. In Claude Code, enable sandbox.network.strictAllowlist and provide the hosts from step 1. Non-allowlisted hosts are now refused for sandboxed commands with no prompt — which is the whole point for an unattended run.
If your agent doesn't expose that setting, enforce it one layer down with an egress firewall around the sandbox. The principle is identical — default DENY, then allow the named hosts:
# default deny outbound; allow DNS + loopback + the named hosts
iptables -P OUTPUT DROP
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
for host in registry.npmjs.org github.com api.anthropic.com; do
iptables -A OUTPUT -d "$host" -j ACCEPT
done
Either way you've replaced "ask a human who isn't there" with "refuse anything I didn't approve."
3. Verify it fails closed — both directions#
An allowlist you didn't test is an assumption. Run two probes: one to a host you allowed, one to a host you didn't.
curl -sSf https://registry.npmjs.org >/dev/null && echo "allow: OK"
curl -sSf https://example.com >/dev/null && echo "LEAK: reachable" || echo "deny: OK"
The first must succeed; the second must be refused. If the second prints LEAK, your policy isn't being enforced. If it hangs instead of failing fast, packets are being dropped silently — usable, but an explicit reject is better because runs then fail loudly instead of stalling. You want a refusal you can see in a log, not a timeout you have to guess at.
Why do this before you scale the agent up#
Deny-by-default egress is the boundary; everything you do to make the agent more capable happens inside it. The week Claude Code shipped strictAllowlist, it also raised the default subagent nesting depth to 3 — a deeper tree of subagents is a bigger surface, all of it reaching the same fixed network wall. Set the wall first. A container gives you filesystem and process isolation but not network containment by default; egress policy is the separate switch, and it's the one that decides whether an unattended run can phone home.
Named hosts in, everything else refused, both cases tested. That's a run you can leave alone.



