boxer

boxer.toml

Every boxer.toml key with its type, default and an example, where boxer reads configuration, and how devcontainer.json maps onto it.

# boxer.toml at the repository root
image       = "node:24-bookworm-slim"
setup       = ["npm ci"]
start       = ["npm run dev"]
ready       = "curl -sf http://127.0.0.1:3000/"

[network]
mode        = "allowlist"
allow_hosts = ["registry.npmjs.org"]
ports       = ["auto:3000"]

[tasks]
test = "npm test"

Every key is optional. boxer doctor prints each resolved value and the file or variable it came from.

Files and precedence

boxer merges these in order, each overriding the one before.

  1. Built-in defaults, as listed on this page.
  2. .devcontainer/devcontainer.json, else .devcontainer.json, from the worktree root, else the repository root, so a branch that changes its devcontainer gets its own. See the mapping.
  3. $XDG_CONFIG_HOME/boxer/boxer.toml, else ~/.config/boxer/boxer.toml.
  4. boxer.toml at the repository root (the main checkout).
  5. boxer.toml at the worktree root, when it differs from the repository root.
  6. BOXER_* environment variables.
  7. The [harness.<name>] table for the harness making the call.

A later file replaces a list and merges into a table key by key. An unknown key is an error, so a typo cannot weaken enforcement. An unknown top-level table is a warning, so a repository written for a newer boxer still loads. A value outside its allowed set is an error that names the allowed values.

Scope

A scope is what one sandbox belongs to.

isolation

string, default "worktree". One of repo, worktree, session, subagent. Env BOXER_ISOLATION.

What a sandbox is keyed to. session and subagent need an id from the harness or the identity flags. Session and subagent sandboxes in one worktree all mount that same worktree, so two dev servers or installs there still write to the same node_modules and build directories. For dev servers, give each session its own worktree.

isolation = "session"

on_missing_id

string, default "degrade". One of degrade, fail. Env BOXER_ON_MISSING_ID.

What session or subagent isolation does with no id. degrade falls back from subagent to session to worktree and warns. fail refuses with SCOPE_UNRESOLVED.

on_missing_id = "fail"

require_worktree

string, default "warn". One of off, warn, require. Env BOXER_REQUIRE_WORKTREE.

What happens in the main checkout instead of a linked worktree. warn runs and warns, require refuses with WORKTREE_REQUIRED, off says nothing.

require_worktree = "require"

worktree.manage

string, default "off". One of off, detect. Env BOXER_WORKTREE_MANAGE.

detect makes a session in the main checkout share the repository's sandbox, with a warning, until it moves into a linked worktree. It applies only when isolation = "worktree".

[worktree]
manage = "detect"

Integration

integration

string, default "outside". One of outside, inside. Env BOXER_INTEGRATION.

outside runs the harness on the host and its commands in the sandbox. inside runs the harness in the guest. boxer shell and boxer acp set it. See running the harness inside.

integration = "inside"

mode

string, default "rewrite". One of rewrite, tool, off. Env BOXER_MODE.

What a hook does with an intercepted command. rewrite turns it into boxer run -c '<command>'. tool denies it and names boxer_run and boxer run. off lets everything through.

mode = "tool"

enforcement

string, default "both". One of hook, shim, both, audit. Env BOXER_ENFORCEMENT.

Which mechanisms keep commands in the sandbox. With shim or both, a hook on a harness that cannot rewrite (Kimi, DSH) lets the bare command reach the shim, but only when boxer's shims are on the harness's PATH and the command names the program bare. Without shims, or for a program named by path (/opt/homebrew/bin/npm), the hook denies it with the boxer run line to use. With hook, the hook always denies it. audit makes hooks print boxer: audit: would <action> on stderr and allow everything. Shims follow the same setting: under mode = "off", enforcement = "hook" or "audit", a shim runs its program on the host.

enforcement = "hook"

on_sandbox_unavailable

string, default "fail". One of fail, passthrough. Env BOXER_ON_SANDBOX_UNAVAILABLE.

What boxer run does when the sandbox cannot be provisioned. fail refuses. passthrough runs the command on the host, unsandboxed.

on_sandbox_unavailable = "passthrough"

What gets sandboxed

intercept

[]string, default below.

