Skip to main content
Find the line you are looking at in the table below, then read its entry. Two commands answer most triage questions before you get that far: xorcise doctor checks this host’s prerequisites and prints an exact fix per failure, and xorcise status probes the REST plane, the OTLP plane, and the Docker daemon.

Find your problem

Startup problems

Cause. xorcise up is real by default and needs a reachable Docker daemon.Fix. Start Docker — sudo systemctl start docker on Linux, or open Docker Desktop on macOS — and run up again. To look around without Docker, xorcise up --stub boots a demo in which missions do not execute.
Cause. Your user is not a member of the docker group. This is the standard fresh-Ubuntu case and it is not a XORCISE problem.Fix. Add yourself to the group, then start a new login session:
Log out and back in. newgrp docker is not enough — it changes only the shell you type it into, so xorcise up in any other terminal keeps failing.
Cause. XORCISE requires the Docker Compose v2 plugin, invoked as docker compose. The legacy standalone docker-compose v1 binary does not satisfy it, so a host with only v1 installed fails this check while docker-compose --version looks fine.Fix. Install the plugin:
Docker’s own docker-compose-plugin package works equally well. Confirm with docker compose version.
Cause. openssl is a hard prerequisite. XORCISE uses it to generate the private certificate authority and TLS certificate for the local Headscale control plane, so without it no run can get a network.Fix. Install it — sudo apt install openssl — and re-run xorcise doctor to confirm.
Cause. None. When a port is taken, XORCISE scans up to 50 ports above it, moves, and prints a notice per move. This is a notice, not an error.Fix. Use the address up prints. Other XORCISE commands find the relocated brain by themselves. If you need a fixed port, pin it before starting with xorcise up --port 3010, or set rest_port in ~/.xorcise/config.toml. A port held by another program is stepped past in the same way. A conflict is reported only when a port is taken between the scan and the server’s bind, which names the port and plane and needs that process stopped (up then reports that XORCISE did not become healthy, and the message is in ~/.xorcise/serve.log), or when all 50 ports are taken, which stops up with no free port for <plane> in <first>-<last>.
Cause. An instance is already up in this home directory.Fix. Nothing. xorcise up against a running instance is success and exits 0 — that is deliberate, so scripts and agents can re-run it defensively. You do not need xorcise down first, and there is no restart to perform. If the previous process exited uncleanly, up clears the stale record and continues on its own. If the pid file is gone or stale but an instance of this home still answers on its REST port, up adopts that instance rather than start a second one over the same database, and prints already running (pid record repaired) — UI at … instead.
Cause. Your database holds data written by an older build. XORCISE brings an empty database to the current schema automatically but never migrates a populated one, because migrating real data stays a decision you make.Fix. Stop any running XORCISE, then migrate and start it again:
db upgrade refuses while a server of this home is still running. The same status is visible on Settings → Environment, which reads behind — migration needed.
Cause. The database was stamped by a different build of XORCISE — one whose migrations were renamed or removed — so the upgrade cannot find a path forward.Fix. Back up the database first — a copy of xorcise.db alone can miss recent writes. Then either install the build that created it and upgrade from there, or re-initialise with xorcise down --purge, which destroys all local data. There is no downgrade path.
Cause. xorcise db upgrade found a server of this home running, by its pid file or by asking its REST port, and will not migrate the database underneath it. Migrating rebuilds tables the server has open.Fix. Stop it first, then migrate and start it again:
If down cannot stop it, see the next entry. xorcise db upgrade --force skips the check, and is unsafe while a server is using the database.
Cause. xorcise down signalled the server and it did not stop: the process belongs to another user, or it was still alive after SIGKILL. When that process is the one in the pid file, down leaves the file in place so the server stays findable. Either way it exits 1 before it reaps containers or tears down Headscale. Two related errors stop down the same way: could not stop the instance of this home on port …, for a server it found by asking the port rather than by the pid file, and an instance of this home still answers on port … but did not report a usable pid, for a server from before v0.2.0 that cannot tell down its pid.Fix. Stop the process yourself, as the user that owns it: kill <pid> with the pid the error names, or, when no pid was reported, find the process with the ss -tlnp command the error prints. Then run xorcise down again.
Cause. Before they act, up, down and db upgrade ask /api/system on each REST port an instance of this home could be using, to tell whether one is running there. Something on that port accepted the connection, then gave no answer within 8 seconds, dropped the connection, or answered with a server error. The commands cannot tell whether it is XORCISE, so they refuse rather than guess: a wrong guess starts a second server on the same database, or migrates the database under a live one.Fix. Find what holds the port: ss -tlnp 'sport = :3001' on Linux, lsof -nP -iTCP:3001 -sTCP:LISTEN on macOS. If it is a hung XORCISE, stop it; xorcise down can, while the pid file still names it. If it is another program, give XORCISE a different port with XORCISE_REST_PORT or rest_port in ~/.xorcise/config.toml. xorcise up --port alone does not get past it, because the configured port is still checked.
Cause. Each XORCISE release runs the one Headscale release it was built against, pinned by digest. xorcise up pulls it from Docker Hub when the host does not hold that exact image, and the pull failed: no registry access, a proxy Docker does not use, or a rate limit. The error names the image and ends with Docker’s own message.Fix. On a host that has registry access, pull the image the error names, save it, and load it here:
up recognises a loaded image by its id, so it does not need registry access once the image is here. A headscale/headscale:v0.29.5 tag that names any other build is refused.
Cause. Headscale migrates its own database forward only. The Headscale container on this host runs a newer release than the one this XORCISE release pins, so the pinned release cannot open its database. Before 0.2.1, XORCISE ran whichever release headscale:stable named at the time, which can be newer than the pin; a later XORCISE can also have left a newer one. up checks this only while the old container still exists, which is the case after xorcise down --keep-data.Fix. Install the XORCISE release that pins the newer Headscale, or reset the control plane:
A plain xorcise down tears Headscale down with its tailnet state, which holds the nodes and keys of past runs, so no run should be in flight when you do it. Your runs, results and traces are in xorcise.db and are not touched.
Cause. The brain was spawned but crashed or hung during boot, so the health poll timed out.Fix. Read ~/.xorcise/serve.log — the brain’s stderr goes there, and it is truncated on every up, so the traceback you want is the one at the top. ~/.xorcise/logs/ is created but empty; nothing writes to it, and it will not help you here. Running xorcise serve in the foreground reproduces the failure directly, and xorcise doctor re-checks the prerequisites.
Cause. The static export the brain serves at /ui is stale or half-built. This only happens in a source checkout — an installed package ships the built UI.Fix. Confirm the brain is healthy with xorcise status and reprint the address with xorcise ui. up rebuilds the UI automatically when it needs to, so if the page is still broken, read ~/.xorcise/frontend-build.log for what the build said.

