Skip to the content.

Docker and self-hosting

Docker is the recommended way to run Stageflow. Pull ghcr.io/tejasghutukade/stageflow and start the console with the command in Pull and run. The same page covers data-safety and ops for a self-hosted Host. A multi-stage Dockerfile at the repo root builds the image. On each unpublished package.json version published from main, .github/workflows/publish.yml pushes a multi-arch image to that registry (amd64 + arm64), then cosign-signs the digest keylessly. Building from a checkout still works with Compose or docker build.

See also Data directory, CLI reference, and MCP (including CLI-only capabilities and docker exec commands below).

Stage sandbox in Compose

The shipped docker-compose.yml sets STAGEFLOW_STAGE_CONTAINER_IMAGE=stageflow-bash:local and mounts /var/run/docker.sock. read, write, edit, and bash then run in a sibling container built from docker/Dockerfile.bash-sandbox. The Stageflow process still runs as the stageflow user. Compose starts that service as root only so docker/stageflow-entry.sh can join the socket's group, then drops to stageflow. There is no privileged and no extra capabilities.

That mount lets this container create and remove those sandbox containers on the host daemon. A plain docker run of the published image does not set the variable and does not mount the socket, so sandboxing stays off. To run Compose the same way, delete STAGEFLOW_STAGE_CONTAINER_IMAGE and the socket mount.

The hardened egress reference below is documentation only. It does not mount the socket.

Local try (Compose)

First-run checklist for the shipped docker-compose.yml. The compose file builds the local Dockerfile, mounts a named volume at /data, publishes port 3847, and sets STAGEFLOW_HOME=/data, STAGEFLOW_CREDENTIAL_HOME=/data, and TMPDIR=/data/tmp (STAGEFLOW_BIND=0.0.0.0 is already in the image). The image, Compose, and scripts/docker-smoke.sh set STAGEFLOW_CREDENTIAL_HOME to that data directory, so the volume is the container's operator store. A data directory is its own operator only when that override is set. This is a local try surface — not the hardened egress sandbox.

The compose service runs command: ["sf", "ui"] on purpose: operator console and MCP on the same Host. The image CMD is still sf mcp (headless); override to sf mcp only when you want a UI-less Host. Do not run both sf ui and sf mcp at once — they share port 3847.

Checklist

  1. No control token for this compose file. It publishes 127.0.0.1:3847:3847 and sets STAGEFLOW_ALLOW_UNAUTHENTICATED_BIND=1. The process inside the container still binds 0.0.0.0, which is what Docker port publish needs, and with the opt-in it starts without a bearer. Keep the publish address on 127.0.0.1. To require a token again, remove the opt-in and set STAGEFLOW_CONTROL_TOKEN (≥32 characters, no whitespace).

  2. Create .env from .env.example at the repo root. Fill provider and GitHub vars as needed (see below). Compose loads .env automatically; do not commit .env.

  3. Shell env overrides Compose .env. If you have empty GITHUB_TOKEN / GH_TOKEN (or other keys) exported in your shell, Compose substitutes those empties and blanks the container value. Unset them for the compose invocation when the file should win:

    env -u GITHUB_TOKEN -u GH_TOKEN docker compose up --build -d
    
  4. Start and wait for live:

    docker compose up --build -d
    curl -fsS http://127.0.0.1:3847/livez
    # → {"ok":true,"status":"live",…}
    
  5. Open the console at http://127.0.0.1:3847. With the shipped compose file, API calls do not need a bearer.

Prefer a file secret when the token must not appear in the process environment on the host:

openssl rand -hex 32 > .stageflow-control-token
chmod 600 .stageflow-control-token
# Point STAGEFLOW_CONTROL_TOKEN_FILE at that path inside the container
# (e.g. bind-mount the file and set STAGEFLOW_CONTROL_TOKEN_FILE=/run/secrets/…),
# or pass STAGEFLOW_CONTROL_TOKEN from `.env` as above.

Provider boot (Compose)

Host boot configures providers from STAGEFLOW_PROVIDER_<ID>_API_KEY / _FILE — not bare OPENROUTER_API_KEY / ANTHROPIC_API_KEY alone. For Compose local try, set for example:

See Providers — Non-interactive Host boot credentials. Optional: put custom OpenRouter models / maxTokens in /data/agent/models.json on the data volume (the sibling of the operator auth file, because STAGEFLOW_CREDENTIAL_HOME is /data).

Host GITHUB_TOKEN (clone / fetch)

Repository-bound materialization uses Host askpass. Set GITHUB_TOKEN (or GH_TOKEN) on the Host even for public repos — clone/fetch go through that helper. Stages still need explicit secrets: grants for their own git/gh use (next subsection).

Stage secrets vs Host env

Slot 6 stages get a curated environment — not a copy of Host process.env. Credentials reach a stage only when listed under secrets: (stage body or pipeline entry). GITHUB_TOKEN / GH_TOKEN default to GIT_ASKPASS + a materialised token file (no raw env). Use { name: GITHUB_TOKEN, as: env } when gh or MCP needs the env var (askpass stays set for plain HTTPS git). Full migration and YAML: migration-stage-environment.md, YAML catalog — Stage secrets.

gh is not in the base image

The published image does not install the GitHub CLI. Catalogs that requires: gh need a derived image (or an install step in your Dockerfile). Stageflow never installs tools from requires: automatically.

MCP servers in the container

The image has no .mcp.json, and you do not need one: add servers in Settings → Project MCP (form or Paste JSON). They are saved to /data/mcp/.mcp.json, with tokens moved to /data/secrets/mcp-secrets.json, both on the data volume. OAuth sign-in works too: after approving in the browser the callback lands on localhost, so paste that address back into the sign-in dialog. Signed-in servers work for agents and Check, not pipeline stages (use a bearer token secret there, and list it under the stage's secrets:).

Agents and Check fill ${NAME} from console secrets only. To use a variable that already exists in the container's environment, list its name in STAGEFLOW_MCP_ENV_ALLOW (comma-separated, for example -e STAGEFLOW_MCP_ENV_ALLOW=NOTION_KEY). Idle agent runtimes are released after 30 minutes; STAGEFLOW_AGENT_SESSION_IDLE_MINUTES changes that (0 keeps them). Full walkthrough, secrets rules and routes: MCP servers. Back these files up with the table in Precious vs disposable; sf backup does not include them.

Channels in the container

Communication channels need no extra setup in the image. Connections made in the console are written to /data/connections.yaml and the secrets to /data/channel-secrets.json, both on the data volume, so they survive restarts and rebuilds (they are lost only if you delete the volume). The image includes the optional Spectrum packages that iMessage and WhatsApp use. For a connection that should come from the environment instead (CI or a secrets manager), use env:NAME references in connections.yaml and pass the variables to the container. STAGEFLOW_CONNECTIONS_FILE can point the file elsewhere.

Catalog in the container

The image ships only examples/evals (the sample eval sources that POST /api/evals/sample installs), not the rest of the repo examples/ tree. Workshop saves and the default writable workspace catalog live under /data ($STAGEFLOW_HOME/workspace). For an external catalog, bind-mount it into the container and register / ensure that absolute path with the Host (STAGEFLOW_ENSURE_PROJECT_ROOTS, POST /api/projects, or local sf run ensure-then-start). Remotes cannot invent unknown absolute project_root values. See MCP — catalog roots. Compose has a commented volume-mount pattern in docker-compose.yml.

Repo-bound run (brief) {#repo-bound-run}

A task with repository + ref (or sf run --repository / --ref) materializes a Host worktree under /data/worktrees/<runId> ($STAGEFLOW_HOME/worktrees/…). On succeeded, Stageflow reclaims that worktree immediately (keeps run_branch; checkout_root may still be recorded after the directory is gone). Failed/cancelled runs keep their worktrees until retention SLIM — see Post-mortem debug bundle.

Eval engine in the container {#eval-engine}

The image installs the pinned eval engine (promptfoo 0.124.0, build arg PROMPTFOO_VERSION) at build time into /data/eval-engine/0.124.0 ($STAGEFLOW_HOME/eval-engine/<version>). It adds about 1.2 GB to the image. A new named volume is seeded from the image on first mount. An existing volume or a bind mount over /data hides the baked-in copy; run sf eval engine install inside the container to install it there. The engine is only used by evals and never contacts promptfoo servers (a network guard blocks *.promptfoo.app and *.promptfoo.dev). It needs Node 22.22.0 or newer; sf eval engine status inside the container shows the Node it sees. Run evals with sf eval run through docker exec, the console, or the MCP run_eval tool. A judge model for rubric graders must be anthropic/…, openai/… or openrouter/…; its key is read from the container's auth file (an api_key entry for that provider) or the environment variable of the same name. See Evals.

Entrypoint and HOME. The container starts as root only long enough for the entrypoint (/usr/local/bin/stageflow-entry.sh) to join the Docker socket's group when it is mounted, then it drops to the stageflow user (uid 10001) and sets HOME to that user's home directory, /data. Anything that writes under $HOME (the email store, npm during sf eval engine install) therefore writes to the data volume, not to /root. Started as a non-root user, the entrypoint runs the command unchanged.

Named volume

Compose mounts the named volume stageflow-data at /data. That volume holds the durable store (state.db, credentials, settings). Deleting the volume wipes irreplaceable state — use sf backup before destructive resets. See Precious vs disposable.

Image smoke (CI / local)

Automated gate that the image boots and becomes live — not sf doctor. From the repo root (Docker daemon required):

./scripts/docker-smoke.sh
# → builds with STAGEFLOW_BUILD_SHA from git, runs with a temp volume +
#   32+ char control token, polls http://127.0.0.1:<port>/livez until 200,
#   tears down container/volume, exits 0

Mandatory bar: GET /livez → 200. Optional stretch flags: --readyz, --check-build-sha (asserts /api/health build_sha matches the baked SHA when the control token is sent). CI runs the same script from the docker-smoke job in .github/workflows/ci.yml when packaging-related paths change on a PR (always on main).

Pull and run (GHCR) {#pull-and-run-ghcr}

Published image: ghcr.io/tejasghutukade/stageflow. This is the recommended way to run Stageflow. Tags per release: semver x.y.z and major.minor x.y (same digest). The current minor tag is 0.39.

This starts the console on this machine. The stageflow-data volume keeps providers and pipelines you save in the console. Pass sf ui because the image command is sf mcp. This command does not mount the Docker socket, so stage file tools run in the Stageflow container. Compose from a checkout is what turns the stage sandbox on.

docker run --rm --name stageflow \
  -e STAGEFLOW_ALLOW_UNAUTHENTICATED_BIND=1 \
  -p 127.0.0.1:3847:3847 \
  -v stageflow-data:/data \
  ghcr.io/tejasghutukade/stageflow:0.39 \
  sf ui

Open http://127.0.0.1:3847. Keep the published port on 127.0.0.1. The allow flag is only for that localhost publish: the process inside the container binds 0.0.0.0.

Prefer a digest pin and a control token when the port is not limited to this machine:

export STAGEFLOW_CONTROL_TOKEN="$(openssl rand -hex 32)"
# Prefer digest after pull, or pin directly:
#   docker pull ghcr.io/tejasghutukade/stageflow@sha256:<digest>
docker pull ghcr.io/tejasghutukade/stageflow:x.y.z
DIGEST="$(docker inspect --format='' ghcr.io/tejasghutukade/stageflow:x.y.z)"
# DIGEST looks like ghcr.io/tejasghutukade/stageflow@sha256:…
docker run --rm \
  -e STAGEFLOW_HOME=/data \
  -e STAGEFLOW_CREDENTIAL_HOME=/data \
  -e TMPDIR=/data/tmp \
  -e STAGEFLOW_CONTROL_TOKEN \
  -v stageflow-data:/data \
  -p 3847:3847 \
  "${DIGEST}"

The published image already sets STAGEFLOW_HOME and STAGEFLOW_CREDENTIAL_HOME to /data. Keep the credential override on that directory when the volume should be the operator store. When an existing login lives under another data directory, set STAGEFLOW_CREDENTIAL_HOME to that directory.

Verify cosign attestations with the snippet below. Prefer digest pins (@sha256:…) over floating tags for production.

To use a published image with the shipped compose file instead of a local build, set image: ghcr.io/tejasghutukade/stageflow:x.y.z (or a digest) and omit or skip build:.

Derived images

The base image does not install gh or other pipeline-specific CLIs. When a catalog’s requires: needs extra tools, derive:

FROM ghcr.io/tejasghutukade/stageflow:x.y.z
USER root
RUN apt-get update \
  && apt-get install -y --no-install-recommends <your-tools> \
  && rm -rf /var/lib/apt/lists/*
USER stageflow:stageflow

Stageflow does not build derived images from requires: automatically — requires tells operators (and sf doctor / preflight) what the image must provide.

Browser stages in the container {#browser-stages}

The published image has no browser: no Chromium, no Xvfb, no agent-browser. Stages with a browser: field need them, and a human login stage needs a browser the operator can see. Keeping them out keeps the default image small. docker/Dockerfile.browser is a recipe that extends the runtime image with chromium, xvfb, xauth, fonts, and agent-browser 0.38.2 (the version the browser feature was verified against). It runs as the same non-root stageflow user and bakes in no secrets.

Build the local runtime image:

docker build --target runtime -t stageflow:local .

Build the browser image on top of it:

docker build -f docker/Dockerfile.browser --build-arg BASE_IMAGE=stageflow:local -t stageflow:browser .

To extend a published release instead, pass --build-arg BASE_IMAGE=ghcr.io/tejasghutukade/stageflow@sha256:<digest>. Override --build-arg AGENT_BROWSER_VERSION=... only if you have verified another version.

Put the Host launch options in the Host config, not in pipeline YAML and not in env (the Host rejects unknown STAGEFLOW_* vars and ignores AGENT_BROWSER_*). Copy docker/browser.config.example.yaml to /data/config.yaml:

browser:
  executable_path: /usr/bin/chromium
  launch_args:
    - --no-sandbox

Run it with the config bind-mounted next to the data volume:

docker run --rm -e STAGEFLOW_CONTROL_TOKEN -v stageflow-data:/data -v "$PWD/docker/browser.config.example.yaml:/data/config.yaml:ro" -p 3847:3847 stageflow:browser sf ui

Notes:

An opt-in smoke test builds this recipe and checks it with real Chromium (tests/docker.browserRecipe.smoke.test.ts; needs Docker and several minutes):

STAGEFLOW_DOCKER_SMOKE=1 npx vitest run tests/docker.browserRecipe.smoke.test.ts

Container browser host (development) {#container-browser-host}

A second recipe, docker/Dockerfile.browser-sandbox, is not an image for Stageflow itself. It is the per-run browser sandbox used by browser.host: container (see Browser sessions). It holds Chromium, Xvfb, a TCP forwarder (socat), tini and the profile volume mount, and nothing else: no agent-browser, no Node, no Stageflow code, no tokens. It runs as uid 1000 and clears stale Chromium profile locks at start. docker/browser-sandbox-entrypoint.sh is its entry point.

How it differs from Dockerfile.browser:

docker/Dockerfile.browser docker/Dockerfile.browser-sandbox
Base The Stageflow runtime image debian:bookworm-slim
Contains Stageflow, Chromium, Xvfb, agent-browser Chromium, Xvfb, socat, tini only
Runs The whole Host, browsers on the Host's own virtual display One container per (owner scope, profile, run); the Host stays outside
Selected by Running this image as the Host, plus Host config executable_path and launch_args Host config browser.host: container

Build it (the build context is the docker folder):

docker build -f docker/Dockerfile.browser-sandbox -t stageflow-browser-sandbox:local docker

Host config keys involved (all in $STAGEFLOW_HOME/config.yaml, never in YAML pipelines or env):

browser:
  host: container            # local (default) or container
  owner_hosts:               # optional, per owner scope; unlisted scopes use browser.host
    local: container
  container:
    image: stageflow-browser-sandbox:local   # default
    shm_size: 1g
    memory: 1g               # orchestrator default 1g
    pids_limit: 512          # orchestrator default 512

Constraints:

Opt-in smoke (needs a Docker daemon and several minutes):

STAGEFLOW_DOCKER_SMOKE=1 npx vitest run tests/docker.containerBrowserHost.smoke.test.ts

Precious vs disposable

Path under $STAGEFLOW_HOME Verdict Why
state.db (+ -wal, -shm) Irreplaceable Every run, stage, envelope, event, verification record, and A2A rows. Only ever copy via sf backup.
agent/auth.json Irreplaceable Operator auth file. The image sets STAGEFLOW_CREDENTIAL_HOME to this data directory.
settings.json Irreplaceable Concurrency and credential-source choice.
connections.yaml, channel-secrets.json Irreplaceable Channel connections and the secrets typed into the console. Not covered by sf backup; copy them yourself and keep the secrets file private (0600).
memories/ Irreplaceable Memory pages created in the console. Not covered by sf backup; copy the directory yourself.
mcp/.mcp.json, secrets/mcp-secrets.json Irreplaceable Servers and tokens added in Settings → Project MCP. Not covered by sf backup; copy them yourself and keep the secrets file private (0600).
secrets/mcp-oauth.key, agent/mcp-oauth-encrypted Irreplaceable OAuth sign-ins for MCP servers. Keep the key and the store together; one without the other is useless. Not covered by sf backup.
agent/mcp-cache.json Disposable Tool lists cached from the last successful connect; rebuilt by Check.
agents/pi-sessions/ Situational Agent session transcripts; history lives in state.db.
runs/<runId>/ Situational Artifact bytes; records live in state.db.
a2a-artifacts/ Situational Opt in with sf backup --include-a2a-artifacts.
repos/, worktrees/, cache/ (includes cache/jiti) Disposable Re-clonable / rebuildable.
eval-engine/, eval-runs/ Disposable The installed eval engine (re-installable with sf eval engine install) and per-run engine work files, removed after each eval run. Eval results are in state.db.
backups/, restore-pending/ Disposable Products of backup/restore.

A volume snapshot of the whole data directory is not a substitute for sf backup — it is larger, includes disposable trees, and is taken while the WAL is moving.

Why cp state.db corrupts

Stageflow runs SQLite in WAL mode. Committed transactions often live in state.db-wal, not yet folded into state.db. Concurrent Host + stage workers hold their own connections.

Use sf backup (VACUUM INTO + verified archive) while the Host is up.

Backup / restore runbook

# Live backup (default archive includes credentials at mode 0600 — treat as a secret)
sf backup --json

# Restore requires Host down (CLI probes GET /livez; never autostarts)
sf restore "$STAGEFLOW_HOME/backups/stageflow-….tar.gz" --json

HTTP (control token, drive scope for backup create/download and restore):

Failed apply twice → restore.failed; readiness and API/MCP refuse until the marker is cleared. Previous store files are left in place (never silently served as a successful restore).

MCP tools for backup/restore are deferred; agents should use the HTTP path above while a Host is running. See MCP.

Local volume boundary

$STAGEFLOW_HOME must sit on a local filesystem. Boot refuses nfs / nfs4 / cifs / smbfs / fuse.sshfs (store_unsupported_filesystem) unless STAGEFLOW_ALLOW_NETWORK_STORE=1 (loud warning, unsupported). 9p / virtiofs warn only (Docker Desktop). Detection is Linux mountinfo longest-prefix; non-Linux skips. Overlay/bind-over-NFS may report a local fstype — prefer named volumes.

Rich health reports store_filesystem.

Writable paths and read_only: true

Supported writable set: $STAGEFLOW_HOME and a writable TMPDIR (also used as SQLITE_TMPDIR when unset). When TMPDIR / TMP / TEMP are unset or whitespace-empty, Host boot soft-defaults to $STAGEFLOW_HOME/tmp (creates it) then asserts writability. Boot still fails with tmpdir_unusable if that path (or an explicit TMPDIR) is not writable. Compose/images often set TMPDIR=/data/tmp explicitly — that remains valid.

Logging budget

Stdout is lifecycle-oriented. Per-line bytes are capped (STAGEFLOW_LOG_MAX_LINE_BYTES, default 8192) with truncation markers. Transcripts stay in the run store under retention. Prefer a Docker logging.driver: local with size rotation in compose.

Upgrade / rollback

Provenance

OCI label Value
org.opencontainers.image.source https://github.com/tejasghutukade/stageflow
org.opencontainers.image.revision $ — identical to STAGEFLOW_BUILD_SHA
org.opencontainers.image.version package.json version
org.opencontainers.image.created RFC 3339 build time
org.opencontainers.image.licenses MIT
org.opencontainers.image.title Stageflow
org.opencontainers.image.base.name / .base.digest pinned base

BUILD_SHA is process.env.STAGEFLOW_BUILD_SHA ?? "unknown" and appears on /api/health, MCP get_health, and sf --version --json. Publish sets build-arg STAGEFLOW_BUILD_SHA=$ so revision, the baked SHA, and health build_sha agree.

Buildx also emits provenance and SBOM (--provenance=true --sbom=true). Cosign keyless-signs the manifest-list digest after push (not tag-only).

Labels are hints; attestations are evidence. Anyone with push access can set labels. Verify the signed digest with cosign (identity is any workflow under this repo’s GitHub Actions OIDC issuer):

# Resolve digest from a tag, or paste the sha256 from the Actions log / GHCR UI:
DIGEST=sha256:<digest>
cosign verify \
  --certificate-identity-regexp '^https://github\.com/tejasghutukade/stageflow/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  "ghcr.io/tejasghutukade/stageflow@${DIGEST}"

Egress threat model (report-only)

Stages run with shell tools and inherit curated proxy/CA variables. Stageflow does not ship an allowlisting egress proxy. Health reports whether proxy vars are set (hostname only, never userinfo) and whether the Node EnvHttpProxyAgent dispatcher is installed.

Reference posture: Host on an internal: true network behind a domain-allowlisting forward proxy; NO_PROXY must include loopback for Host self-probes.

Hardened egress reference compose {#hardened-egress-reference-compose}

Documentation only — not a shipped root file. Do not replace docker-compose.yml with this unmarked. Adapt for your proxy image and secrets store; still never mount docker.sock.

services:
  stageflow:
    image: ghcr.io/tejasghutukade/stageflow@sha256:<digest>
    networks: [internal]
    environment:
      STAGEFLOW_HOME: /data
      STAGEFLOW_BIND: 0.0.0.0
      STAGEFLOW_CONTROL_TOKEN_FILE: /run/secrets/control_token
      STAGEFLOW_BUILD_SHA: "<git sha>"
      TMPDIR: /data/tmp
      HTTPS_PROXY: http://egress-proxy:3128
      HTTP_PROXY: http://egress-proxy:3128
      NO_PROXY: 127.0.0.1,localhost,egress-proxy
    volumes:
      - stageflow-data:/data
    read_only: true
    tmpfs: [/tmp]
    cap_drop: [ALL]
    security_opt: [no-new-privileges:true]
    logging:
      driver: local
      options: { max-size: "10m", max-file: "5" }
    restart: unless-stopped
  egress-proxy:
    image: <your-allowlisting-proxy>
    networks: [internal, egress]
networks:
  internal:
    internal: true
  egress:
volumes:
  stageflow-data:

CLI via docker exec {#cli-via-docker-exec}

A remote harness drives the Host over MCP/REST with a control token. Some CLI commands stay exec-only on purpose — see the decision table in MCP — CLI-only capabilities. Below are literal commands assuming the container is named stageflow (replace with your compose service / container id). Prefer catalog-relative paths the Host already knows; mount or bake catalog into the image as your deployment does.

Exec-only commands

# ASCII pipeline diagram (humans). Harnesses: MCP describe_pipeline / get_run.
docker exec -it stageflow sf graph --pipeline examples/hello-world/hello.pipeline.yaml

# Rewrite legacy YAML keys in a checkout (authors on a laptop; not a Host API).
docker exec -it stageflow sf migrate-yaml /workspace/my-catalog --write

# Durable skill install for image bake or a mounted workspace.
# Harnesses: prefer start_run.skills (Slot 9) instead of install.
docker exec -it stageflow sf skills install --from-path /workspace/skills/my-skill
docker exec -it stageflow sf skills install --from-zip "https://example.com/skill.zip" \
  --skill-name my-skill

# A2A deployment config (boot-read; restart Host after changes).
# Read whether A2A is enabled without exec: GET /api/a2a/status (read token).
docker exec -it stageflow sf a2a validate --config /data/a2a.yaml
docker exec -it stageflow sf a2a list --config /data/a2a.yaml
docker exec -it stageflow sf a2a add-caller procurement --config /data/a2a.yaml

# Eval engine, only if /data was mounted over the baked-in copy (see "Eval engine in the container").
docker exec -it stageflow sf eval engine install

# Provider OAuth (interactive / browser). API keys: boot env, not exec — see below.
docker exec -it stageflow sf providers login anthropic --type oauth

Provider API keys (no exec)

Set at Host boot so the container self-configures without docker exec:

# compose / k8s env (example)
STAGEFLOW_PROVIDER_ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key
# or STAGEFLOW_PROVIDER_ANTHROPIC_API_KEY=…
# optional: STAGEFLOW_REQUIRE_PROVIDERS=anthropic

See Providers — Non-interactive Host boot credentials.

The Host re-reads these variables on every start, so a restart restores a key supplied this way even after you replaced it or disconnected the provider in Settings → Providers. Remove the variable to keep a console change.

Failure sorting key. Sorting failed eval results uses TypeSafe Jev. Pass STAGEFLOW_PROVIDER_TYPESAFE_API_KEY (or _FILE) like any provider key, or TYPESAFE_API_KEY / JEV_API_KEY. docker-compose.yml forwards only the OpenRouter and Anthropic keys by name, so add the variable under its environment: block, or use docker run -e. Without a key, sorting can still use the run's judge model.

Worktree escape hatch {#worktree-escape-hatch}

When you need a shell inside a run's checkout (diff inspection, one-off git, post-mortem), obtain the path from the run projection, then exec:

  1. Call MCP get_run with the runId, or GET /api/runs/<runId> with a read token.
  2. Read binding.checkout_root (absolute path inside the container). Unbound runs omit it — there is nothing to enter. Repository-bound runs typically use $STAGEFLOW_HOME/worktrees/<runId>/. After a run reaches succeeded, that worktree may already have been reclaimed even though checkout_root remains recorded on the run — if the path is missing, MCP checkout tools soft-fail with checkout_reclaimed.
  3. Open a shell (or run a one-shot command) with that path as the working directory:
RUN_ID=run_…
# From the Host REST detail (token required when bind is non-loopback):
CHECKOUT=$(curl -fsS -H "Authorization: Bearer $STAGEFLOW_READ_TOKEN" \
  "http://127.0.0.1:3847/api/runs/$RUN_ID" | jq -r '.binding.checkout_root // empty')
test -n "$CHECKOUT"
docker exec -it -w "$CHECKOUT" stageflow bash
# one-shot example:
docker exec -it -w "$CHECKOUT" stageflow git status

There is no MCP shell/exec tool — this is the supported escape hatch.

Post-mortem debug bundle {#debug-bundle}

Distinguish succeed reclaim from retention SLIM/PURGE:

Pull a capped, Slot-6-redacted attachable bundle (export projection + manifest + stage events + verification evidence + get_run_diff + stream-log tails + redacted host config):

# Inside the container (or via docker exec):
docker exec stageflow sf debug-run "$RUN_ID" --out /tmp/debug-$RUN_ID.json

# Remote Host — same shape, read scope (parity with GET …/export):
curl -fsS -H "Authorization: Bearer $STAGEFLOW_READ_TOKEN" \
  "http://127.0.0.1:3847/api/runs/$RUN_ID/debug-bundle" \
  -o "debug-$RUN_ID.json"

For a live shell on the binding path after you have the bundle, use the worktree escape hatch above.

Continuous replication

Litestream (or similar) can replicate state.db for disaster recovery; it is not a substitute for sf backup under live multi-writer WAL. Prefer sf backup for operator restore points.