Programs whose commands run in the sandbox. The sandbox's PATH is the image's own, without node_modules/.bin, so run project tools as npx vite or npm run dev rather than adding bare vite or tsc here. A program matches by name, after skipping leading VAR=value assignments and any path, so bin/rails and ./node_modules/.bin/next match rails and next. List names, not paths: letters, digits, ., _, + and -, the same rule for intercept_also and passthrough.

boxer reads the whole line the way a shell would, and wraps it whole when any program in it matches. That includes:

  • every command joined by &&, ||, ;, |, & or a newline
  • commands inside $(...), backticks and <(...), even in double quotes
  • the line given to sh -c, bash -lc and the others, and to eval
  • the program behind env, sudo, nohup, nice, time, timeout, xargs, command, exec, taskset, chrt, busybox and similar wrappers
  • the commands inside if, while, until, for, case, { ... } and a function body, and the line a trap will run
  • a $(...) in an array index, quoted or not, which arithmetic runs

Text in quotes is text: git commit -m "fix npm test" stays on the host. A line boxer cannot read goes to the sandbox: an unclosed quote, a wrapper option boxer does not know (it may take a value boxer would misread as the program), and an alias, whose new name boxer cannot follow. A script run by path (bash build.sh, ./build.sh) is one program, its own name, and boxer does not read inside it: add the script's name, or intercept the tools it calls through PATH shims (enforcement = "both", the default, once boxer shim install has written them and they are on the harness's PATH). ["*"] intercepts every program not in passthrough. It writes no PATH shims, since there is no program to name, so enforcement rests on the hook. Not settable from the environment.

intercept = ["npm", "npx", "pnpm", "yarn", "bun", "bunx", "node", "deno",
             "python", "python3", "pip", "uv", "pytest", "cargo", "rustc",
             "go", "make", "cmake"]

Under enforcement = "shim" or "both", boxer shim install writes a file for every entry. A node or python shim shadows the interpreter of a harness written in that language, and the harness fails at startup. Narrow the list to build tools first.

intercept_also

[]string, default [].

Programs added to intercept, so you keep the default list and name only what your project needs on top. A program already in intercept is not added twice. boxer doctor suggests it for a Ruby or JVM project whose tools the default list leaves on the host. Not settable from the environment.

intercept_also = ["bundle", "rake", "rails", "rspec"]

passthrough

[]string, default ["git", "gh", "ssh", "boxer", "smolvm"].

Programs that never send a line to the sandbox by themselves. A line made only of passthrough and unlisted programs runs on the host. A line that also runs an intercepted program is wrapped whole, so git pull && npm ci runs both in the sandbox. boxer is always treated as passthrough. Not settable from the environment.

passthrough = ["git", "gh", "ssh", "boxer", "smolvm", "terraform"]

Backend

backend

string, default "smolvm". One of smolvm, container, docker, podman. Env BOXER_BACKEND.

The runtime that hosts sandboxes. A backend that lacks a capability refuses the setting that needs it with cause UNSUPPORTED. Only smolvm supports network.mode = "allowlist", the default, so set network.mode with any other backend. See backends.

backend      = "docker"
network.mode = "on"

Guest

image

string, default "". Env BOXER_IMAGE.

The guest image: an OCI reference, or a local docker save archive or rootfs directory given as a path starting ./, ../, / or ~/, or - for stdin. A local image is not pulled. When empty, boxer picks the image from the first lockfile found in the worktree.

LockfileImage
Gemfile.lockruby:3-slim-bookworm
pom.xmlmaven:3-eclipse-temurin-21
build.gradle, build.gradle.ktsgradle:8-jdk21
bun.lock, bun.lockboven/bun:1-slim
pnpm-lock.yaml, package-lock.json, yarn.locknode:24-bookworm-slim
uv.lock, requirements.txt, pyproject.tomlpython:3.12-slim-bookworm
Cargo.lockrust:1-slim-bookworm
go.sum, go.modgolang:1-alpine
nonedebian:bookworm-slim

With integration = "inside" and no image, the default is mirror.gcr.io/library/node:24-bookworm-slim. The Node, Python and Ruby images carry no compiler. Add one in image_setup. See choosing an image.

image = "node:24-alpine"

smolfile

string, default "". Env BOXER_SMOLFILE.

A smolvm Smolfile to build the guest from. Takes precedence over image. Other backends refuse it.

