Docker Isolation
Run each evaluation task inside its own fresh container. Strong host isolation and a pinned, reproducible agent runtime.
When to use
Section titled “When to use”Set sandbox.driver: docker on a task (or pass --driver docker on the CLI —
a thin alias for -D sandbox.driver=docker) when you want:
- Isolation from the host filesystem/network — agent-generated code can’t reach files outside the sandbox.
- A pinned toolchain — the image bakes in Python 3.13, Node 22 LTS,
@anthropic-ai/claude-code,uv, and the matchingcoder_evalversion, so results don’t drift with host upgrades.
Aggregation (P/R/F1, suite thresholds, reports) always stays on the host. Each container is a sealed “run one task → emit one task.json” worker.
One-time setup
Section titled “One-time setup”make docker-image # core + both built-in agents (default; no credentials)# opt in to the UiPath extra (resolves from public PyPI; no credentials):make docker-image-fullBoth build coder-eval-agent:<pkg-version> and tag it :latest.
make docker-imageinstalls the core package plus both built-in agents — claude-code (baked above) and Codex (--extra codex, public PyPI). It needs no credentials and covers the common case: claude-code or Codex tasks scored withrun_command/file_contains(incl. converted skillsbench tasks).llm_judge/agent_judgework here too (they route through the run’s Anthropic/Bedrock backend).make docker-image-fulladditionally installs theuipathextra. TheuipathSDK resolves from public PyPI (peruv.lock), so the build needs no credentials. Use this only for tasks that shell out to the in-hostuipathCLI. (Codex is already in the default image — no extra needed.)
Codex sandbox under Docker. Codex’s Landlock-backed
read-only/workspace-writesandboxes can’t initialize inside the eval container — their writes/execs fail silently and the agent produces no artifacts (ascore=0FAILURE with no loud error). The docker runner setsCODER_EVAL_IN_CONTAINER=1, and the Codex agent honors it by falling back tofull-access: the container itself is the trust boundary. Host runs (tempdir) are unaffected — Landlock works there and the marker is unset. So Codex tasks run under--driver dockerwith their naturalacceptEditspermission mode; no need to setbypassPermissionsby hand.
Running a task in Docker
Section titled “Running a task in Docker”# Single taskcoder-eval run path/to/task.yaml --driver docker
# All taskscoder-eval run --driver docker
# Or in the task YAMLsandbox: driver: docker docker: network: bridge # or "none" for sealed runs image: my-custom:tag # override the default imageUsing a pre-built custom image
Section titled “Using a pre-built custom image”When your tasks need extra tools or dependencies, extend the framework image once and point tasks at
the result — no per-task build. The custom image must extend coder-eval-agent:<version> so it
inherits the coder-eval runtime, the entrypoint script, and the org.coder-eval.version label the
host’s preflight check reads. (For a task whose Dockerfile can’t be rebased onto Debian, use
the runtime kit
instead. To have coder-eval build the image per task rather than pre-building it, see
Building the image from a task Dockerfile.)
FROM coder-eval-agent:<version> # match the version your host runsRUN apt-get update && apt-get install -y --no-install-recommends custom-tool \ && rm -rf /var/lib/apt/lists/*docker build -t my-team/image:latest .Select it either in the task YAML:
sandbox: driver: docker docker: image: my-team/image:latest…or from the CLI, which overrides whatever the YAML says:
coder-eval run task.yaml -D sandbox.docker.image=my-team/image:latestThe default when you set nothing is coder-eval-agent:<installed package version>.
A worked example ships in-tree: tasks/byod_smoke_test.yaml runs against
templates/byod_smoke_test/Dockerfile, which extends the framework image and drops a marker file
that the task’s success criterion then asserts — proving the custom image was actually used. (The
byod_* names here mean “Bring Your Own Docker”; they are unrelated to the
Bring Your Own Dataset guide, which is about fanning one task out over data rows.)
make docker-image # base image firstdocker build -t byod-custom-image:0.1.0 templates/byod_smoke_test/ # then the derived onecoder-eval run tasks/byod_smoke_test.yamlTroubleshooting custom images
Section titled “Troubleshooting custom images”| Symptom | Cause and fix |
|---|---|
docker: Error response from daemon: pull access denied | The image isn’t built locally and isn’t pullable. Check docker images, then rebuild it. Docker treats an unknown local tag as a remote reference, which is why the error mentions a pull. |
Image <your-image> coder_eval <a> != host <b> | The custom image carries an org.coder-eval.version label inherited from a stale framework base. Rebuild the base with make docker-image, then rebuild your derived image with docker build --no-cache. |
Image <your-image> has no org.coder-eval.version label | The image doesn’t descend from coder-eval-agent (or predates the label). Rebase it on the framework image, or use the runtime kit. |
Building the image from a task Dockerfile
Section titled “Building the image from a task Dockerfile”Instead of pointing at a pre-built image, a task can ship its own Dockerfile
and have coder-eval build it before the run:
sandbox: driver: docker docker: dockerfile_path: ./environment/Dockerfile # relative to the task YAML⚠️ Contract: a task Dockerfile MUST either start with
FROM coder-eval-agent:<version>or use the runtime kit (see below). The container runs the coder-eval orchestrator (coder-eval _run-task-internal) via the framework image’sENTRYPOINT. A task Dockerfile extends that image and adds only task-specific layers — extraaptpackages,COPY-ed inputs, etc.Build the framework base first —
make docker-image(tags bothcoder-eval-agent:<version>andcoder-eval-agent:latest).
Tasks that bring their own base image: the runtime kit (coder-eval-runtime)
Section titled “Tasks that bring their own base image: the runtime kit (coder-eval-runtime)”The FROM coder-eval-agent contract above means a task is rebased onto the
Debian framework image. That breaks tasks whose Dockerfile was written for a
different base image (e.g. a Fedora recipe using dnf, which doesn’t exist on Debian). To keep the task’s own base image and build successfully, coder-eval’s runtime need to be copied into the task’s image. Use make coder-eval-runtime first to make the runtime available for copying.
FROM fedora:41 # the task's own base, kept verbatimRUN dnf -y install ... # the task's native recipe, runs on its own OS# --- copy coder-eval runtime into a task image ---COPY --from=coder-eval-runtime:latest /opt/coder-eval /opt/coder-evalCOPY --from=coder-eval-runtime:latest /usr/local/bin/coder_eval_entrypoint.sh /usr/local/bin/coder_eval_entrypoint.shLABEL org.coder-eval.version="<ver>"make coder-eval-runtime builds the kit (docker/Dockerfile.runtime): a
standalone CPython + Node + the coder-eval CLI + Claude Code, all under
/opt/coder-eval, plus the entrypoint at the same /usr/local/bin/... path
the host pins. The kit is glibc-only — it runs on debian/ubuntu/fedora/rhel/…
but not musl/Alpine.
Both base images are independent and persistent — build each once and run any mix of rebase
and inject tasks without rebuilding. To build both in one shot (no credentials needed), use
make docker-images (= make docker-image + make coder-eval-runtime); reach for the
individual targets when you only need one.
The kit installs the no-credential set only (core + codex), like
make docker-image— it never installs the[uipath]extra, so there is nomake docker-images-full. An inject-mode task that needs the LLMGW/uipathjudge isn’t supported by the kit as built.
# environment/DockerfileFROM coder-eval-agent:latest # inherit runtime + entrypointRUN apt-get update && apt-get install -y --no-install-recommends poppler-utilsRUN pip install --no-cache-dir PyMuPDF==1.24.10COPY input/ /root/input/Behavior:
- Path resolution —
dockerfile_pathis resolved relative to the task YAML’s directory at load time (with$VAR/${VAR}expansion). A missing file fails fast at load, not mid-run. - Entrypoint check — after building, coder-eval inspects the image’s
ENTRYPOINTand aborts with aFROM coder-eval-agenthint if the runtime wasn’t inherited. - Overrides
image— whendockerfile_pathis set, it takes precedence over anyimagevalue. - Build context — the build context is the Dockerfile’s parent directory,
so relative
COPY ./input/...instructions resolve naturally. In the layout above,environment/is the context. - Caching — the image is tagged deterministically as
coder-eval-task-<task_id>:built, so repeat runs of the same task reuse Docker’s layer cache. Edit the Dockerfile and the next run rebuilds the changed layers only. - Version-label check skipped — the
org.coder-eval.versionpreflight only applies to the framework image; task-built images don’t carry it and won’t warn.
A build failure aborts the task with a DockerBuildError (a DockerRunError
subclass) carrying docker build’s output. Because the build runs before the
run dir, docker.log, or task.json exist, the runner explicitly records the
failure so it is never a silent empty result dir: it creates the run dir, writes
the full build log to docker.log, and writes a synthetic task.json with
final_status: BUILD_FAILED (an error-category status) before re-raising. So
a failed build shows up per-task on the dashboard with its build log, exactly
where you’d look for container output.
Customizing the build (docker.build)
Section titled “Customizing the build (docker.build)”The docker build invocation is configurable via sandbox.docker.build:
sandbox: driver: docker docker: dockerfile_path: ./environment/Dockerfile build: args: # -> --build-arg KEY=VALUE PKG_VERSION: "1.2.3" TOKEN: "${HOST_TOKEN}" # values are $VAR / ${VAR} expanded from the host env secrets: # -> --secret <spec> (requires BuildKit) - id=mytoken,env=MY_TOKEN # forward a host env var as a build secret - id=npmrc,src=~/.npmrc # or a file extra_args: ["--target", "runtime"] # escape hatch for any other docker build flag buildkit: true # optional: force DOCKER_BUILDKIT (see below)args→--build-arg KEY=VALUE. Values are environment-expanded against the host. Prefersecretsfor credentials — build-args are recorded in the image history.secrets→--secret <spec>. Useid=NAME,env=VARto forward a host env var orid=NAME,src=PATHfor a file; reference it in the Dockerfile viaRUN --mount=type=secret,id=NAME .... Secrets are exposed only to the mounting RUN step and never baked into layers. Secrets require BuildKit.extra_args→ raw flags inserted before the build context (e.g.--target,--network,--platform). Escape hatch for options without a dedicated field.buildkit→ controls theDOCKER_BUILDKITenv var. Omitted (default), coder-eval inherits the invoker’s environment — setDOCKER_BUILDKIT=1before running coder-eval to enable it globally. Setbuildkit: true/falseto force it per task. Ifsecretsare configured but BuildKit isn’t enabled, coder-eval logs a warning (the build would otherwise fail).
The build context is always appended last, so extra_args can’t displace it.
Authentication
Section titled “Authentication”macOS users — read this first. Claude Code’s OAuth tokens live in the macOS Keychain. The container has no path to the Keychain, so the bundled CLI inside will return
Not logged in · Please run /loginand every task will fail at iteration 1. Before running--driver docker, set one of these on the host:
ANTHROPIC_API_KEY=...(direct Anthropic), orCLAUDE_CODE_USE_BEDROCK=1+AWS_BEARER_TOKEN_BEDROCK=...+AWS_REGION=...(Bedrock).Linux hosts where Claude Code stores creds under
~/.claudealready work because that directory is bind-mounted into the container.
Credentials are forwarded via --env VAR (name-only, never embedded in argv) for these vars when set on the host: ANTHROPIC_API_KEY, API_BACKEND, UIPATH_*, AWS_BEARER_TOKEN_BEDROCK, AWS_REGION, CLAUDE_CODE_USE_BEDROCK, ANTHROPIC_MODEL.
To add one or two custom vars to the defaults (recommended), use env_passthrough_extra:
sandbox: driver: docker docker: env_passthrough_extra: ["MY_CUSTOM_TOKEN", "DEBUG_FLAG"] # Keeps all defaults + theseTo completely replace the list, use env_passthrough:
sandbox: driver: docker docker: env_passthrough: ["MY_CUSTOM_TOKEN", "ANTHROPIC_API_KEY"]HOME is forwarded by default
Section titled “HOME is forwarded by default”The default env_passthrough includes HOME so the in-container ~/.claude lookup resolves at the same path as on the host (the mount lands at $HOME/.claude symmetrically). Practical contract:
Path.home()inside the container returns the host’sHOMEvalue (e.g./Users/youon macOS). The directory exists in the container because Docker auto-creates it as the mount parent for~/.claude.~/.claudeis not the host’s real dir — the runner makes a throwaway lean copy in a tmp dir per task and mounts that copy read-write at$HOME/.claude. The copy keeps the small set the container needs (auth via.credentials.json,settings.json,plugins/) and drops heavy or transient per-session state —security/(often hundreds of MB),projects/,cache/,file-history/,backups/,downloads/,sessions/,telemetry/,shell-snapshots/,todos/,session-env/, plus the volatile churn dirs the live CLI rewrites. The skip set is a denylist; the authoritative list isCLAUDE_COPY_IGNOREinsrc/coder_eval/isolation/docker_runner.py(a test asserts this doc and that constant agree, so the list never silently drifts). The container may write anywhere under~/.claude; those writes hit the copy and are discarded when the task ends — the host’s real~/.claudeis never modified. Note the copy includes the OAuth token (.credentials.json) and is mounted read-write, so the in-container agent can read and tamper with the token copy — contained, since the copy is discarded at task end and the host’s real dir is untouched. Opt out entirely withCODER_EVAL_NO_CLAUDE_MOUNT=1.- Writes under
$HOMEoutside the~/.claudemount land in the container’s ephemeral rootfs overlay. Don’t expect them to persist or to be visible to the host. - If a tool detects platform from
HOME(e.g. “starts with/Users/→ macOS”), it will draw the wrong conclusion. Vanishingly rare in practice.
Remove HOME from env_passthrough if you don’t want this behavior — the container’s image-default HOME=/root will win, but then the host’s OAuth dir is no longer reachable.
Run directory safety (--run-dir)
Section titled “Run directory safety (--run-dir)”The host’s run dir is bind-mounted read-write into the container at the same absolute path (so task.json and artifacts land directly on the host filesystem). This makes --run-dir load-bearing for isolation:
- Do not point
--run-dirat a symlink. Docker resolves the source of a bind mount; following a symlink would silently grant the container RW access to a different host location. - Do not point
--run-dirat a sensitive parent (e.g.$HOMEdirectly,/etc, a repo root). Use a dedicatedruns/subtree. - The default (
runs/<timestamp>/) is safe.
Boundary
Section titled “Boundary”| Layer | Location |
|---|---|
| Agent process (Claude Code SDK) | inside container |
| Sandbox + per-row criterion checking | inside container |
task.json serialization | container → host bind mount |
Per-criterion aggregate() (P/R/F1, suite thresholds) | host |
| Reports, run summary, experiment rollups | host |
task.json is the only artifact crossing the boundary. Aggregation reads it via the existing host pipeline unchanged.
Limitations
Section titled “Limitations”- Relative template paths:
template_sources[].pathis resolved to a host absolute path before staging, so it won’t exist inside the container unless you also forward the parent dir viasandbox.docker.extra_mounts. - No container reuse across tasks: each task = one fresh container. Adds ~1–3 s startup overhead per task; negligible vs. LLM latency.
- macOS Keychain auth: not reachable from the container; set
ANTHROPIC_API_KEY(direct) or Bedrock credentials instead.
Architecture
Section titled “Architecture”The host’s DockerRunner (coder_eval/isolation/docker_runner.py) renders the docker run argv, bind-mounts task inputs at /work/input, allocates an output dir at /work/output, and tails container stdout into docker.log in the task’s run dir.
Inside the container, the entrypoint invokes coder-eval _run-task-internal (hidden subcommand), which loads the staged YAML + context, runs the standard in-process Orchestrator (driver auto-coerced back to tempdir), and writes task.json to the output mount. Host reads it and feeds the existing aggregation pipeline.
A result_kind discriminator on CriterionResult ensures ClassificationCriterionResult subclasses survive the JSON round-trip — without it, host-side aggregation would silently lose observed_label/expected_label.