---
title: defineEnvironment
description: >-
  Every field of an environment, the order an attempt applies them in, extend
  and overrides, and the fields it refuses.
---
> ## 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.

`defineEnvironment(name, spec)` describes what an eval runs in: the sandbox, the code the workspace starts as, variables, secrets, files, rules, setup steps, skills, plugins, MCP servers and egress.

```ts title="evals/environments.ts"
import { DEFAULT_EGRESS_ALLOW, defineEnvironment } from '{{ pkg }}'
import { docker } from '{{ pkg }}/sandbox'

export const base = defineEnvironment('base', ({ secret }) => ({
  sandbox: docker({ from: 'node:24-bookworm' }),
  env: {
    NODE_ENV: 'test',
    NPM_TOKEN: secret({ hosts: ['registry.npmjs.org'] }),
  },
  files: { '~/.npmrc': 'fund=false\n' },
  setup: [{ run: 'npm ci', timeout: '5m' }],
  egress: { allow: [...DEFAULT_EGRESS_ALLOW] },
}))

export const withSkill = base.extendEnvironment(
  'with-skill',
  ({ file, mcp, skill }) => ({
    skills: [skill.local('./skills/house-style')],
    rules: file('./rules/house.md'),
    mcp: { docs: mcp.stdio('npx', ['-y', '@acme/docs-mcp@1.0.0']) },
  }),
)
```

Use it as `environment` when the eval does not compare. For a compare, pass the variants, and the baseline unless the eval's `environment` is the control. Share layers first with `extendEnvironment`; `compare` does not layer them:

```ts title="evals/style.eval.ts"
group(
  'house style',
  { compare: { baseline: base, variants: [withSkill] } },
  () => {
    evaluate('formats with tabs', async (c) => {
      await c.send('Format src/index.ts.')
    })
  },
)
```

## Signature

```ts
defineEnvironment(name, spec)
defineEnvironment(name, (helpers) => spec)
```

<Property name="name" type="string">
  Letters, digits, `.`, `_`, `@`, `+` or `-`, starting with a letter or digit. It names the environment in the plan, the cell key and the record.
</Property>

<Property name="spec" type="EnvironmentSpec | ((helpers) => EnvironmentSpec)">
  The fields below, or a callback that gets `secret`, `skill`, `plugin`, `mcp`, `file` and `git` (see [Helpers](/reference/helpers)). The callback runs once, synchronously, when the file loads; returning a `Promise` is an error. Relative paths resolve against the file that calls `defineEnvironment`.
</Property>

It returns a frozen `Environment` with `name`, `layers` and `extend` / `extendEnvironment`.

## Fields

<Property name="sandbox" type="SandboxDescriptor" optional>
  Where the attempt runs: `docker()` or `fakeSandbox()`. A child's `extend` / `extendEnvironment` replaces the parent's entirely when it sets one; when the child omits it, the parent's sandbox is kept. `install` is not merged onto the parent image. A bare environment that is not an extend, and that omits `sandbox`, uses the cascaded option (`defineConfig` / profile defaults / group / eval), else implicit Docker. It does not inherit the config environment's sandbox. The cell key includes the image (`from`, `dockerfile`, `install`), so Node 22 and Node 24 cannot share a cell. Environments that share an image still share one preflight build.
</Property>

<Property name="workspace" type="GitWorkspace" optional>
  The code every attempt's workspace starts as: a git repository, or one directory of it, from the `git` helper (`git(repo, { ref, path, init, token })`; see [Helpers](/reference/helpers#git)). reeval fetches it on the host once per run, before any attempt, and copies its tree in before `files` and `setup`: without `.git`, or with a new one holding one commit of the tree when `init: true`. Submodules and Git LFS files aren't fetched. The record's `workspace` keeps the commit the ref resolved to and the tree's hash, which is in the environment's digest.
</Property>

<Property name="env" type="Record<string, string | Secret>" optional>
  Variables for the sandbox. Secrets come from the `secret` helper; a literal string under a name containing `KEY`, `TOKEN`, `SECRET`, `PASSWORD` or `CREDENTIAL` is refused. A secret with `hosts` is brokered: the sandbox gets a placeholder, and the egress gateway swaps in the real value on requests to those hosts.
</Property>