smolfile = "./Smolfile"

build

string, default "".

A Dockerfile, relative to the worktree, that boxer builds and boots instead of image. The build runs on the host every time a sandbox is created. The builder's layer cache makes an unchanged build take a second, and a file the Dockerfile copies can change without the Dockerfile changing.

BackendBuilt byBoots
docker, podmandocker build, podman buildthe tag boxer-<hash>
containercontainer buildthe tag boxer-<hash>
smolvmthe host's docker build, then docker savethe archive, under boxer's state directory

On smolvm, docker must be installed on the host even though the sandbox is a microVM. The last two archives per Dockerfile are kept. The tag hashes the Dockerfile's content, so every worktree of a repository shares one image. Build arguments are not passed. Cannot be set with smolfile. Fails with cause BUILD_FAILED when the build does.

build = "Dockerfile"

build_context

string, default "".

The directory the build sees, relative to the worktree. Empty means the directory holding the Dockerfile.

build         = ".devcontainer/Dockerfile"
build_context = "."

cpus

int, default 4. Env BOXER_CPUS.

Virtual CPUs for the guest.

cpus = 2

memory

string, default "4G". Env BOXER_MEMORY.

Guest memory: 4G, 4096M, or a bare number of MiB. A setup step that exits 137 ran out of memory.

memory = "8G"

mount_at

string, default "/workspace". Env BOXER_MOUNT_AT.

Where the worktree is mounted in the guest, an absolute path. boxer run from a subdirectory runs in the matching subdirectory. With integration = "inside" the worktree is mounted at its host path instead.

mount_at = "/src"

mounts

[]string, default [].

Extra host directories, as host:guest or host:guest:ro. A leading ~/ in the host half is expanded. The worktree is always mounted.

mounts = ["~/.m2:/root/.m2:ro"]

volumes

[]string, default [].

Named directories that outlive the sandbox, as name:/guest/path. Each is a directory on the host, ~/.local/state/boxer/volumes/<scope>/<name>, bind-mounted read-write on every backend. It survives boxer down, boxer rm, --recreate, --rebuild and idle reclaim, so a database keeps its data. It is deleted by boxer rm --volumes, or by boxer gc once its worktree has been gone for 7 days. The week covers a drive that was unplugged, or a repository moved and moved back: the first sweep that finds the worktree missing only marks the volumes, and a worktree that comes back clears the mark. Volumes belong to the worktree's path, so a repository moved for good starts with empty ones at its new path, and a new worktree created at an old one's path gets the old one's volumes. An orchestrator that deletes a worktree when you archive a task takes its volumes with it a week later. Copy out what you want to keep first. A name is letters, digits, - and _. Volumes are not part of the environment pack.

volumes = ["pgdata:/var/lib/postgresql/data"]

user

string, default "".

The guest user for setup, start and every command, as a name or uid[:gid], made of letters, digits, _, . and -. Empty uses the image's USER. image_setup always runs as root. When the user cannot write the worktree, boxer gives it your host uid on smolvm and recreates the sandbox once with --userns=keep-id on podman.

user = "node"

Prep

[prep] runs commands on the host while the sandbox boots. See dependencies. Because these run on the host, prep is one of the keys held back until a person approves the configuration with boxer trust.

prep.commands

[]string, default [].

Run on the host with sh -lc, in the worktree, once per worktree, and again when the list or the target changes. A failure stops provisioning with PREP_FAILED. The commands get BOXER_TARGET_OS, BOXER_TARGET_CPU, BOXER_TARGET_LIBC, BOXER_TARGET_FLAGS (--os=linux --cpu=arm64 --libc=musl) and BOXER_WORKTREE. boxer never adds the flags to your command.

[prep]
commands = ["npm ci $BOXER_TARGET_FLAGS"]

[prep] is only safe for packages that ship prebuilt platform binaries. A package that compiles at install time (node-gyp, cgo) builds for the host and fails in the guest with invalid ELF header. boxer doctor names such packages.

prep.target

string, default "auto".

The guest platform offered to prep.commands. auto derives it from the image, none offers nothing, and os/cpu/libc sets it.

[prep]
target = "linux/arm64/musl"

Cache

cache.enabled

bool, default false. Env none.