Run and telemetry problems

Cause. That phase means your agent has not started: XORCISE has seen no run-control call, no tailnet key fetch and no telemetry from it, so the run still reads Created. The run itself is healthy and its budget is counting down. If the budget runs out first, the run ends Never started and is not graded.Fix. Confirm your agent actually started and is talking to this run. Copy the hand-off again — Copy launch command on the live page, or xorcise run launch-cmd <run_id> — and check the entries below. If the fact strip shows the environment as Starting, wait for Ready before launching: an agent that joins early ends up on a tailnet whose targets do not exist yet.
Cause. Your agent has started — it called run-control or fetched its tailnet key, and the fact strip’s Started time says which — but none of its telemetry has arrived. The hand-off card reads Your agent has started — awaiting its first trace.Fix. The agent is running, so do not launch it again. Check its telemetry instead: launch mode and the exported environment are the usual causes, covered under No traces arrive at all below. An agent that never sends telemetry can still be graded on its artifacts and observed facts; Running without telemetry says what you lose.
Cause. Every lab mission runs its stack inside the run’s own container, so the host has to be able to run a container inside a container — at the platform of the mission’s image, which is not always your host’s own. XORCISE verifies that before creating a run and refuses with a diagnosis rather than failing partway through:
The CLI prints it after request failed (503): and exits 1.Fix. <what to do about it> is one of three fixes:
  • this mission's image is <platform>, but this host executes <host> natively and cannot run <platform> containers nested. The mission has no image for your host’s platform, so it would run under emulation, and this host cannot nest an emulated container. The usual case is an amd64-only mission on an arm64 Linux host. With no emulator installed, the probe’s container cannot start at all (exec format error). With a qemu emulator, it starts, but the Docker daemon inside it fails to set up its firewall (iptables: Failed to initialize nft). No setting changes this: choose a mission that publishes an image for your host’s platform, or run this one on a host of the platform it names.
  • enable Rosetta for x86/amd64 emulation in Docker Desktop. An Apple Silicon Mac was asked to run an amd64 image and Rosetta is off or failing. Turn on Docker Desktop’s Rosetta setting, make sure the VM is the Apple Virtualization framework, because Docker VMM has no Rosetta, then restart Docker Desktop.
  • Check that Docker can run privileged containers, then re-run 'xorcise doctor'. This host cannot nest even its own platform.
