---
title: Your first real run
description: >-
  Run the quickstart's evals on Cursor in a Docker sandbox, with your keys kept
  on the host.
---
> ## 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.

The quickstart ran one deterministic eval on Cursor. Here you look closer at a real run: Cursor does the task in a fresh Docker container, and a real judge grades it.

**Before you begin:** Node {{ node }} or later, a running Docker, a Cursor API key and a key for your judge.

<Info>
  reeval's sandboxes are Linux containers. On Windows, use Docker Desktop on the WSL 2 backend, running Linux containers (not Windows containers). `reeval doctor` checks both and names the fix.
</Info>

## Point the config at Cursor

```ts title="reeval.config.ts"
import { defineConfig } from '{{ pkg }}'
import { cursor } from '{{ pkg }}/harness'
import { docker } from '{{ pkg }}/sandbox'

export default defineConfig({
  include: ['evals/**/*.eval.ts'],
  harness: cursor({ model: 'composer-2.5' }),
  sandbox: docker(),
  judge: { model: 'google/gemini-3.8-flash', threshold: 0.8 },
  timeout: '15m',
})
```

The [harness](/reference/harness/cursor) is the agent, and the [sandbox](/reference/sandbox/docker) is where it runs. `docker()`, the default, uses reeval's own image: Node 22, git and Cursor's agent, which reeval drives through the Cursor TypeScript SDK. `model` is required.

## Write the evals

These are the `greet` evals the rest of the Guide builds on:

```ts title="evals/hello.eval.ts"
import { evaluate, expect, group } from '{{ pkg }}'

const callGreetWithNoArgs = `node -e "import('./greet.js').then((m) => {
  const greet = m.greet ?? m.default?.greet ?? m.default
  process.exit(greet() === 'Hello, world!' ? 0 : 1)
})"`

