~/.xorcise/config.toml, ~/.xorcise/.env and the process environment. Every key below is settable in all three places.
Every configuration key
config.toml is a flat key-and-value file with no sections. Keys are lowercase and unprefixed. The environment variable for any key is XORCISE_ followed by the key in upper case.
Precedence
Highest wins:xorcise up and xorcise serve resolve --port, --otlp-port and --stub and then stamp the resolved values into the environment before settings are read.
The rung that surprises people is the third one: .env beats config.toml. A key written in both files is read from .env. Since the web UI and the xorcise config setters all write to .env, a value you edited by hand in config.toml can be silently overridden by a value the UI wrote.
An XORCISE_* variable set in your shell beats both files, including when the CLI is only talking to a brain rather than starting one. Setting XORCISE_REST_PORT in your shell is read as a deliberate “talk to this port” instruction.
Where to put each key
.env is created at mode 0600 and re-set to 0600 on every write. config.toml is world-readable. That difference is the whole rule.
Put in .env:
Put in
config.toml: non-secret shape — role, host, and the port keys. This is what xorcise up scaffolds on first start, and it is where up appends its managed Headscale block.
Both files are created by the first xorcise up and never overwritten afterwards.
Judge model keys
The judge is bring-your-own-model. XORCISE bundles none, and calls whichever OpenAI-compatible endpoint you configure.model_key
str · default unset · XORCISE_MODEL_KEY
Your API key. With no key set, the judge half of every score degrades to 0.0 and the run is still graded deterministically. Keep it in .env.
model_base_url
str · default unset · XORCISE_MODEL_BASE_URL
The OpenAI-compatible base URL. Unset means the client’s own default, https://api.openai.com/v1. Point it at a self-hosted endpoint to keep transcripts on infrastructure you control.
model_name
str · default unset · XORCISE_MODEL_NAME
The model to grade with, for example gpt-4o-mini.
model_timeout_seconds
float · default 120.0 · XORCISE_MODEL_TIMEOUT_SECONDS
HTTP timeout for one judge call. The default is generous so that a slow reasoning model does not report as judge-unavailable.
judge_transcript_max_tokens
int · default 0 · XORCISE_JUDGE_TRANSCRIPT_MAX_TOKENS
A pre-flight ceiling on the estimated judge prompt. 0 disables it, which is the default: the token count is an estimate, so a hard gate risks rejecting prompts the provider would have accepted. Letting the provider’s own limit be the gate returns an accurate error, and the run stays re-gradeable. Set a positive value only for a small-context self-hosted judge.
judge_span_max_tokens
int · default 2000 · XORCISE_JUDGE_SPAN_MAX_TOKENS
Caps each span’s body in the distilled transcript, keeping a head and tail window with an elision marker between them. Transcripts are fat-tailed — a few enormous tool-output spans dominate the count — so this collapses the tail without dropping any span. 0 disables it.
judge_tokenizer
str · default "o200k_base" · XORCISE_JUDGE_TOKENIZER
The tiktoken encoding used to count the budget. Tokenizers are not interchangeable, so the choice is explicit. An unrecognised name degrades to a coarse characters-divided-by-four heuristic. For a non-OpenAI judge this is an approximation, not an exact count.
Terrain attribution keys
Terrain attribution asks a model which part of the mission map each event touched. Each key falls back to its judge equivalent when unset, so by default terrain uses the same model as the judge. The timeout is alwaysmodel_timeout_seconds.
terrain_model_key
str · default unset · XORCISE_TERRAIN_MODEL_KEY
Overrides model_key for attribution only. Secret; keep it in .env.
terrain_model_base_url
str · default unset · XORCISE_TERRAIN_MODEL_BASE_URL
Overrides model_base_url for attribution only.
terrain_model_name
str · default unset · XORCISE_TERRAIN_MODEL_NAME
Overrides model_name for attribution only. A cheaper, smaller model is a reasonable choice here.
terrain_transcript_max_tokens
int · default 256000 · XORCISE_TERRAIN_TRANSCRIPT_MAX_TOKENS
A safety cap on a single attribution call. Unlike the judge, terrain attributes in bounded batches, and an oversized batch is shrunk by dropping trailing events rather than failed. Lower it for a small-context attribution model.
terrain_auto_attribute
bool · default true · XORCISE_TERRAIN_AUTO_ATTRIBUTE
Attributes the terrain plane when a run goes terminal, so every run’s map is complete afterwards regardless of who looked at it. It is a no-op when no terrain or judge model is configured.
Storage keys
Each of these is empty in the model and filled at load time with a path under~/.xorcise. Setting one moves that store.
database_url
str · default sqlite:///~/.xorcise/xorcise.db · XORCISE_DATABASE_URL
A SQLAlchemy URL. This one database holds agents, runs, results, grades and the raw OTLP traces and logs. PostgreSQL needs the postgres extra and is not a tested path for normal use.
missions_root
str · default ~/.xorcise/missions · XORCISE_MISSIONS_ROOT
Where ingested and pulled missions are installed.
tailscale_cache_root
str · default ~/.xorcise/cache/tailscale · XORCISE_TAILSCALE_CACHE_ROOT
Where the pinned static Tailscale client is cached, one tarball of roughly 37 MB per architecture. Pre-seeding this directory lets an air-gapped agent join the tailnet with no public egress.
Mission library keys
XORCISE is connected to the hosted library whencatalog_enabled is true and catalog_url is set. Both ship that way, so a default install is connected to the production catalog and the library is populated on first run. Disconnected means an empty remote library, never a fabricated one. No API key, account or sign-in is involved at any point.
catalog_url
str · default https://api.xorcise.ai · XORCISE_CATALOG_URL
The library endpoint, defaulting to the production XORCISE catalog. Point it at another tier to use one, or clear it and the library reports disconnected rather than probing an unreachable host. Use catalog_enabled rather than this key to disconnect: the switch is a clean boolean, and clearing the URL is what a disconnect should not depend on.
catalog_enabled
bool · default true · XORCISE_CATALOG_ENABLED
The connect and disconnect switch, written by the Settings page and by xorcise catalog connect. It is a boolean rather than an empty URL so that disconnecting cannot silently revert to the default endpoint. It also governs the web UI’s announcement banners: switched off, or with catalog_url cleared, XORCISE answers the announcement check with an empty list before making any network request. use_stubs does the same.
catalog_key
str · default unset · XORCISE_CATALOG_KEY
Declared but read by nothing. Setting it has no effect. See keys that nothing reads.
Runner keys
use_stubs
bool · default false · XORCISE_USE_STUBS
Forces stub adapters so XORCISE boots without Docker. Runs created in this mode do not launch containers. xorcise up --stub sets it for one boot.
docker_platform
str · default "" · XORCISE_DOCKER_PLATFORM
An operator override for the platform passed to docker pull and docker run. Leave it empty — the default — and XORCISE selects per mission, native-first: your host’s own platform when the mission published a validated image for it, linux/amd64 under emulation when it did not, and a named refusal before any download when neither is possible. See Which architecture gets pulled.
Set a value and it wins unconditionally, including over a mission that never validated that platform. Use it to reproduce an amd64 result on an ARM host, not as a default. That works on an Apple Silicon Mac through Docker Desktop’s Rosetta. An arm64 Linux host cannot run linux/amd64 nested, so with this set to linux/amd64 every lab run there is refused by the nested-container check.
nested_container_check
str · default "enforce" · XORCISE_NESTED_CONTAINER_CHECK
Whether run creation verifies that this host can run a container inside a container. enforce refuses run creation with a diagnosis on a host that cannot. skip bypasses only the check, for hosts where the probe itself cannot run — restricted CI, no privileged containers — but nesting is known good.
The check runs at the platform of the mission’s image, not only at your host’s own, because a host that nests its own platform may not nest an emulated one: an arm64 Linux host runs arm64 missions nested but not amd64 ones. Each platform’s verdict is kept until XORCISE restarts or xorcise doctor asks the running server to probe again. The setting is read when XORCISE starts, so after you change it, in the server’s environment or as nested_container_check = "skip" in config.toml, restart with xorcise down and then xorcise up.
Every lab mission runs its stack inside the run’s own container. skip does not change that and cannot restore the older topology that put mission containers on your own Docker daemon.
Role and endpoint keys
XORCISE serves two planes: the REST API with the web UI on3001, and OTLP/HTTP ingest on 4318. Both scan up to 50 ports above the requested one when the port is busy, printing a notice for each move, so read the ports xorcise up printed rather than assuming the defaults. There is no MCP plane and no port 3100.
role
str · default "all" · XORCISE_ROLE
Which service role xorcise serve boots. See deployment roles. xorcise up always boots all.
host
str · default "127.0.0.1" · XORCISE_HOST
The address XORCISE prints in URLs, health-polls and scans during preflight. It is not the bind address. In the default local topology both planes bind 127.0.0.1 and ::1, plus the Docker bridge gateway on native Linux. Setting host to 0.0.0.0 widens the bind to the IPv4 wildcard. Setting it to any other non-loopback address adds that address to the bind, so the unauthenticated planes are served on it as well, without a wildcard. See Security and isolation.
rest_port
int · default 3001 · XORCISE_REST_PORT
The REST API and web UI port. xorcise up --port overrides it for one boot.
otlp_port
int · default 4318 · XORCISE_OTLP_PORT
The OTLP/HTTP ingest port, matching the OpenTelemetry specification default. xorcise up --otlp-port overrides it for one boot.
runner_port
int · default 8800 · XORCISE_RUNNER_PORT
Only used by the experimental runner role, which serves nothing but a health endpoint.
headscale_port
int · default 8080 · XORCISE_HEADSCALE_PORT
A fallback used to derive a plain-HTTP login server when headscale_url is unset. On a normal install xorcise up always writes headscale_url, so this never applies.
Per-run network keys
xorcise up writes headscale_url, headscale_ca_cert, headscale_host_alias and headscale_advertise_host into a managed block in config.toml, delimited by # >>> xorcise headscale (managed) >>>. The block is rewritten on every up and any hand-written configuration outside it survives. Set these by hand only when you are pointing XORCISE at a control plane you run yourself, and use xorcise config set-network, which is marked experimental and needs a restart.
headscale_container
str · default "headscale" · XORCISE_HEADSCALE_CONTAINER
The container name that control operations docker exec into. Control is container-based, not URL-based, which is why xorcise doctor probes the container. A healthy container is only half of what a run needs, so doctor also probes headscale_url.
orchestrator_user
str · default "orchestrator" · XORCISE_ORCHESTRATOR_USER
The Headscale user that owns per-run subnet routers.
router_tag
str · default "tag:router" · XORCISE_ROUTER_TAG
The access-policy tag whose auto-approvers accept each router’s advertised routes.
headscale_url
str · default "" · XORCISE_HEADSCALE_URL
The login server tailnet joins use. A locally provisioned control plane sets https://<host-ip>:443. A value that does not match the managed block is treated as an external control plane, verified at boot rather than provisioned.
<host-ip> is the address up detected when it provisioned the control plane, and every run’s subnet router dials this URL. If the host’s address changes afterwards, nothing answers there any more. xorcise doctor probes this URL as its control plane address check, says when the host’s current address differs from the recorded one, and gives xorcise down && xorcise up as the fix. An external control plane is probed at its URL with no address comparison.
headscale_ca_cert
str · default "" · XORCISE_HEADSCALE_CA_CERT
Path to the PEM delivered to each run’s router so it trusts the self-signed control-plane certificate.
headscale_host_alias
str · default "" · XORCISE_HEADSCALE_HOST_ALIAS
A headscale.local:<ip> pair injected as the router’s extra_hosts, and the source of the host-to-IP rewrite in a join URL.
headscale_advertise_host
str · default "" · XORCISE_HEADSCALE_ADVERTISE_HOST
The host per-run routers and agents dial. host is the REST bind address and is not reachable from inside the fused container’s nested network, so this is set to the Docker bridge gateway instead.
deployment_topology
"local" or "distributed" · default "local" · XORCISE_DEPLOYMENT_TOPOLOGY
local binds both planes to loopback — plus, on native Linux, the Docker bridge gateway — which is the narrowest set that still lets a container-launched agent reach them through host.docker.internal. distributed binds host alone and adds an orchestrator router to the control-plane compose file. Distributed operation is unfinished; treat it as experimental.
With distributed and a loopback host, which is the default, agents on other machines could reach nothing, so xorcise up and xorcise serve refuse to start and exit 1 with deployment_topology is 'distributed', but XORCISE would listen only on 127.0.0.1 …. Set host in ~/.xorcise/config.toml to the address agents use, not in one shell’s environment, because every later CLI command finds the server at host too. Do not use 0.0.0.0: it exposes the unauthenticated API on every network the host is on.
base_network
str · default "10.200.0.0/16" · XORCISE_BASE_NETWORK
The address pool each run’s subnet is carved from. Change it if it collides with your own networks.
cidr_prefix
int · default 24 · XORCISE_CIDR_PREFIX
The size of each run’s carved subnet. With the defaults this gives 254 concurrent runs.
key_expiration
str · default "1h" · XORCISE_KEY_EXPIRATION
Time-to-live of the single-use Headscale pre-auth keys minted for each run’s agent and router.
Run lifecycle keys
default_budget_seconds
int · default 3600 · XORCISE_DEFAULT_BUDGET_SECONDS
The wall-clock budget a run gets when you do not set one. A run terminated by its budget is marked partial.
budget_watchdog_interval_seconds
float · default 5.0 · XORCISE_BUDGET_WATCHDOG_INTERVAL_SECONDS
How often XORCISE scans for runs past their budget. Expired runs are terminated even when they send no traffic.
readiness_timeout_seconds
float · default 300.0 · XORCISE_READINESS_TIMEOUT_SECONDS
How long a deployed run may take to bring its environment up before XORCISE closes it out as deploy_failed, counted from the moment the run was created. Deployment does not block on bring-up, so without this gate a run whose environment died would sit non-terminal forever while the agent worked a target that never existed. 0 disables the gate.
The window bounds only a bring-up that never finishes. An environment whose container exits is closed out at the next scan, inside the window or not, so a long window costs nothing there. It does cost on the ambiguous case: an environment whose inner Docker daemon never answers counts as not ready, so it is closed out only after the whole window, about five minutes at the default. A large mission can need well over a minute to load its images before its services even start, so raise the window if runs are closed out while they are still coming up. With the gate disabled, nothing reports an environment ready either: for as long as the run is live, the Environment chip on the run page reads Starting and GET /api/runs/{run_id}/environment returns starting. What the gate checks and what it records is on Runs.
readiness_scan_interval_seconds
float · default 5.0 · XORCISE_READINESS_SCAN_INTERVAL_SECONDS
How often the readiness gate scans.
telemetry_drain_seconds
float · default 5.0, clamped to 0 through 300 · XORCISE_TELEMETRY_DRAIN_SECONDS
How long OTLP ingest stays open after a run goes terminal. Short-lived harnesses commonly flush their final tool result after completing the run, and sealing before that flush rejects the last batch permanently.
release_sweep_interval_seconds
float · default 30.0 · XORCISE_RELEASE_SWEEP_INTERVAL_SECONDS
How often the release sweeper scans. A finished run’s container and tailnet nodes are torn down once its grading ends; when that fails, usually because Docker or Headscale is unreachable, the run keeps its environment and the sweeper retries it until the teardown lands. It never gives up. Each run backs off on its own, waiting 30 seconds after its first failure and doubling up to 10 minutes, so a plane that stays down is not hammered. It takes a run only once its grading has settled, and while any run has been failing for over an hour it logs one summary line at error level, at most hourly. 0 disables the sweeper, so a failed teardown is no longer retried automatically.
release_sweep_batch
int · default 20, at least 1 · XORCISE_RELEASE_SWEEP_BATCH
At most how many runs one sweeper scan attempts; the rest wait for the next scan. It bounds the Docker and Headscale work a long outage can pile up. The first start after upgrading from 0.1.x also revisits every finished lab run recorded before the upgrade once, because those runs carry no record of a completed teardown. For a run that was released properly that is an idempotent no-op, and a run whose tailnet nodes leaked under the earlier version is finally cleaned up.
attachment_ttl_seconds
int · default 300 · XORCISE_ATTACHMENT_TTL_SECONDS
Lifetime of a signed attachment download URL.
Signing and telemetry keys
run_control_signing_secret
str · default unset · XORCISE_RUN_CONTROL_SIGNING_SECRET
The HMAC secret that signs short-lived attachment download URLs. When it is unset, XORCISE generates a fresh random secret at every process start, so a link minted before a restart stops verifying; with the default attachment_ttl_seconds of 300 that is harmless. Set it in .env only to keep links valid across restarts or to share one secret between processes, then restart XORCISE.
otel_mirror_enabled
bool · default false · XORCISE_OTEL_MIRROR_ENABLED
A reserved seam for mirroring traces to your own OpenTelemetry service. No exporter is built. Setting it to true fails the boot with an explicit message rather than quietly doing nothing.
otel_mirror_endpoint
str · default "" · XORCISE_OTEL_MIRROR_ENDPOINT
Reserved alongside otel_mirror_enabled. Nothing reads it.
otel_drop_spool_enabled
bool · default false · XORCISE_OTEL_DROP_SPOOL_ENABLED
The OTLP receiver always counts and logs a batch it refuses: one that carries no run id it can read, or one for a run that is already sealed. With this on, it also writes each refused batch to otel_drop_spool_dir as one JSON file, so you can read what was refused. It is read when XORCISE starts. The spool is a diagnostic, not evidence: nothing in XORCISE reads the files back, and a spooled batch never joins a run. The receiver takes no credential, so with the spool on, anyone who can reach the OTLP port can fill it up to the two bounds below. Send traces and logs shows how to read the counters.
otel_drop_spool_dir
str · default ~/.xorcise/otel-dropped · XORCISE_OTEL_DROP_SPOOL_DIR
Where the spool writes, created on the first refused batch after the spool is on. Each file holds the time the batch arrived, its signal, the reason it was refused, the run id when it named one, and the batch as received.
otel_drop_spool_cap
int · default 200, at least 1 · XORCISE_OTEL_DROP_SPOOL_CAP
The most files the spool keeps. After each write, the oldest files are deleted until this bound and otel_drop_spool_max_bytes both hold.
otel_drop_spool_max_bytes
int · default 67108864 (64 MiB), at least 1 · XORCISE_OTEL_DROP_SPOOL_MAX_BYTES
The most bytes the spool keeps in total. The receiver sets no limit on the size of a request, so this is what bounds the disk an unroutable export can use. A single batch larger than the whole budget is counted and logged but not written.
Keys that nothing reads
Two keys exist in the settings model and have no effect. They are listed here so that setting one and seeing no change is not mistaken for a bug.headscale_port is a third near-miss: it is read, but only as a fallback that a normal install never reaches.
Deployment roles
xorcise up always boots role all and never passes a role flag, so a default install cannot reach any other role. The role surface is reachable only through xorcise serve --role, and the CLI labels it EXPERIMENTAL.
Booting an experimental role prints its caveat before it does any work.
The ~/.xorcise directory
$XORCISE_HOME overrides the location. The first xorcise up creates everything below.
xorcise down removes only logs/, runtime-ports.json and xorcise.pid. xorcise down --purge removes the entire directory, including .env and your database.
Related pages
- Security and isolation — what the bind address, the signing secret and the model keys mean in practice.
- CLI reference — the commands that read and write these keys.