Run the probe yourself and read the remediation it prints:
The nested containers check boots a throwaway privileged container, and pulls its image on a cold host, so it can take a minute with no output. Wait rather than interrupting it. It runs only under doctor, never under xorcise up, because a host that cannot nest can still serve the web UI and run static missions.Run creation reads the verdicts the running server holds, and the server keeps each platform’s verdict, refusals included. When XORCISE is running, doctor asks the server to probe again and shows what the server now holds, so re-running xorcise doctor after you fix the host clears a cached refusal without a restart. It probes your host’s own platform and every other platform a run has asked about, each on its own nested containers (<platform>) line, then create the run again. With XORCISE stopped there is nothing cached to clear, so doctor probes in its own process, and only your host’s own platform. A server from before 0.2.1 cannot probe on request: doctor says so, and the fix is still xorcise down and then xorcise up.Set XORCISE_NESTED_CONTAINER_CHECK=skip only where the probe cannot run but nesting is known good — restricted CI, no privileged containers. It bypasses the check, not the requirement: a host that genuinely cannot nest will still fail, later and less clearly. XORCISE reads the setting when it starts, so it takes effect only after xorcise down and xorcise up, whether you set the variable or nested_container_check in ~/.xorcise/config.toml.
Cause. Every run’s subnet router logs in to the control plane at headscale_url. xorcise up built that URL from the host address it detected when it provisioned Headscale: the Docker bridge gateway on Linux, the LAN address on macOS, or XORCISE_HEADSCALE_HOST_IP if you set it. If that address changes afterwards, for example when a Mac joins another network or gets a new DHCP lease, the Headscale container stays healthy but nothing answers at the recorded address. No router can log in, so every lab run ends deploy_failed at the readiness window, and its recorded reason begins:
xorcise run status prints that reason, and the run page shows it. It appears whenever a run’s router never joined the tailnet, so a moved address is the likeliest cause, not the only one.Fix. Confirm it with xorcise doctor. Its control plane address check probes the recorded URL and, when the address up would detect today is different, names both:
Then restart XORCISE. up detects the address again and provisions the control plane on it:
down removes every run container, including those of runs still in progress, so let those finish first. If you set XORCISE_HEADSCALE_HOST_IP, up and doctor both use it instead of detecting the address, so change it to the new address before up. A failure that names no current address means XORCISE could not detect one, for example because Docker did not answer within five seconds. up runs the same detection and says what stopped it.
Cause. Launch mode, in the large majority of cases. Container mode bakes host.docker.internal into the telemetry endpoint, and a plain terminal cannot resolve that name — the exporter fails silently and not one span arrives.Fix. Ask for the mode that matches where the agent actually runs:
The live page has the same toggle, labelled Running the agent on:. An agent that genuinely runs in a container needs --add-host host.docker.internal:host-gateway instead.Two other causes account for most of the rest. A bare KEY=VALUE paste is not inherited by the child process — use set -a; source launch.env; set +a, or paste the single launch block, which already does that. And xorcise run launch-profile printing nothing at all means no collector host is configured for this run.
Cause. Telemetry reached XORCISE and was refused: the batch carried no run id XORCISE could read, or it named a run that is already sealed. The message ends …or run is sealed in both cases, and it is the definitive signal for this class of problem. Rejected batches are dropped, never blended into another run.Fix. Work down these in order.
  • xorcise.run_id was set as a span attribute. Only the resource attribute correlates; set it through OTEL_RESOURCE_ATTRIBUTES.
  • You already exported OTEL_RESOURCE_ATTRIBUTES and overwrote it. Append xorcise.run_id=<id> comma-separated instead.
  • The prompt’s xorcise.run_id=<id> marker line was deleted. For a harness with no telemetry provider that marker is the only correlation channel there is.
  • The run is already terminal. When a run ends, XORCISE keeps ingestion open for a short drain window, telemetry_drain_seconds (five seconds by default), then seals the run and rejects everything after that, by design. A rejected batch is dropped, never stored or graded, and a sealed run cannot be added to, so the lost tail cannot be backfilled. On the next run, have the agent flush its telemetry before it reports completion, shorten its export interval, or raise telemetry_drain_seconds.
  • The prompt was copied from an earlier run, so the marker names that run.