group('greet module', () => {
  evaluate('creates a tested greet module', async (c) => {
    const turn = await c.send(
      'Create greet.js exporting greet(name) that returns "Hello, <name>!", ' +
        'and greet.test.js that tests it with node:test. No dependencies.',
    )

    await expect(turn).toHaveFile('greet.js')
    await expect(c.exec('node --test')).toSucceed()
    expect(
      await c.judge(
        "Does the agent's final message accurately describe what it changed?",
      ),
    ).toPass()
  })

  evaluate('adds a default name in a follow-up turn', async (c) => {
    await c.send(
      'Create greet.js exporting greet(name) that returns "Hello, <name>!". ' +
        'No dependencies.',
    )
    await c.send(
      'Make name default to "world" ' +
        'when greet() is called without arguments.',
    )

    await expect(c.exec(callGreetWithNoArgs)).toSucceed()
    expect(
      await c.judge(
        'Did the agent add the default without breaking greet(name)?',
      ),
    ).toPass()
  })
})
```

[Evals and checks](/guide/writing-evals) explains each line.

## Build the sandbox image

```bash
npx reeval image build
```

```text
Building reeval-sandbox:cursor-0.1.6
Building reeval-sandbox-base:latest
Installing the Cursor harness bootstrap
Cursor @cursor/sdk 1.0.40
✓ docker · cursor: reeval-sandbox:cursor-0.1.6 054cc8b21b68
1 image ready
```

reeval bakes the agent into its image once, tagged with the harness adapter's version (`reeval-sandbox:cursor-0.1.6`). Rebuild after you upgrade reeval.

## Add your keys

```bash title=".env.local"
CURSOR_API_KEY=...
GOOGLE_GENERATIVE_AI_API_KEY=...
```

`reeval run` reads `.env.local` and `.env` from the directory that holds `reeval.config.ts`, wherever you run it from. The judge's and `analysis.model`'s keys depend on the model's prefix: `google/*` uses `GOOGLE_GENERATIVE_AI_API_KEY`, `anthropic/*` uses `ANTHROPIC_API_KEY`, `openai/*` uses `OPENAI_API_KEY` and `typesafe-ai/jev-1.14.0` uses `TYPESAFE_AI_API_KEY`. reeval uses no gateway unless you configure one: `AI_GATEWAY_API_KEY` is read only when the config lists [`vercel()` in `gateway`](/reference/gateway#the-judge-analysis-and-captures), and then for any prefix, with a judge there limited to decision models such as Jev.

<Warning>
  Put `.env.local` next to `reeval.config.ts`. A key in another directory, the current one included, isn't read.
</Warning>

## Your key stays on the host

```text
[agent#1 cursor]  17:40:47 broker    CURSOR_API_KEY stays on the host · the gateway swaps it in on POST api2.cursor.sh/auth/exchange_user_api_key
```

`reeval run -v` prints that line for every attempt. `CURSOR_API_KEY` never enters the container: the agent sees a placeholder, and reeval's egress gateway swaps in the real key only on the request that exchanges it with Cursor.

A check that prints `env` in the sandbox finds `aisdkhc_…`, not your key. [Credential brokering](/concepts/credential-brokering) shows how.

## Run it

```bash
npx reeval run hello --log-order grouped --output-logs full
```

```text
plan  run … · 2 evals · 2 cells · 2 attempts · ~6m 00s serial (~3m 00s at concurrency 4)
reeval 0.10.0 · run … · 1 eval file · harness cursor:composer-2.5 · sandbox docker · concurrency 4 · grouped logs
[creates#1 cursor] ▶ evals/hello.eval.ts › greet module › creates a tested greet module
[creates#1 cursor]  +0:00 sandbox ready 2s
[creates#1 cursor]  +0:01 turn 1 done 6s · 1 tool call, 2 file edits
[creates#1 cursor]  +0:07 $ node --test  exit 0 159ms
[creates#1 cursor]  +0:08 judge  pass 1.00 (≥ 0.8)  Does the agent's final message accurately describe what it changed?
[adds#1 cursor] ▶ evals/hello.eval.ts › greet module › adds a default name in a follow-up turn
[adds#1 cursor]  +0:00 sandbox ready 2s
[adds#1 cursor]  +0:01 turn 1 done 8s · 3 tool calls, 1 file edit
[adds#1 cursor]  +0:10 turn 2 done 4s · 1 tool call, 1 file edit
[adds#1 cursor]  +0:14 $ node -e "import('./greet.js').then((m) => {…  exit 0 103ms
[adds#1 cursor]  +0:14 judge  pass 1.00 (≥ 0.8)  Did the agent add the default without breaking greet(name)?

pass             greet module › creates a tested greet module
pass             greet module › adds a default name in a follow-up turn

17s · 132k in (49k cached) · 1,489 out · results .reeval/runs/…/summary.md
2 pass
Set analysis.model in reeval.config.ts for answers to what each eval asks.
```

`hello` is a filter: it matches the file `evals/hello.eval.ts`. Each eval ran as one [attempt](/concepts/attempt-lifecycle) in its own container, side by side. With `--log-order grouped`, each attempt prints as one block when it ends: the sandbox, each turn, each check and each judge verdict (`pass 1.00 (≥ 0.8)`: its score against the threshold).

Then comes a block per eval: a pass prints its name only. The line under the blocks has the run's time and the tokens Cursor reported (input, the cached part and output); reeval records them per turn and per attempt but prices no models, so there's no cost. Last are the tally and a hint: this config sets no `analysis.model`, which a config from `reeval init` sets so a model answers what each eval asks ([Insights and remarks](/concepts/insights)).

On a terminal, the same run is in colour:

<Image src="/assets/cli/run-tty.png" alt="A colour terminal showing a reeval run with two passing evals and their result blocks" width={800} />

## Find the run

```text
.reeval/runs/
  20260926T174005Z_b010670_ada-lovelace_xdb9.json    the record
  20260926T174005Z_b010670_ada-lovelace_xdb9/
    summary.md                                     the run as Markdown
    <eval>/<cell>/1/                               transcript, diff, checks, judge input
```

Every `reeval run` writes a scratch run under `.reeval/runs/`. The name is the UTC start time, the commit, the author and a random suffix; the plan line prints it, and `.reeval/current` points at it so `reeval run --resume` can continue after a crash. [History](/guide/history) shows how to keep runs in git.

## Run from any directory

```bash
cd src/lib
npx reeval run --dry-run
```

```text
plan  config ../../reeval.config.ts · …
```

`reeval` uses the nearest `reeval.config.ts` in the current directory or above, as Vitest and ESLint find their configs, and the plan line names it. Every path is the config's: the evals, `.env.local` and `.reeval/runs/` stay at the project root, for every command. A nested project with its own config uses that one; [Loading](/reference/config#loading) has the rules.

## Ignore local files

```text title=".gitignore"
.reeval/
.env*
```

`.reeval/` holds scratch runs, caches and debug data. `.env*` holds your keys.

A real agent passed your evals in Docker, and your key never left the host.

## Next

<CardGroup cols={2}>
  <Card title="Evals and checks" icon="flask" href="/guide/writing-evals">
    Prompts, commands, matchers and the judge.
  </Card>
  <Card title="Credential brokering" icon="lock" href="/concepts/credential-brokering">
    How the agent works without seeing its key.
  </Card>
</CardGroup>
