Codex vs Claude

Default-Deny Egress: The Control You Turn Off in the First Hour

David Guzenburg/ / 11 min read

Most of what you are worried about needs a connection out. Cut it and a class of bad days becomes impossible rather than unlikely.

egress filteringsupply chainisolationleast privilege

Egress is the control that matters and the first one people turn off

Of everything you can restrict about an agent's environment, outbound network access has the best ratio of protection to cost. Most of the outcomes worth worrying about — a credential leaving the machine, a poisoned dependency phoning home, an instruction smuggled into a fetched document causing data to be posted somewhere — require a connection out. Cut that, and a large class of bad days becomes impossible rather than unlikely.

It is also the restriction that breaks the most, which is why the usual sequence is: enable the sandbox, discover that npm install fails, turn networking on, and never revisit it. The default is correct and the friction is real, so the interesting work is in keeping most of the benefit while removing most of the friction.

Default-deny is not the same as no network

The distinction that makes this tractable. Default-deny means nothing connects unless something says it may. It does not mean the process is air-gapped, and framing it that way makes the whole approach sound impractical.

In practice you end up with a small allowlist: your package registry, your version control host, maybe an internal service. Three or four entries covers almost every repository, and everything outside them fails closed. That is a dramatically better position than open egress, and it costs an afternoon.

What actually needs the network

Dependency installation, which is the big one. Version control operations against a remote. Tests that hit an external service, which are usually tests that should have been using a fixture. Tooling that checks for updates, phones home with telemetry, or fetches a schema. And documentation lookups, if the agent has a fetch tool.

Notice how much of that list is avoidable rather than essential. A substantial fraction of the network traffic from a development environment is tooling doing things you did not ask for, and a deny-by-default posture makes that visible for the first time.

Pre-seed dependencies outside the sandboxed session

The highest-value trick. Run the install before the agent's session starts, with network available, then run the session with egress denied against an already-populated dependency tree.

#!/usr/bin/env bash
# Resolve everything that needs the network first, then work offline.
set -euo pipefail

npm ci --prefer-offline                 # populate node_modules
uv sync --frozen                        # and the python side
go mod download                         # and modules

export NPM_CONFIG_OFFLINE=true
export PIP_NO_INDEX=1
export GOFLAGS=-mod=mod

exec agent-cli --sandbox workspace-write --no-network "$@"

This inverts the usual order and removes the main objection at a stroke. The agent never needs the registry because the dependencies are already there, and an attempt to install something new fails loudly — which is itself useful information, because unplanned dependency additions during an agent session are worth noticing.

The package registry is the exception that carries the risk

If you do allow one thing, it will be your registry, and it is worth being clear-eyed that this is also the highest-risk allowance you can make. A registry is the delivery mechanism for supply-chain compromise, and install scripts run with your permissions on a machine that has your credentials.

Two mitigations that do not cost much. Disable lifecycle scripts during installs where your ecosystem allows it, so fetching a package does not execute code. And install from a lockfile with integrity hashes rather than resolving fresh, so what arrives is what was reviewed. Neither is agent specific; both matter more when the install may be initiated by something that read an instruction in an issue comment.

DNS is where the allowlist gets awkward

Host-based rules are easy to state and hard to enforce at the packet level, because a hostname is resolved before a connection is made and addresses move. Allowing an IP range for a CDN-backed registry means allowing a large fraction of the internet that happens to share the CDN.

The practical answer is to filter at a proxy rather than at the firewall. An HTTP proxy sees the hostname, can allow or deny by name, and can log what was requested. Point the sandboxed environment at it, deny direct egress, and you have name-based control with a record, which no set of address rules will give you.

Exfiltration does not need much bandwidth

A useful corrective to the instinct that blocking large transfers is sufficient. An API key is forty characters. It fits in a URL query string, in a DNS lookup for a subdomain, in a header on a request that looks entirely ordinary.

So a policy that permits "just DNS" or "just requests to this one documentation site" is not a small allowance if either can carry arbitrary text outbound. The relevant question is not how much data can leave but whether any channel out exists that an attacker can put text into — which is a much stricter test, and the reason a proxy that logs full request targets is worth more than a bandwidth limit.

The pattern to deny by name

Fetching a script and piping it to a shell is the single most common way an agent turns a documentation page into code execution. Even inside an allowlist, deny that shape: it composes a network read and an execution in one step, with no artefact left behind to review.

The honest limitation

The agent's own model API is an outbound connection, and it carries your repository contents as context. So "no network" never means nothing leaves the machine; it means nothing leaves except through the channel the whole system is built on.

That is not a gotcha, and it is worth being precise about, because people sometimes adopt egress controls believing they have solved a data-residency question that they have not. Egress control defends against arbitrary destinations chosen at runtime. What goes to the model provider is a separate decision, made when you chose the provider, and it is covered in AI gateways and data retention.

Log what was allowed, not just what was blocked

Denials are self-announcing: something failed and somebody investigates. The permitted traffic is the part that tells you whether your policy still matches reality, and nobody looks at it because nothing broke.

A weekly glance at the proxy log answers questions you would not otherwise ask: which hosts are actually being contacted, whether a tool started calling something new after an update, whether an allowlist entry added for a one-off is still being used. That is ten minutes and it is how an allowlist stays small instead of accreting.

Verify the denial

Network policy fails open when misconfigured, silently. The only way to know it is in force is to attempt a connection that should fail and confirm that it does — the same argument as testing a sandbox profile, and the same two-second check.