<Property name="files" type="Record<string, string | HostFile>" optional>
  Files to write before the agent starts. Keys are workspace-relative paths, or `~/` paths in the sandbox's home, with forward slashes; an absolute path or a `..` segment is refused. Values are the content, or `file(path)` for a host file, copied byte for byte. Spread `directory(path)` to seed a whole host directory; a later key overrides.

  The workspace needs its own `.gitignore`: your project's isn't copied in unless you list it here or it's in the `workspace` repository. It decides what counts as the agent's change, so without one the build output and caches the agent creates land in the diff the [judge](/reference/judges#what-the-judge-is-given) reads.
</Property>

<Property name="rules" type="string | HostFile" optional>
  Project rules, as markdown or `file(path)`. They're written where the harness's agent reads them: `CLAUDE.md` for Claude Code, `AGENTS.md` for every other agent.
</Property>

<Property name="setup" type="Array<string | { run: string, timeout?: string | number, cwd?: string }>" optional>
  Shell commands run in every attempt, in order, after files and rules and before the agent starts. They see the environment's secrets, including those sent only `to: 'checks'`. Each step's `timeout` defaults to `'5m'`; `cwd` is the directory it runs in, relative to the workspace or absolute. A step object takes only `run`, `timeout` and `cwd`. Output goes to the attempt's `setup.log`. A failing step is an infra error, not a failed eval.
</Property>

<Property name="skills" type="SkillSource[]" optional>
  Skills from `skill.local`, `skill.git`, `skill.npm`, `skill.registry` or `skill.inline`. By default the harness adapter installs them where its agent reads skills; `placement: 'workspace'` writes them into the repo's skill directory instead. See [Helpers](/reference/helpers#skill).
</Property>

<Property name="plugins" type="PluginSource[]" optional>
  Experimental: which parts each harness loads, and how, may change in any release. Plugins in Claude Code's layout, from `plugin.local`, `plugin.git`, `plugin.npm` or `plugin.marketplace` (an entry in a Claude Code marketplace): a directory with `.claude-plugin/plugin.json` and any of `skills/`, `agents/`, `commands/`, `hooks/hooks.json`, `.mcp.json` and `rules/`. `only` narrows the parts placed. The plugin's MCP servers join `mcp` as `<plugin>:<server>`, so egress and secrets treat them like any other server. reeval copies each plugin to `~/.reeval/plugins/<name>/` before the snapshot. A plugin with a `build` is built first in its own cached image layer on the harness image (as root, with no secrets, from the whole source), and only its `path` is copied from that layer; see [Helpers](/reference/helpers#plugin) and [Docker](/reference/sandbox/docker#images-and-caching). Cursor and Antigravity load every part, Claude Code and Grok Build every part but `rules`, OpenCode every part but `hooks` and `rules`, pi its `skills`, `commands` and `mcp`, and Codex, Goose and fx their `skills` and `mcp`; the plan refuses a part the harness doesn't load, and the fake harness takes every part. See [Helpers](/reference/helpers#plugin).
</Property>

<Property name="mcp" type="Record<string, McpServer>" optional>
  MCP servers, keyed by name, from `mcp.stdio`, `mcp.http` or `mcp.sse`. The agent gets them when its session starts.
</Property>

<Property name="egress" type="'proxy' | 'allow-all' | 'deny-all' | { allow: string[] }" optional>
  Where the sandbox may connect. It replaces the policy from the config and profile `defaults`; a group's or eval's own `egress` replaces it in turn. See [Egress](/reference/egress).
</Property>

## Phase order

Every attempt applies an environment in the same order:

<Steps>
  <Step title="Image">
    The sandbox's image is ready (built once and cached; see [docker()](/reference/sandbox/docker)).
  </Step>
  <Step title="Variables, workspace, files and rules">
    The sandbox starts with `env`; reeval unpacks the `workspace` repository's tree, then writes `files` (over it) and `rules`.
  </Step>
  <Step title="Plugins">
    Each plugin's kept parts are copied to `~/.reeval/plugins/<name>/`, with `${CLAUDE_PLUGIN_ROOT}` in its MCP servers set to that directory.
  </Step>
  <Step title="Setup">
    `setup` steps run in order, outside the eval timeout.
  </Step>
  <Step title="Snapshot">
    reeval snapshots the workspace, so the repository, files, rules, plugins and setup never show in the agent's diff.
  </Step>
  <Step title="Agent">
    The harness starts with the environment's `skills`, `plugins` and `mcp` servers.
  </Step>
</Steps>

## extendEnvironment

```ts
parent.extendEnvironment(name, spec)
parent.extendEnvironment(name, (helpers) => spec)
```

`extendEnvironment` is how two environments share setup. The child's spec layers on its parent's:

| Field | When both set it |
|---|---|
| `sandbox` | The child's replaces the parent's entirely; omit it and the parent's is kept |
| `env` | Merged by variable name; the child wins |
| `files` | Merged by path; the child wins |
| `skills`, `plugins`, `mcp` | Merged by name (a plugin's is its manifest's `name`); the child wins |
| `rules` | The child's replace the parent's |
| `workspace` | The child's replaces the parent's |
| `egress` | The child's replaces the parent's |
| `setup` | Concatenated, parent first |

Every key the child replaced is listed in the environment's `overrides` in the record (`sandbox`, `env.NODE_ENV`, `files.src/a.ts`, `rules`, `workspace`, `skills.house-style`, `plugins.review`, `mcp.docs`, `egress`).

Call it before `compare`. A variant cell then runs as that environment alone. `extend` is an alias of `extendEnvironment`.

## Refused fields

Image fields belong on `sandbox: docker({ … })`, not as sibling keys:

```text
reeval: evals/e.eval.ts: "image" isn't an environment field (defineEnvironment("e")). Put the image on the sandbox: sandbox: docker({ from: … }).
```

`install`, `dockerfile` and `from` get the same message, naming their own key; `image` set to a string, such as `'node:22'`, names `docker({ from: … })`. Near misses get a hint: `steps`, `exec` and `run` point to `setup`; `vars` and `secrets` to `env`; `network` and `networkPolicy` to `egress`; `skill` to `skills`; `plugin` to `plugins`; `file` to `files`; `agents` to `rules`; `use` to `extend`; `repo`, `git` and `template` to `workspace`.

## Errors

These stop the run with exit 2, prefixed by the eval file.

| Mistake | Message |
|---|---|
| A bad name | `defineEnvironment needs a name of letters, digits, ".", "_", "@", "+" or "-" (got "my env")` |
| An unknown field | `Unknown field "steps" in defineEnvironment("e"). Did you mean "setup"?` |
| A `Promise` | `defineEnvironment("e") returned a Promise. The callback runs once at definition time and must return the environment synchronously; fetch remote content in a setup step instead.` |
| A literal secret | `env.GITHUB_TOKEN in defineEnvironment("e") is a literal string. Pass secrets with secret.fromEnv() so they're redacted and never recorded.` |
| `setup` not a list | `setup in defineEnvironment("e") must be a list (got "npm ci")` |
| A bad step | `setup[0] in defineEnvironment("e") must be a command string or { run, timeout?, cwd? } (got 3)`; an unknown key gets `setup step "npm ci" in defineEnvironment("e") has an unknown key "env"; a step is a string or { run, timeout?, cwd? }.` |
| A skill path string | `skills in defineEnvironment("e") must come from the skill helper, e.g. skills: [skill.local('./skills')] (got "./skills")` |
| A plugin path string | `plugins in defineEnvironment("e") must come from the plugin helper, e.g. plugins: [plugin.local('./plugins/review')] (got "./plugins/review")` |
| An MCP object | `mcp.docs in defineEnvironment("e") must come from the mcp helper, e.g. mcp.stdio('npx', ['-y', 'server']) (got {"command":"npx"})` |
| A bad file value | `files["a.txt"] in defineEnvironment("e") must be a string or file(path) (got 1)` |
| Bad rules | `rules in defineEnvironment("e") must be markdown or file(path) (got 3)` |
| A `workspace` that isn't from `git()` | `workspace in defineEnvironment("e") must come from the git helper, e.g. workspace: git('https://github.com/acme/app', { ref: 'v1.0.0' }) (got "https://github.com/acme/app")` |
| A list as `environment` | `environment takes one environment, not a list (evaluate("x")). To compare environments, use compare: { baseline, variants: [a, b] }. baseline is the control; variants are not layered on it.` |

A `workspace` whose repository, ref, directory or token doesn't work is reported before any attempt starts and fails only the attempts that use it, as infra errors (exit 3), like a sandbox image that failed to prepare; see [Errors](/reference/errors#git-workspaces). The plan refuses a plugin it can't place with exit 2, naming the eval and the harness: on a harness that can't load it or one of its kept parts, a plugin MCP server that reads a host variable on Claude Code, Antigravity, Grok Build or Codex (or shares a name with another server on Grok Build, Codex or pi), a hook event or type the harness doesn't run, a plugin with ChatGPT apps on Codex, a plugin with a manifest Antigravity or Codex reads in place of `.claude-plugin/plugin.json`, two plugin items that clash on Cursor, a local plugin with no manifest, `only` naming a part the plugin lacks, a `build` on `fakeSandbox()`, or an MCP server or hook that runs `npx`, `bunx`, `uvx` or `pipx run` under an egress policy that doesn't allow the registry (see [Errors](/reference/errors#plugins)). When an attempt starts, a missing secret, a skill directory without `SKILL.md`, a git, npm or marketplace plugin with one of those problems, a marketplace without `.claude-plugin/marketplace.json` or without the entry named, or a failing setup step is an infra error for that attempt. See [Errors](/reference/errors).

## Related

- [Environments guide](/guide/environments)
- [Helpers](/reference/helpers)
- [Egress](/reference/egress)
- [Credential brokering](/concepts/credential-brokering)
