Skip to content

Configuration

Every cairntrace surface reads from a single config source: cairntrace.config.yml. The file is optional — a spec with absolute URLs runs without one — but anything project-specific (environments, services, secrets, retention, integrations) lives there. The schema is strict: unknown top-level keys are rejected, so a typo fails cairn config validate instead of silently being ignored.

bash
cairn config validate --json          # validate structure + cross-field rules
cairn config validate --config ./cairntrace.config.yml

File location resolution

The loader walks the file system upward from the spec file looking for the first cairntrace.config.yml. If your specs live in specs/flows/login.yml and the project root has the config, that's used. --config <path> overrides the lookup on every command that reads config.

Schema

yaml
# cairntrace.config.yml
version: 1                           # required, always 1
project: my-app                       # used in artifacts, breadcrumbs, MCP resource names
defaultEnvironment: local             # the env used when no --env is passed (default: local)
artifactRoot: ~/.cairntrace/runs      # override the default artifact root
workflowRoots: [./flows, ./checks]    # dirs cairn scans for specs (cairn explain / codemap)

environments:                         # required — at least one environment
  local:
    baseUrl: http://localhost:3000
    vars: { allowedCountry: US, retryCount: 3, useSandbox: true }
    viewport: { width: 1280, height: 800 }
  staging:
    baseUrl: https://staging.app.com
    services: false                    # disable all services for this env (app is remote)
    secrets: { provider: tvault, tvault: { group: payments, env: prod } }

secrets:                              # default secrets block (an env-level secrets replaces it)
  provider: tvault                    # env | tvault
  keys: [API_KEY, DB_URL]             # selected TinyVault values for this invocation
  required: [API_KEY, DB_URL]         # fail the run if these are unset/empty
  tvault:                             # required when provider: tvault
    project: my-app                    # direct mode — OR —
    # group: payments                  # inheritance mode (requires env)
    # env: prod

retention:
  keepRuns: 10                        # prune to newest N runs/spec after every run
  keepFailedRuns: 10                  # newest N failed/errored runs survive pruning anyway (default: 10)
  publish: { enabled: true, retentionDays: 7 } # explicit remote archive before pruning

report:
  theme: cairn                        # cairn | slate | midnight | contrast
  colors: { accent: "rgb(94,129,172)", danger: "#c0392b" }   # optional CSS token overrides

browser:                              # browser-backend tuning (agent-browser)
  verifyAfterClick: true              # confirm same-tab link delivery (default: true)
  postClickSettleMs: 20000            # opt in to network-idle after every click
                                      # click settleMs > spec settleMs > this value

webServer:                            # optional single-server lifecycle for `cairn run`
  command: "node .output/server/index.mjs"
  build: "bun run build"               # run once before command (skipped when reusing)
  url: http://localhost:3000           # readiness probe (defaults to the env baseUrl)
  waitForText: "listening on"          # …or treat ready when this hits stdout/stderr
  reuseExisting: true                  # reuse a server already answering (false in CI)
  readyTimeoutMs: 60000
  setup: ["bun run db:migrate"]        # run after ready, before specs
  teardown: ["rm -rf ./.cairntrace/tmp"]  # best-effort, after specs

services:                             # multi-service lifecycle (docker/seed/tmux) — see Services page
  docker: { command: "docker compose up -d", reuseExisting: true, readinessCheck: "curl -sf http://localhost:27017" }
  seed: { command: "yarn demo-import", ttlSeconds: 21600, freshnessCheck: "mongosh --quiet --eval 'db.count()' mydb" }
  tmux:
    session: myapp
    windows:
      - { name: web, cwd: web-app, command: "yarn serve", readyOn: { url: http://localhost:8080 } }
  teardown: ["tmux kill-session -t myapp", "docker compose down"]
  stash: { enabled: true, autoStash: always, capture: [tmux, docker, seed] }

stash:                                # fcheap run-artifact stash — see Stash page
  enabled: true
  autoStash: on-failure               # on-failure | never
  tags: [regression, audit]

clips:                                # default tags; clip points live in the spec
  tags: [regression]

investigate:                          # fcheap connect + vecgrep — see Investigate page
  codebaseDir: ./src                   # default codebase for `cairn investigate --connect`
  mode: hybrid                         # semantic | keyword | hybrid
  limit: 10
  index: false                         # build/refresh vecgrep before connecting
  autoInvestigate: on-failure          # on-failure | never

annotate:                             # codemap annotation — see Annotate page
  enabled: true
  autoAnnotate: on-run                 # on-run (pass+fail) | on-investigate | never
  source: cairntrace

The schema is .strict() at every level, so a misspelled key (e.g. runner: or run: — neither exists) is a validation error, not a silent no-op.

Agent-browser confirms same-tab link delivery from URL, document, or DOM evidence by default without waiting for network-idle. A positive click-step or spec-root settleMs, or browser.postClickSettleMs, opts into network-idle settling; click/spec values take precedence over config. Playwright honors explicit click/spec values and otherwise keeps its native waits. A resolved value of 0 skips both the extra settle and the link-delivery probe at that scope. browser.verifyAfterClick: false disables the agent-browser guard globally.

Environments

environments is a required record of name → EnvironmentConfig. Each environment can carry:

KeyEffect
baseUrlprepended to open: steps that begin with /; also ${baseUrl}
varssubstituted as ${vars.X} in specs (config env vars)
viewportbrowser viewport applied at run start (spec-level viewport: wins)
servicesfalse disables all services for this env; a partial services: block deep-merges over the top-level one
secretsreplaces the top-level secrets: block for this env entirely

The active environment is --env <name>, else defaultEnvironment, else local.

Placeholder resolution

For every ${baseUrl}, ${env.X}, ${vars.X}, or ${secrets.X} in a spec:

  • ${baseUrl} → the active environment's baseUrl.
  • ${env.X} → the invocation environment. With secrets.provider: tvault, only secrets.keys, secrets.required, and names referenced in the root spec or its imported actions are resolved; they never mutate global process.env. ${env.X:-default} resolves a default expression when the var is missing or empty; nested placeholders inside the default work.
  • ${vars.X} → merged vars, in priority order: CLI --var key=value (highest) > spec vars: > environments.<env>.vars.
  • ${secrets.X} → the secrets bag (env or tvault-resolved).

Each placeholder is resolved exactly once and emitted verbatim — a value that itself contains ${...} stays inert, so there is no cross-secret injection. An unresolved ${vars.X} fails at parse time with a typed error pointing at the spec line.

Where run-time settings actually live

Several settings that look like config actually live on the spec, not cairntrace.config.yml:

  • backend, mode, viewport, vars, environment, settleMs, coldStart, preconditions, session, redaction, metadata, artifacts (capture policies, video, clip points) — all spec-root keys.
  • redaction: on a spec is { headers?, queryParams?, storageKeys?, values? } (arrays of strings to scrub), not a regex list.

Backend choice and capture policies are per-spec because they describe what this flow observes, not project plumbing. Project plumbing (environments, services, secrets, retention, integrations) is what goes in config.

Validation

cairn config validate runs the zod schema plus cross-field .refine() rules:

  • secrets.provider: tvault requires a tvault: block with either project (direct) or group+env (inheritance) — not both.
  • tmux window names must be unique within a session.
  • A tmux window with readyOn must specify at least one of url or text.
  • An xlsx verifier (spec-side) requires sheets or validations.

Run it in CI before cairn run so a malformed config fails fast instead of mid-run.

See also

Local-first browser specs for coding agents. Released under the MIT License.