Mount the host's package caches into the guest, read-only. Installs still run in the guest, so only the download is saved. Installing a package the host has not cached fails, because the tool cannot write to its read-only cache. See host package caches.

[cache]
enabled = true

cache.managers

[]string, default ["auto"].

Which caches. auto detects them from lockfiles. Otherwise any of npm, pnpm, yarn, bun, uv, poetry, cargo, go.

[cache]
managers = ["pnpm", "cargo"]

Setup and services

See environment for when to use each.

image_setup

[]string, default [].

Commands that change the image, run in the guest as root, once per sandbox (on smolvm, once per host). On smolvm the result is saved in the environment pack, so the next worktree skips them. A failure is SETUP_FAILED.

image_setup = ["apt-get update && apt-get install -y build-essential"]

setup

[]string, default [].

Commands that prepare this worktree, run in the guest as user, once per worktree and again when the list changes or on boxer up --rebuild. A pack never skips them, because the worktree is mounted from the host. They must write into the worktree: a recreated sandbox does not run them again, so anything installed elsewhere is lost. A failure is SETUP_FAILED, and the next command in that sandbox runs setup again rather than taking a running sandbox as ready.

setup = ["npm ci"]

start

[]string, default [].

Commands run detached every time the sandbox starts, after setup. A service that crashes is restarted, as restart says. Their output goes to /tmp/boxer-start.log in the guest. A step that cannot be launched is START_FAILED.

start = ["npm run dev"]

ready

string, default "".

A command polled in the guest until it exits 0, before the sandbox is reported up. Empty means no wait.

ready = "curl -sf http://127.0.0.1:3000/"

ready_timeout

string (Go duration), default "60s".

How long to poll ready. When it runs out, the command fails with NOT_READY and quotes the tail of the start log.

ready_timeout = "3m"

restart

string, default "on-failure". One of on-failure, never.

What happens when a start service exits non-zero. on-failure restarts it after 1 second, then 2, 4 and so on up to 30. A run that lasted 10 seconds or more resets the delay. After five quick crashes in a row it gives up, and the start log says so. A service that exits 0 has finished and is not restarted. never leaves a crashed service down. Every restart is a line in /tmp/boxer-start.log. boxer restart relaunches the services by hand.

restart = "never"

Environment

env

table of strings, default {}.

Set in the guest for every command. Part of the environment pack's key and saved in the pack, so keep secrets out of it. A later file replaces a secrets or env_passthrough list whole rather than adding to it, so a secrets = [...] in boxer.toml drops the names a devcontainer forwarded; list them together if you need both. Names are letters, digits and _, not starting with a digit, as are the names in secrets and env_passthrough.

[env]
NODE_ENV = "development"

env_passthrough

[]string, default ["CI"].

Host variables forwarded to the guest by name at run time, when the host sets them. Values never enter a command line, a pack or an event. They reach setup and image_setup too.

env_passthrough = ["CI", "DEBUG"]

secrets

[]string, default [].

Forwarded exactly like env_passthrough. List credentials here so the file says which variables are secrets. See secrets.

secrets = ["NPM_TOKEN"]

Network

network.mode

string, default "allowlist". One of allowlist, on, off. Env BOXER_NETWORK_MODE.

allowlist lets the guest reach allow_hosts plus the registry hosts its image pull needs. on allows all traffic. off turns the network off. allowlist needs backend = "smolvm".

[network]
mode = "on"

network.allow_hosts

[]string, default [].

What the guest may reach under allowlist. Each entry is one of these.

EntryExampleAllows
a hostnameregistry.npmjs.orgthat exact name, and the addresses it resolves to when the sandbox starts
an address203.0.113.7, 2001:db8::1that address
a CIDR block10.0.0.0/8, 2001:db8::/48every address in the block
an address range10.0.0.10-10.0.0.40every address from the first to the last, inclusive

An address, block or range is reachable on every port and needs no DNS. A hostname is resolved once, when the sandbox starts. If its addresses change later, run boxer up --recreate. A name that is not listed does not resolve in the guest at all.

These are refused when boxer.toml loads, with an error that names the entry:

  • Wildcards, such as *.example.com. smolvm matches exact names only, and it accepts a wildcard without allowing anything. List each host, or use allow_presets.
  • Ports, such as example.com:443 or example.com:8000-8100. Every allowed host or range is reachable on every port.
  • A block with host bits set, such as 10.0.0.1/8. The error suggests 10.0.0.0/8.