Most exporters never show you that response body. The OTLP receiver also counts every refusal by reason: curl http://127.0.0.1:4318/healthz (or the OTLP port xorcise up printed) returns a drops object, whose unroutable_* counters cover the first three causes above and sealed_* the last two. ~/.xorcise/serve.log has one otlp drop: line per refused batch. Send traces and logs explains each counter.
Cause. The logs exporter is not configured. XORCISE ingests two signals, and for some harnesses the readable content is entirely on the second one.Fix. Export logs as well as traces. Codex is the extreme case — its spans carry lengths and durations with no text, so a Codex run without logs replays as nothing at all. Claude Code needs the logs signal for the assistant’s own prose. The per-harness variables are on Send traces and logs.
Cause. One of the two telemetry warnings. span(s) matched no classification rule means the generic adapter rendered the run and recognised none of those span names, so it shows them as they arrived rather than as tool calls. none of the … carries a content-bearing attribute means the spans and log records carried names and ids but no commands, output or text, so the judge’s transcript was empty and it graded from the submitted artifacts alone.Fix. Fix the harness, then start a new run: both warnings describe what was sent, so a run already recorded cannot be repaired. If your harness is Claude Code, Codex or OpenHands, register the agent with that kind so its own adapter is used. For Claude Code, check the OTEL_LOG_* variables. When you drive the OpenHands SDK from your own code, export the variables before importing openhands.sdk. Send traces and logs has the per-harness detail.
Cause. The join script hands the one-time join key to tailscale up as a file, never as a command-line argument, because any user on the machine can read a process’s arguments with ps. The tailscale already on the agent’s PATH predates reading the key from a file, so the script stops rather than expose the key.Fix. Upgrade that client. On Linux you can instead run the pinned client the script fetches into its scratch directory, which installs nothing:
Cause. The join script keeps the join key, briefly, in a private directory it creates under XORCISE_TS_DIR, or under $TMPDIR or /tmp when that is unset. The named directory is owned by someone other than you or root, or other users can write to it without the sticky bit /tmp has, so one of them could swap the script’s directory for their own.Fix. Point XORCISE_TS_DIR at a directory only you can write to, or unset it. See Settings for the join script.
Cause. The prompt inside the launch command refers to $XORCISE_RUN_BEARER rather than carrying the run’s token, and the launch block exports that variable just before the command. The agent did not inherit it. That happens when only the command line was copied without the export lines, or when the command starts the agent in a container or on another machine and does not pass the variable through. Every call then sends an empty token and gets {"detail": "invalid run-control credential"}.Fix. Run the whole block from xorcise run launch-cmd <run_id>, or Copy launch command on the live page. If your command starts the agent elsewhere, pass the variable through, for example docker run -e XORCISE_RUN_BEARER. To start an agent by hand instead, give it the prompt from xorcise run prompt <run_id>, which carries the token inline.
Cause. Either name resolution or proxy mode. There is no DNS on the run’s tailnet, so a compose service name never resolves; and when the agent could not get a TUN device it joined in userspace mode, where the host has no direct route to target IPs.Fix. Reach targets by IP. The objective substitutes the resolved target IP for you, and the prompt lists the targets explicitly. In userspace mode, send every target connection through the SOCKS5 address the join script printed — it is randomised per join, so read it from the script’s output or from $XORCISE_SOCKS5 in the sourceable env file it writes. Do not hardcode a port, and configure the proxy explicitly in tools like pwntools rather than assuming a direct connection.
Cause. The mission environment did not come up inside its readiness window, so XORCISE closed the run out with the trigger deploy_failed. A run that ends this way is not graded: it has no score, and it is left out of every median and rate rather than counted against the agent. The same trigger also marks a run whose deploy never finished; see the not graded entry under Grading.Fix. Read what XORCISE recorded when it closed the run out: the live page shows it under the run’s status, and xorcise run status <run_id> prints it. The first line is the readiness gate’s verdict — the environment failed with the exit code it died with, or not ready within the readiness window with what was still missing: mission services still starting, an inner Docker daemon that never answered, or a subnet router that never joined the tailnet (for that one, see The control plane address is not answering above). Below it is the tail of the run container’s log, which is where a failed compose up says why, and the inner Docker daemon’s log while the container was still running. While the run is live, the environment chip’s tooltip carries the same reading. Every run loads its mission’s images inside its own container before the services start, which takes longer for a large mission on a busy host; if runs are closed out while they are still coming up, start fewer at once or raise readiness_timeout_seconds above its default of 300 seconds. If it fails repeatedly, the mission’s compose file is the place to look.
Cause. Once a run’s grading is over, XORCISE stops its container and removes its tailnet nodes. One of the two planes was unreachable at that moment, so part of the environment is still up. The error says which: container: is Docker, tailnet: is Headscale. Each half is released on its own, so the other one is normally gone already. The same state shows as environment not released under xorcise run status, as a releases: line in xorcise system, and as releasing with a non-zero release_attempts from the REST API. The recorded result is unaffected: the teardown runs after it.Fix. Bring the named plane back. Start Docker if it is down. For the local Headscale, xorcise doctor checks the control plane. A container that was only stopped comes back with docker start headscale (the name is headscale_container). Otherwise restart XORCISE with xorcise down, then xorcise up, which provisions it again — but down also removes the containers of any run still in progress, and the next start closes those runs out as crashed. xorcise up alone does nothing while XORCISE is already running. You do not need to touch the run: XORCISE retries the teardown on its own until it lands, backing off per run from 30 seconds to at most 10 minutes between attempts, and xorcise system stops listing it once it is released.
Cause. Deleting a run, or an agent with finished runs, tears down any environment that is still up before removing anything, because the run’s record is the only handle XORCISE keeps on it. Docker or Headscale is still unreachable, so that teardown failed again and the delete was refused rather than leave the environment running with nothing to clean it up.Fix. Bring the plane back as in the entry above, then delete again. To delete regardless, add --force to xorcise run delete or xorcise agent rm, or choose Delete anyway in the web UI. That accepts the leak: the container stays until XORCISE next stops or starts and reaps it, and nothing is left that can remove its tailnet nodes. A delete refused because a run has not finished or is still being graded cannot be forced — terminate the run, or wait for its result.

