---
title: Helpers
description: >-
  The secret, skill, plugin, mcp, file, directory and git helpers that
  defineConfig and defineEnvironment callbacks receive.
---
> ## 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.

`defineConfig` and `defineEnvironment` callbacks receive seven helpers: `secret`, `skill`, `plugin`, `mcp`, `file`, `directory` and `git`.

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

export const tools = defineEnvironment(
  'tools',
  ({ directory, file, mcp, secret, skill }) => ({
    env: {
      GEMINI_API_KEY: secret({ hosts: secret.hosts.gemini }),
      DB_URL: secret.fromEnv('TEST_DB_URL', { to: 'checks' }),
      SENTRY_DSN: secret({ optional: true }),
    },
    skills: [
      skill.local('./skills', {
        exclude: [...DEFAULT_SKILL_EXCLUDE, 'drafts/'],
      }),
      skill.git('github:acme/skills', { ref: 'v2.4.0', only: ['genkit'] }),
      skill.npm('@acme/skills@1.2.3'),
      skill.registry('acme/skills@review', { version: 'v1.0.0' }),
      skill.inline({
        name: 'commit-style',
        description: 'How to write commit messages here.',
        content: 'Use the imperative mood.',
      }),
    ],
    mcp: {
      docs: mcp.stdio('npx', ['-y', '@acme/docs-mcp@1.0.0']),
      search: mcp.http('https://mcp.acme.dev/mcp', {
        headers: { Authorization: secret.template('Bearer ${ACME_TOKEN}') },
      }),
    },
    files: {
      ...directory('./fixtures/app', { into: 'app' }),
      'fixtures/data.json': file('./fixtures/data.json'),
    },
  }),
)
```

Group and eval options are plain objects, so the helpers aren't available there: put secrets in an environment or the config's `env`.

## secret

A secret is read from the host when an attempt starts. Its value is masked in everything reeval writes, travels through the Docker CLI's environment rather than its arguments, and only its name is recorded.

| reeval masks | In |
|---|---|
| Declared secrets and secret-named host variables (`KEY`, `TOKEN`, `SECRET`, `PASSWORD`, `CREDENTIAL`) | The terminal, `.reeval/debug`, records, transcripts, history and the studio |
| Their JSON-, URL- and base64-encoded forms | The same |
| Any stretch of 8 or more of their characters, such as a key's last 12 | The same, for declared secrets of 12+ characters and token-like variables (16+ characters of letters, digits and `_.~+/=:-`, with at least one letter and one digit, not a path or URL) |
| Well-known token shapes (`AIza…`, `sk-…`, `ghp_…`, AWS keys, JWTs, private keys) | The same, even when they aren't set on the host |

A stretch made only of lower-case letters, upper-case letters or digits isn't masked on its own, so words and numbers stay readable, nor is one with quotes, commas, colons, brackets or spaces, so a record's JSON stays whole. Secret-named host variables under 8 characters and declared secrets under 4 aren't masked at all.

Still, don't print any part of a key to check for a leak: compare on the host, or pass the sandbox a hash.

<Property name="secret(options?)" type="Secret">
  Reads the host variable with the same name as the key it's set under.
</Property>

<Property name="secret.fromEnv(name, options?)" type="Secret">
  Reads the host variable `name`, whatever key it's set under.
</Property>

<Property name="secret.template(template)" type="SecretTemplate">
  A string with host variables substituted, for MCP headers: `secret.template('Bearer ${TOKEN}')`. Every variable must be set.
</Property>

<Property name="secret.hosts" type="{ gemini, openai, anthropic, github }">
  The hosts well-known APIs take their keys on, for `hosts`.
</Property>

Options for `secret()` and `secret.fromEnv()`:

<Property name="hosts" type="string[]" optional>
  Brokers the secret. The sandbox gets a per-attempt placeholder (`reeval_secret_` and 32 characters), and the Docker egress gateway swaps in the real value on requests to these hosts. Host names only, no URL or port; `*.example.com` matches subdomains. Brokered hosts are allowed through a restricted egress policy automatically.
</Property>

<Property name="in" type="Array<'header' | 'query' | 'body'>" optional>
  Where in a request the gateway substitutes the placeholder. Default `['header']`. Only for a brokered secret.
</Property>

<Property name="to" type="'agent' | 'checks'" optional>
  `agent` (default) puts it in the container's environment. `checks` gives it only to `c.exec` and `setup` steps, never the agent.
</Property>

<Property name="optional" type="boolean" optional>
  When the host variable isn't set, leave the variable out instead of failing. The record lists it as unset.
</Property>

| Key | Hosts |
|---|---|
| `secret.hosts.gemini` | `generativelanguage.googleapis.com` |
| `secret.hosts.openai` | `api.openai.com` |
| `secret.hosts.anthropic` | `api.anthropic.com` |
| `secret.hosts.github` | `api.github.com`, `uploads.github.com` |

A secret without `hosts` reaches the sandbox as its real value, and the plan warns (or fails, with `secrets: { unbrokered: 'error' }` in the config). An `optional` secret whose host variable isn't set never reaches it, so it doesn't warn; `'error'` refuses it either way, so the config passes or fails the same on every machine:

```text
warning: SENTRY_DSN 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('SENTRY_DSN', { hosts: ['api.example.com'] }).
```

A `to: 'checks'` secret without `hosts` warns that it `reaches the checks (c.exec, setup steps) as its real value; the agent never gets it.`

A required secret that isn't set fails the attempt with `Secret NAME for <where> isn't set. Set it in the environment or .env.local.` Secrets in the config (and profile `defaults`) are checked before the run starts. See [Credential brokering](/concepts/credential-brokering).