A failed command names the hosts and addresses the allowlist refused, and boxer doctor lists them under denied_hosts.

[network]
allow_hosts = ["registry.npmjs.org", "github.com", "10.20.0.0/16", "192.168.1.10-192.168.1.20"]

network.allow_presets

[]string, default [].

Package ecosystems whose hosts join allow_hosts, so you do not have to find out which CDN a registry downloads from. Only used under allowlist. Under on or off it is ignored, without an error, so one boxer.toml works on every backend.

PresetHosts
alpinedl-cdn.alpinelinux.org
cratescrates.io, index.crates.io, static.crates.io
debiandeb.debian.org, security.debian.org
githubgithub.com, api.github.com, codeload.github.com, objects.githubusercontent.com, raw.githubusercontent.com
goproxy.golang.org, sum.golang.org, storage.googleapis.com
gradleservices.gradle.org, downloads.gradle.org, plugins.gradle.org, plugins-artifacts.gradle.org, repo.maven.apache.org
mavenrepo.maven.apache.org, repo1.maven.org
npmregistry.npmjs.org
pypipypi.org, files.pythonhosted.org
rubygemsrubygems.org, index.rubygems.org, rubygems.global.ssl.fastly.net
ruststatic.rust-lang.org, sh.rustup.rs
yarnregistry.yarnpkg.com, repo.yarnpkg.com, registry.npmjs.org
[network]
allow_presets = ["npm", "github"]

network.ports

[]string, default [].

Port forwards. host:guest and a bare guest are bound to 127.0.0.1; an explicit address you write (0.0.0.0:8080:3000, [::1]:8080:3000) is kept as written, so only you can publish on every interface. A /tcp, /udp or /sctp suffix and one-to-one ranges (8000-8002:8000-8002) are allowed. auto:<guest> takes a free host port, so worktrees do not collide, and never takes a port a stopped sandbox holds. boxer status shows which host port it got. When no port is free, the command fails with NO_FREE_PORT. A port is 1 to 65535.

[network]
ports = ["auto:3000", "5432:5432"]

network.dns

string, default "".

The guest resolver on smolvm. Empty uses the host's caching resolver, off uses smolvm's default public resolvers, and an address is used as given.

[network]
dns = "10.0.0.2"

URLs

[urls] names forwarded ports through portless. See URLs.

urls.enabled

bool, default false. Env BOXER_URLS (a boolean).

Give each forwarded port a stable name, such as https://fix-ui.myapp.localhost:1355. Needs portless on the host. Without it, boxer warns and runs as if this were off.

[urls]
enabled = true

urls.provider

string, default "portless". One of portless.

What serves the names.

[urls]
provider = "portless"

urls.name

string, default "".

The base hostname, before the worktree prefix. Empty uses the repository's directory name.

[urls]
name = "shop"

urls.names

table of strings, default {}.

A label per guest port, giving <label>.<name>. One forwarded port needs no label. Several unlabelled ports are named by their number.

[urls]
names = { "3000" = "web", "8080" = "api" }

Tasks

tasks.<name>

string or table, default none.

A named command line, run with boxer run --task <name>, or with the boxer_run MCP tool's task argument. The table form takes cmd (required), description, junit (paths or globs), timeout (a Go duration) and env. Any other key is an error. An empty cmd or a bad timeout is an error. A task is replaced whole by a later file, not merged key by key: a later file that sets only description for a task drops its cmd. See tasks.

[tasks]
test = "npm test"

[tasks.e2e]
cmd         = "npm run e2e"
description = "the browser suite; needs the dev server"
junit       = ["reports/junit.xml"]
timeout     = "10m"
env         = { HEADLESS = "1" }

Results

See test results.

results.auto

[]string, default [].

Worktree-relative JUnit XML paths or globs to summarise after every boxer run. A task's junit and the --junit flag take precedence.

[results]
auto = ["reports/*.xml"]

results.fail_on_test_failures

bool, default false.

Exit 1 when the command exited 0 and a test in the reports failed. A non-zero exit is never changed.

[results]
fail_on_test_failures = true

Lifecycle

create_on

[]string, default ["session_start", "run", "mcp"].

