---
title: Egress
description: >-
  Egress policies, the hosts reeval allows automatically, DEFAULT_EGRESS_ALLOW,
  the plan line and the egress field of the record.
---
> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reeval.ai/llms.txt
> Use this file to discover all available pages before exploring further.

`egress` says where the sandbox may connect. The Docker sandbox enforces it and records every connection through its egress gateway.

```ts title="evals/offline.eval.ts"
import { DEFAULT_EGRESS_ALLOW, evaluate } from '{{ pkg }}'

evaluate(
  'installs from the registry only',
  { egress: { allow: [...DEFAULT_EGRESS_ALLOW] } },
  async (c) => {
    await c.send('Add lodash and use it in index.js.')
  },
)
```

## Policies

<Property name="'proxy'" type="EgressPolicy">
  Any host over HTTP(S), through the egress gateway only. The default. The container has no network of its own, so every connection is brokered, checked against the [refused addresses](#addresses-that-are-always-refused) and recorded. Raw TCP and UDP (SSH, database drivers, a client that ignores `HTTPS_PROXY`) get no connection.
</Property>

<Property name="'allow-all'" type="EgressPolicy">
  Anywhere, by any protocol. The container keeps its Docker network: proxy-aware HTTP(S) still goes through the gateway, brokered and recorded, but anything else goes out directly, unfiltered and unrecorded, and reaches cloud metadata and the host's network. The record marks the attempt's egress `partial`.
</Property>

<Property name="'deny-all'" type="EgressPolicy">
  Nowhere but the automatic hosts below.
</Property>

<Property name="{ allow: string[] }" type="EgressPolicy">
  The listed hosts, plus the automatic hosts. `*.example.com` matches any subdomain of `example.com` (not `example.com` itself). IPv4 addresses are allowed as entries. Host names only: no scheme, path or port.
</Property>

A refused request doesn't fail the attempt: the client gets an error and the agent carries on. The refusal is recorded, and the response body says why: `blocked by the reeval egress policy: <reason>`.

## Where it's set

`egress` is accepted in `reeval.config.ts`, profile `defaults`, environments, groups and evals. The most specific wins, and a policy replaces rather than merges:

| Level | Wins over |
|---|---|
| The eval, then its groups | Everything below |
| The environment (its last layer that sets `egress`) | Config and profile `defaults` |
| Profile `defaults`, then the config | The default, `'proxy'` |

## Automatic hosts

Under `'deny-all'` or an allowlist, reeval adds the hosts the attempt can't work without:

| Source | Hosts |
|---|---|
| `cursor()` | `api2.cursor.sh`, `*.cursor.sh` |
| `claudeCode()` | `api.anthropic.com`, `*.anthropic.com` |
| `antigravity()` | `generativelanguage.googleapis.com` |
| `opencode()` | The model's provider's API hosts (`generativelanguage.googleapis.com` for `google/…`, `api.anthropic.com` for `anthropic/…`, [every provider](/reference/harness/opencode#supported-providers)) and `mcp.exa.ai` (its web search) |
| `goose()`, `pi()` | `generativelanguage.googleapis.com` |
| `codex()` | `api.openai.com` |
| `fx()` | `ai-gateway.vercel.sh` for a gateway id, `api.openai.com` for a bare OpenAI id |
| `grokBuild()` | `api.x.ai` |
| `fakeHarness({ layout })` | That agent's hosts |
| A brokered secret | Its `hosts` |
| An `mcp.http` or `mcp.sse` server | Its host |

Hosts already in the allowlist aren't repeated.

## DEFAULT_EGRESS_ALLOW

The package registries and GitHub, exported from `'{{ pkg }}'` for spreading into an allowlist:

```text
registry.npmjs.org, registry.yarnpkg.com, github.com, *.github.com,
*.githubusercontent.com, pypi.org, files.pythonhosted.org
```

## Addresses that are always refused

```ts
egress: { allow: ['db.internal', '10.0.0.5'] } // a private host: its name and its address
```

Whatever the policy, the gateway refuses loopback and link-local addresses (`127.0.0.0/8`, `169.254.0.0/16`, `0.0.0.0/8`, `::`, `::1`, `fe80::/10`). It also refuses private and reserved addresses unless an allowlist names the address itself: `10.0.0.0/8`, `100.64.0.0/10`, `172.16.0.0/12`, `192.168.0.0/16`, `fc00::/7`, multicast (`224.0.0.0/4`, `ff00::/8`) and `255.255.255.255`. A NAT64 address (`64:ff9b::/96`) is checked by the IPv4 address inside it too.

To reach a private host by name, list both its name and its address. A `*.` pattern never matches a host that resolves to a private address, even when the address is listed. The allowlist is checked before any DNS lookup, so a name it doesn't list is never resolved.

| Refusal reason | When |
|---|---|
| `the policy denies all egress` | `'deny-all'`, and the host isn't automatic |
| `<host> isn't in the allowlist` | An allowlist that doesn't match |
| `can't resolve …` | DNS failed (a 502) |
| `… is a loopback or link-local address` | Always refused |
| `… is a private or reserved address, which allow-all doesn't reach; list the address in an allowlist to reach it` | `'proxy'` or `'allow-all'` reaching a private or reserved address through the gateway |
| `… is a private or reserved address; list the address itself in the allowlist to reach it` | An allowlist that doesn't name the private or reserved address |
| `… resolves to a private address, which a *. pattern doesn't allow; list the host itself` | A wildcard reaching a private address |

<Warning>
  An allowlist with `DEFAULT_EGRESS_ALLOW` still reaches GitHub, npm and PyPI, where the agent could push or publish data. Under `'allow-all'` the container keeps its own network, so a client that ignores the proxy bypasses the gateway and these refusals.
</Warning>

## allow-all on a cloud VM or CI runner

```text
warning: egress 'allow-all' on a GitHub Actions runner (GITHUB_ACTIONS): anything in the sandbox can reach the cloud metadata service (169.254.169.254, which hands out the machine's credentials), the host's network and its services around the egress gateway, unfiltered and unrecorded. Under it: evals/db.eval.ts › reaches the database (allow-all). Keep 'allow-all' to the evals that need raw TCP or UDP; the default, 'proxy', sends everything through the gateway
```

`reeval run` prints this warning under the plan when a Docker attempt runs under `'allow-all'` on a CI runner (`GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI` or `CI`). [`reeval doctor`](/reference/cli/doctor) warns too when the config or a profile sets `'allow-all'` there, on a cloud VM (its firmware's vendor) or where the metadata service answers.

## Connections that never reach the gateway

```text
Remarks:
  reaches the database: It failed with "Network is unreachable" and nothing reached the egress gateway: under egress 'proxy' only HTTP(S) through the gateway leaves the sandbox, so raw TCP, UDP, SSH and database connections fail.
    fix  evals/db.eval.ts:5  set egress: 'allow-all' on the eval if it needs raw TCP or UDP, or point the client at HTTPS_PROXY
```

Under `'proxy'`, a raw TCP connection fails with `Network is unreachable`, `connection refused` or a name that doesn't resolve, and the gateway records nothing. When a failed attempt's error, failed checks or turns show one of those and the gateway refused nothing, the run ends with the `proxy-only-egress` remark. Set `egress: 'allow-all'` on that eval, or point the client at `HTTPS_PROXY`.

## The plan line

A restricted policy shows in the plan's notes column, with the automatic hosts after `+ auto:`:

```text
egress: deny-all + auto: api2.cursor.sh, *.cursor.sh
```

```text
brokered: API_KEY → api.example.com · egress: allow registry.npmjs.org + auto: api2.cursor.sh, *.cursor.sh, api.example.com
```

With `-v`, each attempt logs a `gateway` line per request, allowed or refused. See [Debugging](/guide/debugging).

## In the record

Each attempt's record has an `egress` field. From a fake agent in Docker that ran `curl https://registry.npmjs.org/left-pad`, under the default and under `'allow-all'`:

```text
"egress": {"policy": "proxy", "allowed": null, "recorded": true, "contacted": [{"host": "registry.npmjs.org", "port": 443, "connections": 1, "sent": 916, "received": 26363}], "blocked": [], "expectedBlocked": []}
"egress": {"policy": "allow-all", "allowed": null, "recorded": true, "partial": true, "contacted": [{"host": "registry.npmjs.org", "port": 443, "connections": 1, "sent": 916, "received": 26343}], "blocked": [], "expectedBlocked": []}
```

<Property name="policy" type="EgressPolicy">
  The policy the attempt ran under.
</Property>

<Property name="allowed" type="string[] | null">
  Every allowed host, automatic ones included. `null` under `'proxy'` and `'allow-all'`.
</Property>

<Property name="recorded" type="boolean">
  Whether traffic was recorded: `true` in Docker, `false` in the fake sandbox.
</Property>

<Property name="partial" type="true" optional>
  Set when the record can't be complete: under `'allow-all'` in Docker, traffic that ignored the proxy went out directly and isn't in `contacted`.
</Property>

<Property name="contacted" type="{ host, port, connections, sent, received }[]">
  Hosts the sandbox reached, with connection counts and bytes each way.
</Property>

<Property name="blocked" type="{ host, port, connections, reason }[]">
  Hosts the gateway refused.
</Property>

<Property name="expectedBlocked" type="{ host, port, connections, reason }[]">
  Refused hosts the harness is known to try and doesn't need, such as Antigravity's telemetry. Kept apart from `blocked` so they don't look like a problem.
</Property>

Any policy but the default is part of the environment's fingerprint, so runs under different policies don't compare as equal.

## Errors

| Mistake | Message |
|---|---|
| A bare list | `egress in reeval.config.ts is 'proxy' (the default), 'allow-all', 'deny-all', or { allow: ['api.example.com', '*.example.org'] } (got ["api.example.com"]). For a list of hosts, use { allow: ["api.example.com"] }.` |
| A deny list | `Unknown egress key "deny" in reeval.config.ts. A policy lists what's allowed; everything else is refused.` |
| A URL | `egress.allow in reeval.config.ts: hosts are host names, not URLs: use 'api.example.com' (got 'https://api.example.com/v1')` |
| A port | `egress.allow in reeval.config.ts: hosts are host names without a port: use 'api.example.com' (got 'api.example.com:443')` |
| A top-level wildcard | `egress.allow in reeval.config.ts: '*.com' would allow every host under .com; name the domain, e.g. '*.example.com'.` |
| A wildcard address | `egress.allow in reeval.config.ts: '*.10.0.0.1' isn't a host name or an IPv4 address, and a *. pattern never matches an address.` |
| A policy in the fake sandbox | `… runs under egress 'deny-all', which needs turning egress off, but the fake sandbox enforces no egress policy (its commands run on the host). Run it in docker(), or set egress: 'allow-all'.` |

## Related

- [Egress concepts](/concepts/egress)
- [Secrets and egress](/guide/secrets-and-egress)
- [Credential brokering](/concepts/credential-brokering)
- [docker()](/reference/sandbox/docker)
