---
title: Secrets and egress
description: >-
  Give the sandbox keys without letting the agent read them, and control where
  it can connect.
---
> ## 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.

Anything you put in the sandbox, the agent can read. A brokered secret is the exception: the sandbox holds a placeholder, and reeval swaps in the real value only on its way to the hosts you name.

```ts title="evals/environments.ts"
export const genkit = defineEnvironment('genkit', ({ secret }) => ({
  env: {
    GOOGLE_GENAI_API_KEY: secret.fromEnv('GOOGLE_GENERATIVE_AI_API_KEY', {
      hosts: secret.hosts.gemini,
    }),
    GEMINI_API_KEY: secret.fromEnv('GOOGLE_GENERATIVE_AI_API_KEY', {
      hosts: secret.hosts.gemini,
    }),
  },
}))
```

Say your evals build Genkit apps that call Gemini. With this environment the apps work, and the key never enters the container.

## Pass a secret

```ts title="evals/guide-secret.eval.ts"
const plain = defineEnvironment('plain', ({ secret }) => ({
  env: { ACME_API_KEY: secret.fromEnv('ACME_API_KEY') },
}))
```

```text
warning: ACME_API_KEY is in the sandbox as its real value. To give the agent only a placeholder, add the hosts it's sent to, e.g. secret.fromEnv('ACME_API_KEY', { hosts: ['api.example.com'] }).
```

`secret.fromEnv(name)` reads a host variable when the attempt starts. reeval masks the value in everything it writes and records only the name.

Without `hosts`, though, the sandbox gets the real value, so the plan warns. `secrets: { unbrokered: 'error' }` in `reeval.config.ts` makes that warning an error.

<Warning>
  A literal string under a secret-looking name (`*_KEY`, `*_TOKEN`…) is refused. Read keys with `secret.fromEnv`, never inline them.
</Warning>

## Broker a secret to its hosts

The plan lists each eval's brokered secrets:

```text
evals/gemini.eval.ts › the agent calls Gemini with a brokered key  1          —         1     1         brokered: GOOGLE_GENAI_API_KEY → generativelanguage.googleapis.com; GEMINI_API_KEY → generativelanguage.googleapis.com
```

With `hosts`, the agent and checks get a per-attempt placeholder (`reeval_secret_…`). reeval's egress gateway swaps in the real value only on HTTPS requests to those hosts, in headers by default, and `in: ['query']` or `in: ['body']` opts in to the query string or the body.

`secret.hosts` has presets for `gemini`, `openai`, `anthropic` and `github`.

```ts title="evals/gemini.eval.ts"
const key = await c.exec('printenv GEMINI_API_KEY')
expect(key.stdout).toMatch(/^reeval_secret_/)
```

A client must use the proxy variables and trust store the gateway sets (curl, npm, pip, git and Node's `fetch` do). [Credential brokering](/concepts/credential-brokering) explains the limits.

## Keep a secret from the agent

```ts title="evals/guide-matrix.eval.ts"
export const service = defineEnvironment('service', ({ secret }) => ({
  env: {
    ADMIN_TOKEN: secret.fromEnv('ACME_ADMIN_TOKEN', {
      hosts: ['api.example.com'],
      to: 'checks',
    }),
  },
}))
```

`to: 'checks'` gives a secret only to `c.exec` and setup steps. The agent's own requests during a turn go up with the placeholder, so a check can use an admin API that the agent can't.

## Restrict egress

```ts title="evals/guide-matrix.eval.ts"
import { DEFAULT_EGRESS_ALLOW, defineEnvironment } from '{{ pkg }}'

export const offline = defineEnvironment('offline', { egress: 'deny-all' })

export const registries = defineEnvironment('registries', {
  egress: { allow: [...DEFAULT_EGRESS_ALLOW, 'api.example.com'] },
})
```

```text
evals/guide-matrix.eval.ts › works offline                     1          —         1     1         egress: deny-all + auto: api2.cursor.sh, *.cursor.sh
evals/guide-matrix.eval.ts › installs from the registries      1          —         1     1         egress: allow registry.npmjs.org, registry.yarnpkg.com, github.com, *.github.com, *.githubusercontent.com, pypi.org, files.pythonhosted.org, api.example.com + auto: api2.cursor.sh, *.cursor.sh
```

`egress` is `'proxy'` (the default), `'allow-all'`, `'deny-all'` or `{ allow: [hosts] }`, where `*.example.com` matches subdomains. A restricted policy also allows what the attempt needs: the harness's API, each brokered secret's hosts and each MCP server's host, so an agent still works under `'deny-all'`.

`DEFAULT_EGRESS_ALLOW` is the package registries and GitHub.

`egress` also works as an option on the config, a profile's defaults, a group or an eval. Each level replaces the one before.

## See what was refused

```text
[brokers#1 fake]  13:09:54 gateway   CONNECT blocked.example.net:443 → 403 · blocked: blocked.example.net isn't in the allowlist · ↑0 B ↓0 B · 0ms · no key substituted
```

A request to any other host gets a `403` from the gateway, and the attempt goes on: the checks decide whether that matters. `reeval run -v` prints a line per request.

## Raw TCP needs allow-all

```ts title="evals/db.eval.ts"
// A raw TCP connection to a database on the host's network
const db = "bash -c 'exec 3<>/dev/tcp/172.30.0.2/5432 && head -c 2 <&3'"

evaluate('reaches the database', { egress: 'allow-all' }, async (c) => {
  await c.send('Run the migrations.')
  await expect(c.exec(db)).toSucceed()
})
```

Under every policy but `'allow-all'`, the container has no network of its own: HTTP(S) through the gateway is the only way out. SSH, a database driver or any client that ignores `HTTPS_PROXY` gets `Network is unreachable`, and the run ends with a [remark](/reference/egress#connections-that-never-reach-the-gateway) saying so.

Under `'allow-all'` the container keeps its network. That traffic goes out directly, unfiltered and unrecorded, and can reach cloud metadata and your LAN; the record marks the egress `partial`. Set it on the evals that need it, not the config.

## What's recorded

<Image src="/assets/studio/attempt-state-egress.png" alt="The studio's State tab for an attempt, with an egress panel listing hosts contacted and refused" width={800} />

Each attempt's record keeps its egress: the policy, the allowed hosts, each host it contacted with bytes sent and received, and each host refused with the reason. For secrets, it keeps only names, and which ones were brokered. The studio's State tab shows both.

<Info>
  `fakeSandbox()` runs commands on the host, so it enforces nothing under `'proxy'` or `'allow-all'`. A restricted policy on it stops the run before anything starts.
</Info>

## Next

<CardGroup cols={2}>
  <Card title="Sizing runs" icon="gear" href="/guide/profiles">
    `pr` and `nightly` profiles, dry runs and budgets.
  </Card>
  <Card title="Egress policies" icon="globe" href="/concepts/egress">
    What each policy enforces, and what it can't.
  </Card>
</CardGroup>