Grading problems

Cause. judge_status reads model-not-configured: no judge model is set, so XORCISE never called one. The judge half scored 0.0, and because that zero is still multiplied by 0.5, the run’s ceiling was 50%. A score of 42.5% here is half a score, not a failed agent. In the terminal, xorcise run status prints JUDGE DEGRADED — model-not-configured under the scores, xorcise run list marks the score with *, and xorcise leaderboard counts the run under No judge and leaves it out of the median.Fix. Configure a judge model — see Configuration — then re-evaluate the run. Re-grading replays the already-sealed evidence against your current settings. Do not run the agent again; nothing about the run needs to change.
Cause. judge_status reads unavailable and judge_detail carries the provider’s own message, usually about context length. A long lab run produces an enormous transcript — a measured ten-minute run produced a 369,000-token prompt against a 272,000-token ceiling — and when the call cannot be made, the score degrades to deterministic-only. xorcise run status prints JUDGE DEGRADED — unavailable under the scores, with that message in parentheses.Fix. Lower the per-span cap, which defaults to 2000 tokens and trims each span to a head-and-tail window while keeping every action:
Then re-evaluate the run. Moving to a larger-context judge model works equally well. If you turned the pre-flight transcript cap on and it is rejecting runs that would have fit, set --transcript-max-tokens 0 to switch it back off and let the provider’s real limit be the gate.
Cause. Sealing and grading are deliberately split. When a run ends, XORCISE waits a short drain window for the agent’s last spans, freezes the trace, and then grades — which takes as long as the judge takes.Fix. Wait and ask again. The scorecard appears by itself, and xorcise run status <run_id> re-checks from the terminal. Polling also re-drives grading that was lost to a restart, so a run stuck in this state past the judge’s timeout usually resolves by being asked once more.
Cause. The run ended before its agent started. Cancelled means someone terminated it while it still read Created. Never started means its budget ran out first. Either way XORCISE saw no run-control call, no tailnet key fetch and no telemetry from the agent, so there was nothing to grade. The run’s result reads not graded, it cannot be re-evaluated, and it is left out of every performance view. This is final: xorcise run status exits 0.Fix. Find out why the agent never checked in. Check that it was launched against this run, with this run’s prompt and launch profile — Copy launch command on the live page, or xorcise run launch-cmd <run_id> — and that it can reach the run-control address in the prompt; launch mode is the usual cause, covered under No traces arrive at all in Run and telemetry problems. Then start a new run, and watch it move from Created to Xorcising once the agent starts. Give it a budget long enough to cover the time before you launch it, because the budget counts from creation.
Cause. If the run reads Cancelled or Never started, its agent never started; see the entry above. Otherwise the run ended deploy_failed or crashed: an environment failure, not the agent, cut it short. deploy_failed means the mission environment never became ready or failed while the run was live — or that the deploy never finished, because the create failed or XORCISE stopped mid-deploy, and tearing down what it had started failed too. crashed means the run’s environment was gone when XORCISE restarted — a xorcise down, or a host or Docker restart, while the run was live. Neither is graded, so there is no score to wait for, and the run is left out of every median and rate. This is final: xorcise run status exits 0, not 3.Fix. Nothing about the agent needs fixing. Read the reason printed under the status, and the evidence recorded with it for a deploy_failed run, then start a new run. Re-evaluating does not help: the server refuses to grade such a run. For a crash, keep XORCISE and Docker up until a run finishes.The likeliest way to get a deploy_failed run whose deploy never finished is Headscale stopping partway through a run create. A Headscale that is already down fails the create with a 503 before anything is reserved, so it leaves no run. One that stops after that check fails the teardown too, so the run is kept as deploy_failed and its reason names the create’s error. The release sweeper retries the teardown until the environment is gone, and until then xorcise system counts the run among the finished runs that still hold their container or tailnet nodes. Once released, the run stays in the run list until you delete it.