## skill

Every skill source takes `placement`: `'harness'` (default) lets the harness adapter install it where its agent reads skills; `'workspace'` writes it into the repo's skill directory (`.cursor/skills`, `.claude/skills` or `.agents/skills`, depending on the harness).

<Property name="skill.local(path, options?)" type="SkillSource">
  A directory with a `SKILL.md`, or a directory of such directories. `path` is relative to the calling file, or an absolute path or `URL`.
</Property>

<Property name="skill.git(repo, options?)" type="SkillSource">
  A git repository: `https://…`, `git@…`, `github:owner/repo`, or a GitHub `/tree/<ref>/<path>` URL. `ref` is a branch, tag or SHA; `path` is the directory inside the repository. Without `path`, reeval reads the root when it has a `SKILL.md`, else the repository's `skills/` directory when it has one, as `skill.registry` does, else the root's subdirectories. From there it looks one level down only: each skill is a directory with its own `SKILL.md`.
</Property>

<Property name="skill.npm(spec, options?)" type="SkillSource">
  A pinned npm package, fetched with `npm pack` on the host from the registry npm is configured for (`.npmrc` or `npm_config_registry`). A local tarball or folder (`./vendor/skills-1.2.3.tgz`, `../pkg`, an absolute path, or `file:` with one of those) resolves against the file that calls `skill.npm`, as `file()` does. Skills are read from `path` inside it, default `skills`.
</Property>

<Property name="skill.registry(id, options?)" type="SkillSource">
  A skills.sh id, `owner/repo@skill`. reeval clones the GitHub repository and reads that one skill from its `skills/` directory when it has one. `version` is a git ref; the record keeps the commit it resolved to.
</Property>

<Property name="skill.inline(skill, options?)" type="SkillSource">
  A skill written in place: `name` (kebab-case), `description`, `content` (the body of `SKILL.md`) and optional `files`, keyed by path.
</Property>

Directory sources (`local`, `git`, `npm`) also take:

<Property name="only" type="string[]" optional>
  The skill names to take from a directory of skills. A name that isn't there fails each attempt as an infra error: `only: ["name"] not found in <source> (found: …)`.
</Property>

<Property name="exclude" type="string[]" optional>
  Paths to leave out of the sandbox, gitignore-style. It replaces `DEFAULT_SKILL_EXCLUDE`, so spread that to add to it, or pass `[]` to keep everything. `.git` is always left out. What's left out is recorded as the skill's `excluded`.
</Property>