Signals that may create a sandbox. Any of session_start, subagent_start, run, mcp. Without run, boxer run fails with NO_SANDBOX until boxer up has run.

create_on = ["session_start", "subagent_start", "run", "mcp"]

destroy_on

[]string, default [].

Hook signals that delete the sandbox. Any of session_end, subagent_stop, never. Empty keeps the sandbox after the session, for gc to reclaim when idle.

destroy_on = ["session_end"]

warm_on_session_start

bool, default false. Env BOXER_WARM_ON_SESSION_START.

When true, the session-start hook provisions in a detached boxer up and returns at once, so the session never waits for a create. The first intercepted command waits for it instead. When false, the session-start hook waits for the sandbox, up to the hook's 10-minute limit.

warm_on_session_start = true

Reclaiming

auto_reclaim

bool, default true.

When true, ordinary commands start the gc sweep in the background, at most once per reclaim_every. BOXER_NO_RECLAIM=1 skips it for one process.

auto_reclaim = false

reclaim_every

string (Go duration), default "6h".

The minimum interval between automatic sweeps.

reclaim_every = "1h"

idle_timeout

string (Go duration or "never"), default "2h".

A sandbox or environment pack unused this long is reclaimable. What happens to a sandbox is idle_action. never turns the idle rule off. gc judges each sandbox by its own repository's idle_timeout and idle_action, wherever it runs, so one repository's settings never reclaim another's sandboxes. The current directory's value ages packs. A sandbox with a fork child in use is not idle.

idle_timeout = "24h"

idle_action

string, default "stop". One of stop, delete.

What idle reclaim does to a sandbox whose worktree still exists. stop frees its memory and keeps its disk, so the next command starts it again with what it had installed and stored. delete frees both, which was the only behaviour before 1.2. A sandbox whose worktree is gone is always deleted. gc --all still deletes stopped sandboxes.

idle_action = "delete"

min_free_gb

float, default 5.

The free disk space boxer keeps, in GB. Below it, boxer skips writing a pack and pulls the image instead, and boxer doctor warns. 0 turns the check off.

min_free_gb = 20

packs_keep_last

int, default 5.

Keep at most this many unused packs, whatever their age: environment, image and harness packs together, newest first. A pack a sandbox was created from is not counted and not removed. 0 keeps every pack the idle rule allows.

packs_keep_last = 2

Harness overrides

harness.<name>

table, default none.

Overrides for one harness, applied after every file and variable. Only mode, enforcement and isolation are accepted, each checked against the same values as the top-level key. <name> is the harness name boxer install takes, and the call selects it with --harness or BOXER_HARNESS. Inside mode's program names work too: [harness.claude] and [harness.claude-code] configure the same harness. A name boxer does not know is an error, so a misspelt table cannot quietly do nothing.

[harness.kimi]
mode        = "tool"
enforcement = "hook"
isolation   = "session"

Telemetry

See events.

telemetry.enabled

bool, default false.

Record events. Off means no log, no metrics and no network. true with sink = "none" uses file.

[telemetry]
enabled = true

telemetry.sink

string, default "none". One of none, file, stderr, otel. Env BOXER_TELEMETRY_SINK.

Where events go. Setting the variable to anything but none also sets enabled.

[telemetry]
enabled = true
sink = "stderr"

telemetry.path

string, default "".

The file sink's path. Empty uses $XDG_STATE_HOME/boxer/events.jsonl, else ~/.local/state/boxer/events.jsonl. BOXER_TRACE overrides it.

[telemetry]
path = "/tmp/boxer-events.jsonl"

telemetry.record_commands

bool, default false.

Keep command lines, argv, env and setup in event payloads. Otherwise each is replaced by its length, as command_len and so on.

[telemetry]
record_commands = true

telemetry.endpoint

string, default "".

The OTLP endpoint for sink = "otel". Only a binary built with -tags otel has that sink.

[telemetry]
sink     = "otel"
endpoint = "http://127.0.0.1:4318"

Deprecated

These still load, so a boxer.toml that boxer 1.0 read keeps working. Each has no effect, prints a warning, and will be removed in 2.0.

require_linked_worktree

bool, default false. Use require_worktree = "require" instead.

reuse_existing

bool, default true. A scope always reuses its sandbox. Delete the line.

Environment variables