Mission and library problems

Ingesting your own mission bundle is coming soon. In this release, missions come from the XORCISE library — browse them with xorcise mission list, install one with xorcise mission pull <id>, or use the Missions page. xorcise mission ingest is a stub that prints the coming-soon message and installs nothing, whatever arguments you give it, and the Ingest a bundle button on the Missions page opens a product preview rather than a file picker. The manifest rules below describe the format that feature will accept, so you can prepare bundles now — but nothing in this release installs a mission from a local bundle.
Cause. Either the library is switched off or its endpoint is unreachable. These look identical in the catalog and are not the same problem.Fix. Ask the command that separates them:
It reports the saved setting and a live probe as two distinct lines, so a saved preference is never mistaken for a verified connection. Turn it on with xorcise catalog connect, or the XORCISE Remote switch on the Settings page. The library needs no API key, account, or sign-in, so a missing credential is never the cause here.
Cause. A pull runs through several phases and only one of them moves bytes. Resolving the mission, preparing the image, extracting layers, downloading the bundle, and installing all report progress without a byte counter, and extraction of a multi-gigabyte image is slow and silent.Fix. Read the phase label rather than the byte count. A pull that is genuinely wedged can be cancelled with Ctrl-C, which cancels it server-side too and installs nothing. If the CLI gives up before the job does, it exits 3 — the pull is still running on the server.
Cause. The library now serves a different artifact for that mission than the one you installed — either its creator shipped a new version, or the fleet was rebuilt onto a newer base image. The state appears only when the recorded artifact and the catalog’s current one actually differ.Fix. One command, whichever reason caused it:
It re-pulls in place and atomically, so a failed update leaves the mission you already had. An install that already matches the library is an untouched no-op.
Do not delete the mission and pull it again. That was the old remedy and it is retired: it leaves a window where the mission is not installed, and mission update does the whole thing in one step.
Cause. A mission image is 1 to 3 GB. Uninstalling a mission keeps its image, and an update keeps the previous version, so unused images add up. When free space runs low where Docker stores images, xorcise doctor warns only … GB free at … and suggests reclaim space with: xorcise mission prune.Fix. List the mission images nothing uses any more, then remove them:
In the Web UI, Settings → Mission images does the same.Prune removes only XORCISE mission images, and never the image of an installed mission, of a run that has not finished, or of any container. If Docker refuses to remove an image, prune names it, removes the rest, and exits 1. The sizes it reports are an upper bound, because images built on the same base share layers.Do not use docker system prune -a for this. It removes every unused image on the host, not only XORCISE’s.Releases before 0.2.1 also left a Docker volume behind for every lab run: the run container’s own image store, often several GB. XORCISE now removes it with the run’s container, but the volumes earlier runs left carry no label, so XORCISE cannot tell them from anyone else’s and never removes them. docker system df -v shows them under local volumes, and docker volume prune removes the volumes no container uses. On Docker 23 and later that means only anonymous volumes; on older Docker it removes named ones too, so check the list first.
Cause. A mission’s image is fused onto a base image, and that base has a generation number. This build of XORCISE runs one generation and refuses artifacts older than it, rather than starting a run that would fail somewhere in the middle.Fix. Update the mission — the newer artifact is fused onto a base this build can run:
The reverse case is a warning from xorcise doctor: the library promotes a base generation newer than your install, so newly pulled missions would be refused. Upgrade XORCISE with pip install -U xorcise.
Cause. Missions publish images per platform, and this one publishes none your host can execute. XORCISE checks before downloading, so the refusal costs you no bandwidth:
Fix. Run that mission on a host of a supported architecture. There is no local workaround: the image for your platform does not exist.The commoner and benign case is the opposite one. When a mission has no native image for you but does publish linux/amd64, XORCISE runs it under your host’s emulation layer. The server logs a notice for the pull, Native ARM64 image unavailable for this mission. Running the AMD64 mission using compatibility/emulation mode., but the CLI never prints it. That is not an error. On an Apple Silicon Mac with Docker Desktop’s Rosetta on, the mission runs, more slowly. On an arm64 Linux host it does not: xorcise mission pull, xorcise run create and the web UI still download and install the image, but creating a run of it is refused, because this host cannot run an amd64 container nested. See Run creation refuses on nested containers under Runs and telemetry.To pin every pull and run to one platform regardless, set XORCISE_DOCKER_PLATFORM. It overrides the selection unconditionally, including for a mission that never validated that platform, so use it to reproduce a specific result rather than as a default.
The rules. Every model in the manifest forbids unknown keys, so a single stray or misspelled field anywhere is a hard validation error. schema_version must be 2.0 or 3.0, and a 3.0 manifest must carry the mission’s own SemVer in version while a 2.0 one must not. metadata.type is required and accepts only lab or static — the older genre labels such as ctf are dead. A lab mission requires an environment block, a static mission requires at least one attachment, and check weights must either all be declared and sum to 1.0 or all be omitted.Reading a validation error. A manifest that breaks any of those rules fails as ingest failed: invalid mission.json, and the messages are literal — they name the field path, for example invalid mission.json: checks.0. Compare that field against Mission manifest, which is the authoritative shape. Missions installed with xorcise mission pull are already valid, so nothing you install today can fail these rules.