`DEFAULT_SKILL_EXCLUDE` is `['/evals/', '.history/', '*.eval.ts']`, so a skill's own evals don't reach the agent.

A skill needs a `SKILL.md` with YAML frontmatter naming it (kebab-case) and describing it. A git skill on a moving ref, or a registry skill without a version, warns in the plan; the resolved commit is recorded either way:

```text
warning: skill.git(github:acme/skills) uses moving ref "main": results aren't comparable over time. Pin a tag or SHA (the resolved commit is recorded either way).
```

## plugin

```ts title="evals/env.ts"
export const review = defineEnvironment('review', ({ plugin }) => ({
  plugins: [
    plugin.local('./plugins/house-style'),
    plugin.git('github:acme/agent-plugins', {
      ref: 'v1.2.0',
      path: 'plugins/review',
    }),
    plugin.npm('@acme/review-plugin@1.2.0', { only: ['skills', 'mcp'] }),
    plugin.marketplace('lint@github:acme/agent-plugins', { ref: 'v3' }),
  ],
}))
```

<Warning>
Plugins are experimental. Which parts each harness loads, and how, may change in any release.
</Warning>

A plugin is a directory in Claude Code's layout: `.claude-plugin/plugin.json` with a kebab-case `name`, and any of these parts beside it.

| Part | Where |
|---|---|
| `skills` | `skills/<name>/SKILL.md`, read like a skill directory |
| `agents` | `agents/<name>.md` |
| `commands` | `commands/<name>.md` |
| `hooks` | `hooks/hooks.json` |
| `mcp` | `.mcp.json`, or the manifest's `mcpServers` (inline, or a path to a JSON file) |
| `rules` | `rules/*.md` and `rules/*.mdc` |

<Property name="plugin.local(path, options?)" type="PluginSource">
  A host directory, relative to the calling file, or an absolute path or `URL`. The plan reads it, so its refusals show in `--dry-run`.
</Property>

<Property name="plugin.git(repo, options?)" type="PluginSource">
  A git repository, parsed and cloned as for `skill.git`. `ref` is a branch, tag or SHA; `path` is the plugin's directory in it, default the root. Fetched on the host when an attempt starts; the record keeps the commit.
</Property>

<Property name="plugin.npm(spec, options?)" type="PluginSource">
  A pinned npm package, fetched as for `skill.npm`. `path` is the plugin's directory in it, default the package root.
</Property>

<Property name="plugin.marketplace('name@source', options?)" type="PluginSource">
  The entry `name` in a Claude Code marketplace: a git repository (a URL, `github:owner/repo` or a local directory, at `ref`) or an npm package (`npm:<spec>`) whose root holds `.claude-plugin/marketplace.json`. The marketplace and the entry's own source are fetched on the host when an attempt starts, with the same clone and `npm pack` caches as `plugin.git` and `plugin.npm`. The record keeps the marketplace's name, source and commit, and the entry's own source and commit.
</Property>

An entry's `source` is one of the forms Claude Code's marketplaces use:

| Entry `source` | Fetched as |
|---|---|
| `"./plugins/lint"`, `"."`, or a bare name such as `"lint"` under `metadata.pluginRoot` | A directory in the marketplace's own repository or package, which may not leave it |
| `{ "source": "github", "repo": "owner/repo", "ref"?, "sha"? }` | `plugin.git('github:owner/repo')`, at `sha` (a full 40-character commit SHA) or else `ref` |
| `{ "source": "url", "url", "ref"?, "sha"? }` | `plugin.git(url)`, for an `https://`, `ssh://`, `git://` or `git@host:path` URL without a token, or a `file://` URL from a local marketplace |
| `{ "source": "git-subdir", "url", "path", "ref"?, "sha"? }` | `plugin.git(url, { path })`; `url` may be `owner/repo` |
| `{ "source": "npm", "package", "version"? }` | `plugin.npm('package@version')`, for a registry package name; a custom `registry` is refused |

A marketplace in a local directory, a `file://` repository or a local tarball may name a `file://` repository. A remote one, fetched from a git host or the registry, may name only remote repositories and paths inside itself, so its `file://` URLs and host paths are refused:

```text
the acme marketplace (https://git.example.com/acme/plugins.git) names its review plugin at file:///srv/review, a path on this machine: a remote marketplace may only name remote repositories or paths inside itself
```

Only Claude Code's marketplace format is read: a Cursor listing goes in as `plugin.git` on the repository it points at. An entry that sets its parts' paths (`skills`, `commands`, `mcpServers` and so on) is refused, as a manifest that moves a part is. A source without `.claude-plugin/marketplace.json`, or an entry the marketplace doesn't list, fails the attempt as an infra error:

```text
environment typo (evals/typo.eval.ts › names an entry the marketplace lacks [fake/typo/fake#1]): plugin.marketplace('reviwe@file:///tmp/mkt/market'): the acme-tools marketplace (file:///tmp/mkt/market#v3) has no plugin "reviwe" (did you mean "review"?); it lists review, lint, format
```

Every source takes:

<Property name="only" type="Array<'skills' | 'agents' | 'commands' | 'hooks' | 'mcp' | 'rules'>" optional>
  The parts to place; default every part the plugin has. The files of the parts left out never reach the sandbox. A part the plugin doesn't have is refused.
</Property>

<Property name="build" type="string | string[] | { run: string | string[], cwd?: string }" optional>
  Commands that build the plugin in its own cached Docker image layer, on the harness image, as root, with no secrets. They get the whole source and run from the plugin's `path`, or from `cwd` relative to the source's root (`'.'` for the root, for a monorepo's workspace install). For `plugin.marketplace`, the source is the marketplace itself for a `./` entry, and otherwise the entry's own repository or package. Only `path` is placed afterwards, so the build's output belongs inside it; what it leaves elsewhere is dropped. The source's `.git` isn't there, so a build that needs git metadata gets it another way, such as a version in its commands. Docker only: on `fakeSandbox()` the plan refuses it. The record's `plugins[].build` is the layer's tag. See [Docker](/reference/sandbox/docker#images-and-caching).

```ts
plugin.git('github:acme/agent-plugins', {
  ref: 'v1.4.0',
  path: 'plugins/review',
  build: { run: ['pnpm install --frozen-lockfile', 'pnpm build'], cwd: '.' },
})
```
</Property>

Each plugin's MCP servers join the environment's `mcp` as `<plugin>:<server>`, so egress, brokering and the record treat them like any other server. A whole `${VAR}` env value is read as `secret.fromEnv('VAR')`, and `${VAR}` in a header as `secret.template`. `${CLAUDE_PLUGIN_ROOT}` is the plugin's directory in the sandbox, `~/.reeval/plugins/<name>`, and the only variable a command or its args may name. A git or npm plugin's http server needs its host in `egress.allow` under a restricted policy; a local plugin's is an automatic host.

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`; Codex, Goose and fx their `skills` and `mcp`. Claude Code, Antigravity, Grok Build and Codex start a plugin's MCP servers themselves, so one that reads a host variable is refused there, and each harness refuses a hook event it doesn't run. Cursor, OpenCode, pi, Goose and fx take a plugin's items flat, so two with one name clash. The plan refuses a part a harness doesn't load, naming it, and the fake harness takes every part. A server or hook that runs `npx`, `bunx`, `uvx` or `pipx run` fetches at start, so it's refused under an egress policy that doesn't allow the registry, and noted in the plan under `allow-all`:

```text
the fetcher plugin's MCP server "search" runs npx, which fetches from registry.npmjs.org at start; add a build that installs it to keep it off the network
```

## mcp

<Property name="mcp.stdio(command, args?, options?)" type="McpServer">
  A server the agent starts in the sandbox. `options.env` sets its variables, secrets included. When the sandbox has `node`, the agent's MCP config runs it through reeval's timer (`node /reeval/bin/mcp-timer.cjs` in Docker), which passes its stdio through and records each start as `turns[].mcpStartup`. A server that doesn't answer `initialize` within 60s fails the attempt.
</Property>

<Property name="mcp.http(url, options?)" type="McpServer">
  A Streamable HTTP server. `options.headers` values are strings, secrets or `secret.template(…)`.
</Property>

<Property name="mcp.sse(url, options?)" type="McpServer">
  A server-sent events server, with the same options as `mcp.http`. Cursor, Claude Code, Antigravity and fx get it as an SSE server; OpenCode, Goose, pi and Codex have no SSE transport, so a plan that gives any of them an `mcp.sse` server exits 2 before any attempt starts (`--dry-run` shows it).
</Property>

A secret in an HTTPS server's headers is brokered to that server's host unless it sets its own `hosts`, and the host is allowed through a restricted egress policy. The plan shows it:

```text
brokered: GEMINI_API_KEY → generativelanguage.googleapis.com; mcp.search.headers.Authorization → mcp.acme.dev
```

## file

<Property name="file(path)" type="HostFile">
  A file on the host, relative to the calling file (or an absolute path or `URL`), read when an attempt starts and copied byte for byte. Use it for `files` values and `rules`. A file its owner may execute on the host is made executable in the sandbox.
</Property>

## directory

```ts title="evals/env.ts"
export const notes = defineEnvironment('acme-notes', ({ directory }) => ({
  files: {
    ...directory('../fixture', {
      glob: ['src/**', 'package.json', '!**/*.test.ts'],
    }),
    'README.md': 'something',
  },
  setup: [{ run: 'npm ci --no-audit --no-fund', timeout: '5m' }],
}))
```

<Property name="directory(path, options?)" type="Record<string, HostFile>">
  Every file in a host directory, as `files` entries: each path relative to the directory maps to the same `file()` source. `path` is relative to the calling file, or an absolute path or `URL`. It's a plain object, so spread it next to other entries; a later key overrides an earlier one.
</Property>

<Property name="into" type="string" optional>
  A directory to place the files under, relative to the workspace or starting with `~/`: `into: 'app'` seeds `app/package.json`.
</Property>

<Property name="glob" type="string[]" optional>
  Ordered gitignore-style patterns over paths relative to the directory. A plain pattern includes and a `!pattern` excludes.
</Property>

<Property name="defaultIgnores" type="boolean" optional>
  `false` walks into the `DEFAULT_DIRECTORY_IGNORES` too. Default `true`. `.git` is always skipped.
</Property>

`glob` is evaluated per file, in order, and the last pattern that matches decides:

1. A file starts included when `glob` has no plain pattern, and excluded when it has one.
2. Each pattern that matches the file's path sets it: a plain one includes it, a `!` one excludes it. A later `src/gen/keep.ts` re-includes a file an earlier `!src/gen/` excluded.
3. The default ignores apply on top, whatever `glob` says. To seed an ignored directory, set `defaultIgnores: false` and exclude what you don't want with `!` patterns.

Patterns are matched with [picomatch](https://github.com/micromatch/picomatch), with gitignore's anchoring on top. Wildcards match dotfiles.

| Pattern | Matches |
|---|---|
| `*`, `?` | Within one name |
| `**` | Across directories |
| `[abc]`, `[!abc]` or `[^abc]` | One character in the set, or any other |
| `{md,txt}` | Either alternative |
| A `/` other than at the end | Anchored to the directory |
| No `/`, or only a trailing one | A name at any depth |
| A trailing `/` | A directory and everything in it |

Unlike gitignore, a `!` pattern still excludes a file whose directory an earlier plain pattern named: the last match always wins. A fixture's `.gitignore` isn't read, and it's seeded like any other file.

`DEFAULT_DIRECTORY_IGNORES`, exported from `{{ pkg }}`:

| For | Patterns |
|---|---|
| Any | `node_modules/`, `.DS_Store` |
| JavaScript | `dist/`, `.next/`, `.turbo/`, `coverage/`, `.cache/` |
| Python | `__pycache__/`, `.venv/`, `venv/`, `.pytest_cache/`, `.mypy_cache/`, `*.pyc` |
| Dart, Flutter, Gradle and Rust | `.dart_tool/`, `/build/`, `/target/`, `.gradle/` |
| Editors | `.idea/`, `.vscode/` |

`build/` and `target/` are skipped only at the top of the directory, where Flutter, Dart, Gradle and Cargo put their output, so a nested `src/build/` module still lands. `vendor/` isn't in the list: a committed Go or PHP `vendor/` is what an offline build needs.

The directory is walked when the callback runs, so the plan (`reeval run --dry-run --json`) lists each cell's `files`. Each file's content is read when an attempt starts and feeds the environment's digest, as with `file()`. Other details:

- **Symlinks** are followed, to a file or a directory. A dangling symlink, or one to a directory that contains it, fails with its path.
- **Empty directories** aren't seeded. Create one in a `setup` step, or add a placeholder file.
- **File modes:** only the executable bit is kept, as for `file()`; it's part of the digest. Windows hosts have no executable bit, so nothing is made executable from there.
- **No files** after the patterns and ignores is an error, not an empty `files`.

## git

```ts title="evals/env.ts"
export const app = defineEnvironment('app', ({ git }) => ({
  workspace: git('https://github.com/acme/app', {
    ref: 'v2.3.0',
    path: 'examples/basic',
    init: true,
    token: 'GITHUB_TOKEN',
  }),
}))
```

<Property name="git(repo, options?)" type="GitWorkspace">
  A git repository for an environment's `workspace`: `https://…`, `ssh://…`, `git@…`, `file://…`, `github:owner/repo`, a GitHub `/tree/<ref>/<path>` URL, or a local path (`./`, `../` or absolute), which resolves against the calling file as `file()` does.
</Property>

<Property name="ref" type="string" optional>
  A branch, tag or full commit SHA. Unset, it's the remote's default branch. It resolves to a commit once per run, before any attempt, so every attempt in a run starts from the same commit.
</Property>

<Property name="path" type="string" optional>
  A directory in the repository, relative, with forward slashes. Its contents become the workspace root. reeval fetches only that directory's files (a partial fetch), so a subdirectory of a large monorepo stays cheap.
</Property>

<Property name="init" type="boolean" optional>
  Gives the workspace a `.git`: a new repository with one commit of the fetched tree, on `main`, with no remote. The commit is by `reeval` at a fixed date, so every attempt starts at the same `HEAD`. `files` and `setup` run after it, so their changes show in `git status`. The image needs `git`, which reeval's images have.
</Property>

<Property name="token" type="string" optional>
  The name of a host variable holding a token for the fetch, such as `'GITHUB_TOKEN'`, read like `secret.fromEnv`. reeval sends it to the repository's own origin only, as an HTTP header in `git`'s environment, never its arguments; it never reaches the sandbox, the record or `.reeval`, and it's masked in errors. Only for `https://` (or `http://`) URLs. A variable that isn't set fails the workspace's attempts.
</Property>

Without `token`, reeval runs `git` on the host with your own git credentials (credential helpers, `gh auth setup-git`, SSH keys). Either way it copies only the tree into the sandbox, with no remote and no credentials.

A URL with a password or token in it is refused. Submodules and Git LFS files aren't fetched: a submodule is left as an empty directory and an LFS file as its pointer. A branch or no `ref` warns in the plan, as for `skill.git`:

```text
warning: workspace git(github:acme/template) uses no ref (the default branch): results aren't comparable over time. Pin a tag or SHA (the resolved commit is recorded either way).
```

## Errors

| Mistake | Message |
|---|---|
| An empty `hosts` | `secret(): hosts must list the hosts the secret is sent to, e.g. hosts: ['api.example.com'] or secret.hosts.gemini (got [])` |
| `in` without `hosts` | `secret(): in only applies to a brokered secret; add hosts, e.g. { hosts: ['api.example.com'], in: ["query"] }` |
| A bad `in` | `secret(): in lists where the gateway substitutes: 'header', 'query' and/or 'body' (got ["cookie"])` |
| A bad `to` | `secret(): to is 'agent' or 'checks' (got "host")` |
| A bad variable name | `secret.fromEnv("my-var") needs a host variable name: letters, digits and _, not starting with a digit, e.g. secret.fromEnv('API_KEY')` |
| A bad placement | `skill({ placement }) is 'harness' or 'workspace' (got "home")` |
| `exclude` not a list | `skill({ exclude }) must be a list of paths or globs, e.g. ['/evals/', '*.eval.ts'] (got "/evals/")` |
| A bad registry id | `skill.registry("genkit") needs an id like owner/repo@skill, e.g. skill.registry('acme/skills@review')` |
| No repository | `skill.git() needs a repository URL, e.g. skill.git('https://github.com/acme/skills') (got "")` |
| A bad `only` | `plugin.local("./plugins/review"): only lists the parts to place, from 'skills', 'agents', 'commands', 'hooks', 'mcp', 'rules', e.g. only: ['skills', 'mcp'] (got ["apps"])` |
| An unknown plugin option | `plugin.git("github:acme/agent-plugins"): unknown option "refs"; it takes only, build, ref and path. Did you mean "ref"?` |
| A marketplace plugin without its name | `plugin.marketplace() needs 'name@source': the plugin's entry and the marketplace's git repository or npm:<package>, e.g. plugin.marketplace('lint@github:acme/agent-plugins') (got "github:acme/agent-plugins")` |
| `ref` on an npm marketplace | `plugin.marketplace("lint@npm:@acme/plugins@2.0.0"): ref picks a commit of a git marketplace; pin an npm marketplace in its spec, e.g. npm:@acme/plugins@2.0.0` |
| A bad `build` | `plugin.npm("@acme/review-plugin@1.2.0"): build is a shell command, a list of them, or { run, cwd } (got 3)` |
| An unknown `build` key | `plugin.npm("@acme/review-plugin@1.2.0"): build takes { run, cwd }, not "dir"` |
| A bad `build.cwd` | `plugin.git("github:acme/agent-plugins"): build.cwd is a directory relative to the source's root, with forward slashes and no .., or '.' for the root (got "../x")` |
| No plugin repository | `plugin.git() needs a repository: a URL or github:owner/repo, e.g. plugin.git('github:acme/agent-plugins', { ref: 'v1.2.0' }) (got "")` |
| No command | `mcp.stdio() needs a command, e.g. mcp.stdio('npx', ['-y', 'server']) (got "")` |
| A token in the URL | `git("https://***:***@github.com/acme/app"): the URL has a password or token in it. reeval fetches the repository on the host with your own git credentials (a credential helper, gh auth setup-git or an SSH key), and the sandbox never gets them; remove it from the URL.` |
| A bad `path` | `git("github:acme/app"): path is a directory in the repository, relative, with forward slashes and no . or .. (got "../secrets")` |
| The token itself as `token` | `git("github:acme/app"): token names the host variable that holds the token, e.g. token: 'GITHUB_TOKEN'. Don't put the token itself in the eval.` (the value isn't echoed) |
| `token` on an SSH or local repository | `git("git@github.com:acme/app.git"): token only works with an https:// URL; for SSH or a local repository, git uses this machine's own credentials` |
| An unknown option | `git("github:acme/app"): unknown option "branch"; it takes ref, path, init and token. Use ref for a branch, tag or commit.` |
| A missing directory | `directory("../fixture"): /path/to/fixture doesn't exist` |
| A file for `directory()` | `directory("./notes.md"): /path/to/notes.md is a file, not a directory. Use file("./notes.md") for one file.` |
| No files left | `directory("../fixture"): no files in /path/to/fixture match (glob ["*.md"], after the default ignores such as node_modules/ and dist/; defaultIgnores: false keeps them). Empty directories aren't seeded; create one with a setup step.` |
| `ignore` for `directory()` | `directory("../fixture"): unknown option "ignore"; it takes into, glob and defaultIgnores. Exclude with a '!' pattern in glob, e.g. glob: ['!dist/'].` |
| A bad `into` | `directory("../fixture"): into is a directory relative to the workspace (or ~/…), with forward slashes and no .. (got "../app")` |
| A broken symlink | `directory("../fixture"): bin/tool is a symlink to something that doesn't exist. Remove it, or leave it out with glob: ['!bin/tool'].` |

## Related

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