# Each of these must fail. A success is a policy that is not in effect.
getent hosts example.com                       # DNS resolution
curl -sS --max-time 4 https://example.com      # direct HTTPS
curl -sS --max-time 4 http://1.1.1.1           # bypass by IP
nc -z -w2 1.1.1.1 443                          # raw TCP

The third and fourth lines matter most. A policy that filters by hostname and permits raw connections by address is a policy with a hole exactly the size of anyone who thinks to use one.

CI is the easier case, and the more important one

Continuous integration is where default-deny should be uncontroversial: the environment is declared, the dependencies are installed in a known step, and nothing interactive needs to reach out. Yet CI runners routinely have wide open egress, because nobody chose otherwise.

It also matters more than the laptop case, because CI holds deployment credentials and runs unattended against code that may have been authored by an agent and merged by an automated check. If you only do this in one place, do it there. The broader framing of what CI should and should not permit an agent to do is in running agents in CI.

The realistic configuration

Egress denied by default. A proxy with four allowed hostnames: your registry, your version control host, your model provider, and one internal service if you have one. Dependencies pre-seeded before the session so the registry allowance is rarely exercised. Lifecycle scripts disabled on install. Fetch-and-pipe-to-shell denied by pattern. A weekly look at what was permitted, and a self-test after every change to the setup.

None of that is exotic and all of it is boring to run once configured, which is the property you want from a control that has to stay on for years. The version that fails is the one that blocks a legitimate install on a deadline afternoon, because that is the moment it gets switched off for good. Removing that failure mode — by pre-seeding — is what makes the policy survive.

The failure mode of a good policy is a bad afternoon

Every egress policy dies the same way. Someone is trying to ship, an install fails with a network error, they have twenty minutes, and the fastest path is to turn the restriction off. It never gets turned back on, because nothing prompts anyone to do so and everything works.

Which means the durability of the control depends almost entirely on how rarely it blocks legitimate work. A policy that interrupts once a month survives for years. A policy that interrupts twice a week has a lifespan of about a fortnight, no matter how well-reasoned it was.

This is worth saying because the instinct when a control gets bypassed is to add process — a rule that it must not be disabled, an approval to change it. That treats the symptom. The durable fix is to remove the interruptions, which is what pre-seeding does, and then the policy costs nobody anything and nobody has a reason to touch it.

Different postures for different work

One policy for everything is easier to describe and worse to live with. The useful split is by what the session is doing rather than by who is running it.

Exploratory work in an unfamiliar codebase benefits from documentation lookups, and the risk is low because nothing is being deployed. A session making changes to a repository that holds production credentials should have nothing but the registry. An unattended run — a scheduled job, a CI task — should have the narrowest policy of all, because there is no human to notice anything odd, and that is precisely the condition under which a surprising outbound connection matters.

Three profiles, selected by what you are about to do, is more useful than one compromise policy that is too loose for the dangerous case and too tight for the harmless one.

What a blocked connection should look like

An underrated detail: the way a denial surfaces determines whether anyone learns anything from it. A connection that hangs until a timeout produces a confusing failure several minutes later, usually attributed to a flaky network. A connection refused immediately, with the destination named in the error, tells the agent and the human exactly what happened.

Configure for fast, explicit failure. Then a blocked request becomes a useful event — the agent can report it, you can decide whether the destination belongs on the allowlist, and the alternative interpretation ("the network is unreliable") never gets a foothold.

One more small thing that pays for itself: name your allowlist entries with the reason they exist and the date they were added. An allowlist of four hostnames with a one-line justification each is a document anyone can audit in a minute. The same four hostnames with no comments is a list nobody will ever dare to shorten, because removing an entry whose purpose is unknown is a risk and leaving it is not.

Takeaway

Outbound network access is the highest-value restriction on an agent environment and the first one abandoned, because it breaks dependency installs. Pre-seed dependencies before the sandboxed session and that objection disappears. Filter by hostname at a proxy rather than by address at a firewall, deny fetch-piped-to-shell even inside the allowlist, remember that a credential fits in a query string so any channel out is a full channel, and verify the denial by attempting a raw connection by IP.

Keep reading
Codex vs Claude

Below the Prompt: What a Kernel Sandbox Actually Constrains

Seatbelt, bwrap and seccomp enforce policy that injected text cannot argue with. What a profile can express, why a workspace-write policy still permits everything inside your repository including .git/hooks, and a self-test that proves the policy is on.

Codex vs Claude

Approval Fatigue: The Control Degrades Every Time You Use It

Interactive permission prompts spend a consumable resource. Why the count matters more than the wording, why deny lists beat allow lists, and how to make destructive commands break the rhythm instead of matching it.

Codex vs Claude

What the Agent Inherits: Sessions You Are Already Logged Into

Credential discussions focus on secrets in files. On a developer machine most access is an authenticated session an agent can simply use: cloud CLIs, the current kubectl context, a forwarded SSH agent. An inventory script and what to make absent by default.

Security Engineering

Reviewing Third-Party MCP Servers Before You Install Them

A tiered review process short enough to actually run: publisher, source, behaviour and descriptions, plus the pinning and containment that keep working when the review was wrong.

← Generated Placeholder Assets: Borrowing Against a Design You Have Not Made  ·  What a Tool Protocol Standardises, and What It Leaves to You →

All codex vs claude articles  ·  Every article