---
title: claudeCode()
description: >-
  The Claude Code harness, its options and id, the key it needs, and the
  endpoints reeval brokers it on.
---
> ## 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.

`claudeCode()` runs the Claude Code CLI in the Docker sandbox. `model` is required.

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

export default defineConfig({
  harness: [
    cursor({ model: 'composer-2.5' }),
    claudeCode({ model: 'claude-sonnet-5' }),
  ],
})
```

## Options

<Property name="model" type="string">
  A model id or alias the `claude` CLI accepts, such as `claude-sonnet-5` or `sonnet`. Required, since the CLI's default changes under you.
</Property>

<Property name="effort" type="'low' | 'medium' | 'high' | 'xhigh' | 'max'" optional>
  Reasoning effort the Agent SDK `/effort` control takes: `low`, `medium`, `high`, `xhigh` (Extra in the UI) or `max`. Adaptive thinking stays on. A value outside that set is refused. It isn't the measured [effort](/concepts/lift#effort-next-to-lift).
</Property>

<Property name="id" type="string" optional>
  The harness's id in cell keys, filters and the record. Default `claude-code:<model>`, or `claude-code:<model>[effort=<value>]` when `effort` is set.
</Property>

<Property name="gateway" type="'direct' | Gateway" optional>
  The gateway this harness's model calls go through, by name or helper, such as `direct()`. It skips the config's `gateway` fallback: the plan refuses a gateway that isn't listed or can't serve this harness. See [Gateways](/reference/gateway#pinning-a-harness).
</Property>

<Property name="maxConcurrent" type="number" optional>
  The most Claude Code attempts at once across the run, for example for your account's rate limits.
</Property>

<Property name="startTimeout" type="Duration" optional>
  How long its session may take to become ready (bootstrap and session start) before the attempt is an infra error, such as `'3m'`. Default `'90s'`. A start that times out is aborted and started once more; a second timeout fails the attempt with `claude-code: session did not become ready within 90s (tried twice)`, and the record's `startRetries` notes a retried start.
</Property>

## At a glance

| | |
|---|---|
| Import | `'{{ pkg }}/harness'` |
| Credential | `ANTHROPIC_API_KEY` |
| Adapter | `@ai-sdk/harness-claude-code` 1.0.160, with a fix reeval patches in (below) |
| Sandbox | `docker()` only |
| Image | `reeval-sandbox:claude-code-1.0.160-patch.b17cde50` |
| Rules file | `CLAUDE.md` |
| Skills (default placement) | `~/.claude/skills` |
| Skills (`placement: 'workspace'`) | `.claude/skills` in the workspace |
| Egress hosts added | `api.anthropic.com`, `*.anthropic.com` |

Build its image once with `reeval image build` in a project whose config uses `claudeCode()`.

reeval ships the adapter with one fix, which its image bakes: 1.0.160 never reports the served model, because its bridge starts the stream on the first message, and the `command_lifecycle` message and a plugin's `SessionStart` hook messages come before `system/init`. With the fix, each turn records the model Claude Code used. The tag's `patch.b17cde50` names that patch.

## The key

| Method | Path on `api.anthropic.com` |
|---|---|
| `POST` | `/v1/messages` |
| `POST` | `/v1/messages/count_tokens` |
| `GET` | `/api/claude_code/settings` |
| `GET` | `/api/claude_code/policy_limits` |

The harness authenticates only with `ANTHROPIC_API_KEY`, passed to the adapter explicitly. A host `AI_GATEWAY_API_KEY`, a Claude subscription login or a keychain entry is never used, so a run can't quietly bill a different account. The one exception is a [`gateway` list](/reference/gateway#vercel-ai-gateway) that resolves the harness to `vercel()`: then its key is `AI_GATEWAY_API_KEY` (else `VERCEL_OIDC_TOKEN`), the CLI's `ANTHROPIC_BASE_URL` is Vercel's host, and only `POST /v1/messages` and `POST /v1/messages/count_tokens` are brokered, on `ai-gateway.vercel.sh`. On that base URL the CLI makes none of its `/api/claude_code/*` calls.

On a gateway list that resolves it to [`openrouter()`](/reference/gateway#openrouter), the key is `OPENROUTER_API_KEY`, sent as `ANTHROPIC_AUTH_TOKEN` (a Bearer token) with `ANTHROPIC_BASE_URL` at `https://openrouter.ai/api`. Only `POST /api/v1/messages` and `POST /api/v1/messages/count_tokens` are brokered, on `openrouter.ai`, and the model runs as OpenRouter's id, `anthropic/` prefixed and dotted (`anthropic/claude-haiku-4.5` for `claude-haiku-4-5`). Each call records the upstream provider OpenRouter says served it, and its cost. `effort` isn't taken there yet, so a cell with it falls through to the next gateway.

The key never enters the container. The gateway swaps it in on the endpoints above, the ones CLI 2.1.281 sends it on, and nowhere else.

A request carrying the placeholder anywhere else gets a 403 and a `warning: the egress gateway refused …` line, so a CLI update that needs a new endpoint fails loudly.

## Environment

reeval starts the CLI with two variables:

| Variable | Why |
|---|---|
| `IS_SANDBOX=1` | The CLI refuses to skip permission prompts as root otherwise. |
| `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` | No telemetry, error reporting or feature-flag fetches, so flags can't change between runs. |

The agent runs with every permission granted, inside the sandbox.

Its `AskUserQuestion` tool is off, because it waits for a person to answer. Claude Code asks in its reply instead, and the eval's next `c.send()` answers it.

## Skills, rules and MCP

| What | Where |
|---|---|
| Rules | `CLAUDE.md` in the workspace |
| Skills | `~/.claude/skills`, or `.claude/skills` in the workspace with `placement: 'workspace'` |
| MCP servers | Passed to the CLI in the `.mcp.json` shape |
| Plugins | Every part but `rules`, enabled from `~/.claude/settings.json` (below) |

## Plugins

```json title="~/.claude/settings.json"
{
  "extraKnownMarketplaces": {
    "reeval": {
      "source": { "source": "directory", "path": "/home/reeval/.reeval/plugins" }
    }
  },
  "enabledPlugins": { "review@reeval": true }
}
```

reeval copies each plugin to `~/.reeval/plugins/<name>/`, lists them in a `reeval` marketplace there (`.claude-plugin/marketplace.json`) and enables them in Claude Code's settings. When the environment's `files` already write `~/.claude/settings.json`, reeval merges these keys into it. Claude Code loads each whole, namespaced: the subagent `review:reviewer`, the MCP tool `mcp__plugin_review_docs__<tool>`.

Claude Code starts a plugin's MCP servers itself, so reeval leaves them out of the config it passes. reeval rewrites the placed plugin's `.mcp.json` to run each through its MCP timer, so each start is in `turns[].mcpStartup` as `<plugin>:<server>`. That file can't hold a host secret, so a plugin server that reads one is refused at plan time; declare that server in `mcp`.

A plugin's `rules/` aren't read by Claude Code, so the plan refuses a plugin that keeps them:

```text
reeval: evals/review.eval.ts: evaluate("reviews the diff") on claude-code:claude-sonnet-5: Claude Code can't load the review plugin's rules (rules/): use only: ['skills', 'mcp'], or leave the plugin out of this cell
```

## Tokens

| The CLI reports, per model call | The turn records |
|---|---|
| Input tokens | `input` |
| Cache reads | `cacheRead` |
| Cache creation | `cacheWrite` |
| Output tokens | `output` |

The line under the result blocks totals them, and the record keeps them per turn, per attempt and for the run. See [Transcripts](/reference/transcript#tokens-and-the-served-model).

## Errors

| Situation | Message |
|---|---|
| No `model` | `claudeCode() needs a model, e.g. claudeCode({ model: 'claude-sonnet-5' })` |
| No key when the attempt starts | `ANTHROPIC_API_KEY isn't set, so the Claude Code harness can't authenticate. Set it in the environment or .env.local.` |
| A non-Docker sandbox | `The Claude Code harness needs the Docker sandbox: set sandbox: docker() …` |
| The CLI doesn't start | `Claude Code harness failed to start: …` |
| `files` write `~/.claude/settings.json` as something other than a JSON object, with plugins | `~/.claude/settings.json isn't a JSON object, so reeval can't enable the session's plugins in it: write it as one in files, or leave it out` |

A turn that the API rejects (no credit, for example) ends with `status: 'error'`, and the eval's next check fails. The turn line, the eval's result block and `summary.md` show the API's message, without `-v`. Scripted on the fake harness, the same failure prints:

```text
[creates#1 fake]  +0:00 turn 1 error: Credit balance is too low to access the Anthropic API 0ms · 0 tool calls, 0 file edits
…
fail             greet module › creates a tested greet module
  toHaveFile greet.js  failed
    expected file greet.js to exist
  turn error           Credit balance is too low to access the Anthropic API
```

## Related

- [Harnesses guide](/guide/harnesses)
- [cursor()](/reference/harness/cursor)
- [Credential brokering](/concepts/credential-brokering)
- [docker()](/reference/sandbox/docker)