Exit codes in scripts

XORCISE uses five exit codes: 0 success, 1 runtime failure, 2 usage error, 3 operation still in progress, and 130 interrupted.
Code 3 is the one that breaks naive automation. run status, run report, run terminate --wait, and mission pull return it when the work is still running server-side. A CI gate that treats every non-zero code as a failure reads a perfectly healthy in-progress run as broken. Poll until you get 0.
Code 0 also covers idempotent no-ops: up when already running, mission pull when the mission is already installed, and catalog connect when already connected. --json is available on 22 commands, and run status --json is always parseable because it always exits 0 and its envelope carries a status of created, xorcising or grading rather than a score.

Start clean

When you want to discard everything and begin again:
This stops the brain, tears down the local Headscale container along with its network and volume, reaps leftover run containers, and removes ~/.xorcise. Run it before pip uninstall xorcise if you are removing XORCISE entirely — the purge is what cleans up the Docker resources, and once the package is gone nothing knows how to find them.
--purge deletes all of ~/.xorcise, including .env — the only copy of your model API keys — the database, and every run, result, and trace. It asks for confirmation unless you pass --yes, and on a non-interactive shell without --yes it refuses rather than hanging on the prompt.

File a useful bug report

Include five things and most reports become reproducible immediately: the output of xorcise --version, the full output of xorcise doctor, the run id if a run is involved, the exact message you saw rather than a paraphrase of it, and the last few lines of ~/.xorcise/serve.log when the brain failed to start. If XORCISE printed unexpected error: … — re-run with XORCISE_DEBUG=1 for the full traceback, do that and attach the traceback.

Configuration

Judge model, ports, upgrades, and where each setting is written.

CLI reference

Every command, flag, and exit code in one place.