Eighteen scalar keys can be set from the environment, for a harness that offers no other way to configure a subprocess. Lists and tables cannot. A variable overrides every file, and a [harness.<name>] table overrides the variable. boxer doctor names the variable as the source.

VariableKey
BOXER_BACKENDbackend
BOXER_ISOLATIONisolation
BOXER_ON_MISSING_IDon_missing_id
BOXER_REQUIRE_WORKTREErequire_worktree
BOXER_WORKTREE_MANAGEworktree.manage
BOXER_INTEGRATIONintegration
BOXER_MODEmode
BOXER_ENFORCEMENTenforcement
BOXER_ON_SANDBOX_UNAVAILABLEon_sandbox_unavailable
BOXER_IMAGEimage
BOXER_SMOLFILEsmolfile
BOXER_MOUNT_ATmount_at
BOXER_CPUScpus (an integer)
BOXER_MEMORYmemory
BOXER_NETWORK_MODEnetwork.mode
BOXER_URLSurls.enabled (a boolean)
BOXER_TELEMETRY_SINKtelemetry.sink, and telemetry.enabled unless the value is none
BOXER_WARM_ON_SESSION_STARTwarm_on_session_start (a boolean)

A value that does not parse as its type is an error that names the variable, as a bad value in boxer.toml is. BOXER_URLS holding a port map, which boxer sets inside the guest, is not read as this setting. Variables with no key are listed in the CLI reference.

devcontainer.json

boxer reads the part of a devcontainer file it can honour. It is the lowest file layer, so any key boxer.toml sets wins. Comments and trailing commas are accepted.

devcontainer.jsonboxer.toml
imageimage
build.dockerfile, or dockerFilebuild, rewritten to be relative to the worktree
build.contextbuild_context, likewise
initializeCommandprep.commands, on the host, as the specification says
onCreateCommandimage_setup
updateContentCommand, then postCreateCommandsetup
postStartCommandstart
workspaceFoldermount_at
forwardPortsnetwork.ports as auto:<port>. A "service:port" string uses the port
portsAttributes.<port>.requireLocalPortThat forward kept fixed, as <port>:<port>
portsAttributes.<port>.labelurls.names
appPortnetwork.ports, fixed, as Docker's -p is
containerEnv, remoteEnvenv. remoteEnv wins where both name a variable
mountsmounts, bind mounts only
remoteUser, else containerUseruser
hostRequirements.cpus, hostRequirements.memorycpus, memory, raised to the minimum, never lowered

A command given as a string or an array is one line. An object of named commands becomes one line per name, in name order.

Variables are resolved on the host in every string: ${localWorkspaceFolder} and ${localWorkspaceFolderBasename} (this worktree), ${containerWorkspaceFolder} and ${containerWorkspaceFolderBasename} (the mount), and ${localEnv:NAME} or ${localEnv:NAME:default}, also spelled ${env:NAME}. ${containerEnv:NAME} resolves inside remoteEnv when containerEnv defines the name. Another ${containerEnv:NAME} or ${devcontainerId} gets a warning, and an env value or mount that uses it is dropped.

An entry that passes a host variable under its own name, "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}", becomes one of secrets rather than env: env is stored in the environment pack and passed by value, and a secret is read from the host when each command runs. Any other entry that takes a host value is kept in env, with a warning naming it. A byte-order mark at the start of the file is ignored.

These are refused with a warning and have no effect.

KeyWhy
featuresboxer does not install features. Put them in image_setup or in the Dockerfile build names
build.args, build.target, build.cacheFrom, build.optionsboxer builds the Dockerfile as it is
dockerComposeFileIt runs several containers. boxer runs one guest per worktree
runArgs, privileged, capAdd, securityOptThey would change the sandbox's own boundary
workspaceMountboxer always mounts the worktree, at mount_at
overrideCommand: falseboxer never runs the image's own command. Put it in start
postAttachCommandNothing attaches to a sandbox
initThe guest already reaps what it starts
hostRequirements.gpu, unless false or "optional"No backend passes a GPU through

Every other key is ignored, including name, customizations, shutdownAction, userEnvProbe, otherPortsAttributes, waitFor (use ready) and hostRequirements.storage (min_free_gb covers disk). boxer always makes user able to write the worktree, as updateRemoteUserUID asks.

On this page