> ## Documentation Index
> Fetch the complete documentation index at: https://comis-feature-matrix-channel.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Environment Variables

> Complete reference for all Comis environment variables

**What this is for:** every environment variable Comis reads — provider API keys, channel tokens, encryption secrets, infrastructure paths. **Who it's for:** anyone wiring API keys, deploying to Docker, or running CI pipelines.

Comis reads 40+ environment variables across encryption, LLM providers, voice/media, web search, channel credentials, and infrastructure. Most variables are **also referenceable from `config.yaml` via `${VAR_NAME}` substitution** — you typically pick one location per credential. The canonical templates are [`/.env.example`](https://github.com/comisai/comis/blob/main/.env.example) (host install) and [`/.env.docker.example`](https://github.com/comisai/comis/blob/main/.env.docker.example) (Docker Compose).

<Tip>
  The interactive setup wizard (`comis init`, `comis configure`) populates the most common variables for you and writes them to `~/.comis/.env`. Manual editing is only needed for advanced or non-interactive setups.
</Tip>

## Comis Variables

### `COMIS_CONFIG_PATHS`

| Property    | Value                                             |
| ----------- | ------------------------------------------------- |
| **Used by** | Daemon, CLI                                       |
| **Format**  | Colon-separated file paths                        |
| **Default** | `~/.comis/config.yaml:~/.comis/config.local.yaml` |

Specifies which YAML configuration files to load, as a colon-separated list (like `PATH`). Files are loaded in order and deep-merged, with later files overriding earlier ones. Non-existent files are silently skipped. The daemon reads this variable at startup to locate config files. The CLI uses it in `config validate` and `pm2 setup` commands. For full details on the config-path syntax and merge behavior, see [Configuration](/installation/configuration).

```bash theme={}
export COMIS_CONFIG_PATHS="$HOME/.comis/config.yaml:$HOME/.comis/config.local.yaml"
```

### `COMIS_DATA_DIR`

| Property    | Value                   |
| ----------- | ----------------------- |
| **Used by** | Daemon                  |
| **Format**  | Absolute directory path |
| **Default** | `~/.comis`              |

Overrides the base data directory where Comis stores its databases, logs, and runtime files. When set, the daemon uses this path instead of the default `~/.comis` directory. This is useful for containerized deployments or when running multiple instances.

```bash theme={}
export COMIS_DATA_DIR="/var/lib/comis"
```

<Note>
  Durable [terminal-driver](/agent-tools/terminal-driver) drives (`drive.durable: true`) persist their resume journals under this directory (`terminal-drive/<agentId>/journals/` — see the [data directory reference](/operations/data-directory)). They use **no dedicated environment variable**: the journals are content-free files under the data dir, so `COMIS_DATA_DIR` (or the default `~/.comis`) is the only knob that relocates them.
</Note>

### `COMIS_GATEWAY_URL`

| Property    | Value                                                                             |
| ----------- | --------------------------------------------------------------------------------- |
| **Used by** | CLI                                                                               |
| **Format**  | WebSocket URL                                                                     |
| **Default** | Resolved from `~/.comis/config.yaml` gateway section, or `ws://localhost:4766/ws` |

Overrides the WebSocket URL the CLI uses to connect to the daemon gateway. The CLI normally reads the gateway host and port from the config file on disk, but this variable takes precedence when set.

```bash theme={}
export COMIS_GATEWAY_URL="ws://192.168.1.100:4766/ws"
```

### `COMIS_GATEWAY_TOKEN`

| Property    | Value                                                      |
| ----------- | ---------------------------------------------------------- |
| **Used by** | CLI                                                        |
| **Format**  | String (bearer token)                                      |
| **Default** | Resolved from first token secret in `~/.comis/config.yaml` |

Overrides the bearer token the CLI sends when connecting to the daemon gateway. The CLI normally extracts the first token secret from the gateway config file, but this variable takes precedence.

```bash theme={}
export COMIS_GATEWAY_TOKEN="my-secret-token"
```

### `COMIS_GATEWAY_HOST`

| Property    | Value                                    |
| ----------- | ---------------------------------------- |
| **Used by** | Daemon                                   |
| **Format**  | Hostname or IP address                   |
| **Default** | Value of `gateway.host` in `config.yaml` |

Overrides `gateway.host`. Applied via `buildGatewayEnvLayer()` at lower priority than `config.yaml`. Use `0.0.0.0` to bind all interfaces (e.g., Docker deployments).

Source: `packages/core/src/config/env-layer.ts:27`

```bash theme={}
export COMIS_GATEWAY_HOST=0.0.0.0
```

### `COMIS_GATEWAY_PORT`

| Property    | Value                                    |
| ----------- | ---------------------------------------- |
| **Used by** | Daemon                                   |
| **Format**  | Integer 1–65535                          |
| **Default** | Value of `gateway.port` in `config.yaml` |

Overrides `gateway.port`. Must be a valid integer 1–65535; non-numeric values are dropped silently.

Source: `packages/core/src/config/env-layer.ts:28`

```bash theme={}
export COMIS_GATEWAY_PORT=4766
```

### `COMIS_INSECURE`

| Property    | Value                           |
| ----------- | ------------------------------- |
| **Used by** | CLI                             |
| **Format**  | `1` to enable, unset to disable |
| **Default** | (unset)                         |

When set to `1`, allows the CLI to send bearer tokens over unencrypted WebSocket connections to non-localhost hosts. Without this variable, the CLI refuses to send credentials over cleartext connections to remote hosts as a security measure.

<Warning>Setting this variable bypasses transport security checks. Use only for development or when TLS termination happens at a reverse proxy. Never use in production without a TLS layer.</Warning>

```bash theme={}
export COMIS_INSECURE=1
```

### `COMIS_LOG_PATH`

| Property    | Value                         |
| ----------- | ----------------------------- |
| **Used by** | CLI (`daemon logs` command)   |
| **Format**  | Absolute file path            |
| **Default** | `~/.comis/daemon.console.log` |

Overrides the file that the `comis daemon logs` command reads from when systemd is not available. That file is the daemon's **raw stdout/stderr capture** (`daemon.console.log`), written by the CLI when it spawns the daemon in direct mode — not the structured JSON log. For the structured application logs, read `~/.comis/logs/daemon.1.log` directly.

```bash theme={}
export COMIS_LOG_PATH="/var/log/comis/daemon.console.log"
```

### `COMIS_OFFLINE`

| Property    | Value                           |
| ----------- | ------------------------------- |
| **Used by** | Daemon, CLI                     |
| **Format**  | `1` to enable, unset to disable |
| **Default** | (unset)                         |

When set to `1`, disables outbound network calls to fetch optional tooling at startup (e.g. binary downloads, model lists, MCP server discovery). Use this for air-gapped deployments. Configured channels and provider APIs continue to work — `COMIS_OFFLINE` only gates *provisioning* traffic, not runtime LLM/channel calls.

```bash theme={}
export COMIS_OFFLINE=1
```

### `COMIS_TRAJECTORY_DIR`

| Property    | Value                                                            |
| ----------- | ---------------------------------------------------------------- |
| **Used by** | Daemon                                                           |
| **Format**  | Absolute directory path                                          |
| **Default** | Value of `observability.trajectory.dirOverride` in `config.yaml` |

Overrides `observability.trajectory.dirOverride`. Relocates runtime trajectory JSONL files to a custom directory. Empty or whitespace values are dropped silently.

Source: `packages/core/src/config/env-layer.ts:30`

```bash theme={}
export COMIS_TRAJECTORY_DIR=/var/log/comis/traces
```

### `COMIS_DISABLE_RECALL_TRACE`

| Property    | Value                               |
| ----------- | ----------------------------------- |
| **Used by** | Daemon                              |
| **Format**  | `1` to disable, unset to use config |
| **Default** | (unset)                             |

Set to `1` to hard-disable the recall trace recorder regardless of `config.yaml`. Secondary escape hatch alongside `diagnostics.recallTrace.enabled: false`.

Source: `packages/observability/src/recall-trace/runtime.ts:304`

```bash theme={}
export COMIS_DISABLE_RECALL_TRACE=1
```

### `COMIS_CONFIG_AUDIT_LOG`

| Property    | Value                              |
| ----------- | ---------------------------------- |
| **Used by** | Daemon                             |
| **Format**  | Absolute file path                 |
| **Default** | `~/.comis/logs/config-audit.jsonl` |

Overrides the full file path of the config-audit JSONL log.

Source: `packages/observability/src/config-audit/log-path.ts:22`

```bash theme={}
export COMIS_CONFIG_AUDIT_LOG=/var/log/comis/config-audit.jsonl
```

### `COMIS_BROKER_TOKEN`

| Property    | Value                                      |
| ----------- | ------------------------------------------ |
| **Used by** | Exec tool (daemon-spawned subprocess)      |
| **Format**  | Opaque bearer token string                 |
| **Default** | (set internally by the daemon per-command) |

**Internal use only.** A single-use proxy token injected into the `exec` tool's spawn environment by the daemon's broker-activation wiring. Consumed by the credential broker via `Proxy-Authorization: Bearer`. Set internally by the daemon per-command — operators do not set this variable directly. Documented here so operators diagnosing broker 407 errors can understand where the token originates.

Source: `packages/daemon/src/wiring/setup-broker-activation.ts:79`

### `COMIS_CAP_LEASE`

| Property    | Value                                                                |
| ----------- | -------------------------------------------------------------------- |
| **Used by** | The jailed `orchestrate(script)` surface (daemon-spawned, sandboxed) |
| **Format**  | Opaque bearer token string                                           |
| **Default** | (set internally by the daemon per root-run)                          |

**Internal use only.** The capability-lease bearer the daemon mints per root-run and injects into the jailed script surface's spawn environment so it can authenticate to the loopback capability endpoint (`COMIS_ORCH_SOCKET`). Multi-use, bounded, revocable, and audience-bound — a captured lease cannot be replayed at a foreign method. Registered with the OutputGuard at mint so it is never logged. Set internally by the daemon — operators do not set this variable directly.

**Trusted-source preservation:** this var rides the daemon's own trusted injection path (the `placeholders` slot, merged last). The terminal-driver / exec env scrub **preserves** `COMIS_CAP_LEASE` on that inherited path while **fail-closed-blocking** the entire `COMIS_*` prefix from an untrusted workspace-loaded `.env` (see [the env-scrub note below](#child-env-scrub-jailed-surfaces)), so attacker-supplied workspace content can never forge or smuggle a lease into the jail.

### `COMIS_ORCH_SOCKET`

| Property    | Value                                                                |
| ----------- | -------------------------------------------------------------------- |
| **Used by** | The jailed `orchestrate(script)` surface (daemon-spawned, sandboxed) |
| **Format**  | Absolute Unix-socket path (mode 0600)                                |
| **Default** | (set internally by the daemon per jail)                              |

**Internal use only.** The filesystem path of the loopback capability endpoint's Unix socket (`0600`, owner-only), bind-mounted into the jail and injected alongside [`COMIS_CAP_LEASE`](#comis_cap_lease). The jailed surface dials this socket, presents the lease bearer, and the endpoint resolves the lease's capabilities and dispatches through the same handler gates as every other origin. Set internally by the daemon per jail — operators do not set this variable directly. Like `COMIS_CAP_LEASE`, it rides the trusted inherited path and is preserved by the env scrub while the `COMIS_*` prefix is blocked from a workspace `.env`.

### `COMIS_AGENT_BIN`

| Property    | Value                                                                  |
| ----------- | ---------------------------------------------------------------------- |
| **Used by** | The jailed `orchestrate(script)` surface (daemon-spawned, sandboxed)   |
| **Format**  | Absolute path to the in-jail `comis-agent` binary                      |
| **Default** | (set internally by the daemon per jail, only when the binary resolves) |

**Internal use only.** The in-jail path of the thin, capability-scoped [`comis-agent` CLI](/reference/comis-agent-cli) binary. The daemon binds the binary **read-only** (`--ro-bind`) into the jail and sets `COMIS_AGENT_BIN` to that same path so the in-jail CLI resolves. The binary is **sha256-pinned** against a committed build manifest: at jail construction the daemon re-hashes the bound bytes and **refuses to bind a mismatched/tampered binary** (a writable or swapped binary would be a host-RCE vector — the bind is always read-only). **Honest-degrade:** when the binary is missing or its hash does not match the pin, the daemon emits a loud WARN, leaves `COMIS_AGENT_BIN` **unset**, and the `comis-agent` CLI surface is unavailable — the `orchestrate` **script** surface still works. Set internally by the daemon per jail — operators do not set this variable directly. Like `COMIS_CAP_LEASE` / `COMIS_ORCH_SOCKET`, it rides the trusted inherited path (merged after the scrub) and is preserved while the `COMIS_*` prefix is blocked from a workspace `.env`.

### Child-env scrub (jailed surfaces)

When the daemon spawns a sandboxed child — a driven CLI via the [terminal-driver](/agent-tools/terminal-driver), or the jailed `orchestrate(script)` surface — it **scrubs the spawn environment** before the jail starts, because bubblewrap inherits the spawner env by default. The scrub is a blocklist (strip the known-dangerous keys, keep the rich env a TUI needs):

* **Interpreter-control vars are stripped unconditionally** — `NODE_OPTIONS`, `BASH_ENV`, `ENV`, `PYTHONSTARTUP`, `RUBYOPT`, `PERL5OPT`, the JVM `*_OPTIONS` set, and the prefix families **`LD_*`**, **`DYLD_*`**, **`PIP_*`**, **`UV_*`**. These instruct a runtime to load attacker-controlled code at startup (dynamic-linker preload, package-index redirection) → remote code execution, so they never reach the child regardless of where they came from.
* **The `COMIS_*` prefix is fail-closed-blocked from an untrusted workspace `.env`.** A `COMIS_`-prefixed key loaded from workspace content is dropped, so attacker-supplied workspace content cannot forge a runtime-control variable (e.g. a fake `COMIS_CAP_LEASE`) into the jail.
* **The daemon's own injection is preserved.** `COMIS_CAP_LEASE` / `COMIS_ORCH_SOCKET` / `COMIS_AGENT_BIN` set by the daemon ride a separate trusted path and **survive** the scrub — blocking them would break the capability socket or the in-jail `comis-agent` CLI. Only the untrusted workspace source is blocked; the trusted daemon-injected source is allowed.

Source: `packages/skills/src/tools/builtin/terminal-driver/terminal-env-scrub.ts`

### Microsoft Teams live-test seams (test-only)

<Warning>
  These two variables exist ONLY to point the Microsoft Teams channel at a local
  loopback emulator for live testing (see the [self-drive
  emulator](/channels/msteams#local-emulator-self-drive-testing)). They are
  **unset in production** — with them unset the daemon behaves identically to a
  normal Teams deployment (the live Bot Framework JWKS validator + the global
  fetch). **Never set them on a production daemon.** Neither relaxes a security
  control: the inbound seam swaps in a full local-JWKS signature/issuer/audience
  verify, and the outbound seam only redirects the network egress of the
  already-allowlisted Connector host — the `isSafeServiceUrl` host gate is
  untouched.
</Warning>

#### `COMIS_MSTEAMS_TEST_JWKS`

| Property    | Value                                        |
| ----------- | -------------------------------------------- |
| **Used by** | Daemon (msteams ingress)                     |
| **Format**  | Absolute path to a public JWKS JSON file     |
| **Default** | (unset) — the live Bot Framework remote JWKS |

When set, the Teams ingress verifies inbound activity tokens against the local
JWKS at this path (the emulator's public signing key) instead of the remote Bot
Framework keys. A full RS256 signature + issuer + audience verify — not a bypass.

Source: `packages/daemon/src/wiring/msteams-test-seams.ts`

```bash theme={}
export COMIS_MSTEAMS_TEST_JWKS=/tmp/comis-msteams-jwks.json
```

#### `COMIS_MSTEAMS_TEST_CONNECTOR`

| Property    | Value                                              |
| ----------- | -------------------------------------------------- |
| **Used by** | Daemon (msteams adapter)                           |
| **Format**  | Loopback base URL (`http://127.0.0.1:<port>`)      |
| **Default** | (unset) — the global `fetch` to the real Connector |

When set, the Teams adapter's outbound Connector + token-mint calls are
redirected to this loopback base (path + query preserved) — the programmatic
equivalent of a hosts-override to the loopback emulator. The `isSafeServiceUrl`
host allowlist still runs on the real `smba.trafficmanager.net` serviceUrl first;
only the transport is redirected after the gate passes.

Source: `packages/daemon/src/wiring/msteams-test-seams.ts`

```bash theme={}
export COMIS_MSTEAMS_TEST_CONNECTOR=http://127.0.0.1:53999
```

## Secret Variables

### `SECRETS_MASTER_KEY`

| Property    | Value                                                                   |
| ----------- | ----------------------------------------------------------------------- |
| **Used by** | Daemon, CLI                                                             |
| **Format**  | 64-character hex string or base64-encoded 32 bytes                      |
| **Default** | Auto-generated on first boot and written to `~/.comis/.env` (mode 0600) |

The AES-256-GCM master encryption key used to encrypt and decrypt secrets stored in `secrets.db`. The key is auto-generated on first boot — you do not need to run `comis secrets init` for a fresh install. The daemon and CLI both resolve this key in the same order: first from the environment, then from `~/.comis/.env`.

Providing this variable explicitly overrides the auto-generated value.

<Warning>Back up `~/.comis/.env`. This file is the only copy of the auto-generated master key. Losing it makes `secrets.db` permanently unreadable — AES-256-GCM has no key escrow or recovery path.</Warning>

```bash theme={}
# Auto-generated on first boot — back up ~/.comis/.env immediately
# To override with a specific key:
export SECRETS_MASTER_KEY="a1b2c3d4e5f6..."
```

To run without the encrypted store, set `security.storage: env` (or `file`) in
`config.yaml`. The daemon emits a startup WARN when a non-encrypted storage mode
is active.

## OAuth Variables

### `OAUTH_OPENAI_CODEX`

| Property    | Value                                                                                         |
| ----------- | --------------------------------------------------------------------------------------------- |
| **Used by** | Daemon (one-time bootstrap when the OAuth credential store has no profile for `openai-codex`) |
| **Format**  | JSON: `{"access":"...","refresh":"...","expires":<epochMs>,"accountId":"...","email":"..."}`  |
| **Default** | (none)                                                                                        |

Pre-seeds the OAuth credential store with a profile for `openai-codex` on the **first** daemon start when the store is empty. After bootstrap, the stored profile is the source of truth — subsequent daemon starts ignore this env var even if its value changes.

```bash theme={}
# Example: pre-seed via env var (CI / Docker)
export OAUTH_OPENAI_CODEX='{"access":"EXAMPLE-access","refresh":"EXAMPLE-refresh","expires":1735689600000,"accountId":"EXAMPLE-acct","email":"user@example.com"}'
```

**Bootstrap precedence:**

1. **Stored profile wins.** If the OAuth credential store already has a profile for `openai-codex`, the daemon uses it and ignores `OAUTH_OPENAI_CODEX` entirely.
2. **Env var is a one-time seed.** When the store is empty for the provider, the daemon parses the env var and writes the profile. Subsequent token refreshes update the stored profile, not the env var.
3. **WARN once on drift.** If the env-var refresh token differs from the stored profile's refresh token, the daemon emits one WARN per (provider, process) with `errorKind: "config_drift"` and `hint: "env-override-ignored"`. Operators clear the drift by either:
   * Running `comis auth logout --profile <id>` then restarting the daemon to re-bootstrap from env, **or**
   * Deleting the env var if the stored profile is the intended source of truth.

<Warning>This env var contains an OAuth refresh token. Treat it like a password. Use your CI / orchestrator's secret-store integration; never paste it into config files or commit it to version control.</Warning>

For the full bootstrap-precedence walkthrough including drift-handling examples, see [OAuth concepts → Env-var bootstrap precedence](/security/oauth#env-var-bootstrap-precedence).

## Standard Variables

### `NODE_ENV`

| Property    | Value                               |
| ----------- | ----------------------------------- |
| **Used by** | Node.js runtime                     |
| **Format**  | `development`, `production`, `test` |
| **Default** | (unset)                             |

Standard Node.js environment variable. Affects behavior of some dependencies and may influence logging verbosity. Comis does not require this variable to be set.

```bash theme={}
export NODE_ENV=production
```

## OpenTelemetry Variables

These are the **standard** OpenTelemetry SDK environment variables, honored by the
opt-in `@comis/observability-otel` extension (the SDK reads them; Comis does not
store them). They apply **only** when `observability.otel.enabled` or
`observability.prometheus.enabled` is set in `config.yaml`. The `config.yaml`
values take precedence for the keys Comis controls. None carry secrets or content.
See [Prometheus & Grafana](/operations/prometheus-grafana) for the full surface.

### `OTEL_EXPORTER_OTLP_ENDPOINT`

| Property    | Value                                        |
| ----------- | -------------------------------------------- |
| **Used by** | Daemon (OTel extension, when `otel.enabled`) |
| **Format**  | URL (e.g. `http://localhost:4318`)           |
| **Default** | (unset — the OTel SDK's own default)         |

The OTLP collector endpoint for traces/metrics/logs push. Used only when the
`observability.otel.endpoint` config key is empty; the config value takes
precedence when set.

```bash theme={}
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
```

### `OTEL_SEMCONV_STABILITY_OPT_IN`

| Property    | Value                                                          |
| ----------- | -------------------------------------------------------------- |
| **Used by** | Daemon (OTel extension, when `otel.enabled`)                   |
| **Format**  | Comma-separated tokens; `gen_ai_latest_experimental` to opt in |
| **Default** | (unset — pre-1.36 GenAI convention shape)                      |

Opts into the latest (pre-stable `Development`) GenAI semantic convention. Set it
to `gen_ai_latest_experimental` alongside `observability.otel.genaiSemconv: true`.
The convention will churn until it stabilizes. Content never leaks regardless — the
exporter re-redacts independently and the message-content attributes are omitted
unless `otel.captureContent` is separately enabled.

```bash theme={}
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
```

### `OTEL_SERVICE_NAME`

| Property    | Value                                        |
| ----------- | -------------------------------------------- |
| **Used by** | Daemon (OTel extension, when `otel.enabled`) |
| **Format**  | Service identifier string                    |
| **Default** | `comis`                                      |

Overrides the `service.name` resource attribute on every emitted signal.

```bash theme={}
export OTEL_SERVICE_NAME=comis
```

## Development and Regression Variables

These variables gate an **optional** developer regression suite. They are **never
required for normal operation** — when they are unset the suite self-skips, so a
default `pnpm test` / `pnpm validate` run makes no provider call and incurs no
cost. They are read only at the test boundary; provider keys live in a git-ignored
`scripts/lcd-regression.env` (the committed `scripts/lcd-regression.env.example`
documents them) and are never logged or committed.

### `COMIS_LCD_REGRESSION`

| Property    | Value                                                                          |
| ----------- | ------------------------------------------------------------------------------ |
| **Used by** | The env-gated real-LLM regression driver (`real-llm-regression.bench.test.ts`) |
| **Format**  | `1` to enable, unset to skip                                                   |
| **Default** | (unset → the suite is skipped)                                                 |

Master gate for the Lossless Context DAG real-LLM regression driver. When
unset, the entire suite is skipped (no provider call, no network). When set, the
driver runs a tool-free baseline probe + a single-read probe and records the
`stream:context` cache-trace.

```bash theme={}
export COMIS_LCD_REGRESSION=1
```

### `COMIS_LCD_ANSWER_PROVIDER` / `COMIS_LCD_ANSWER_MODEL` / `COMIS_LCD_ANSWER_API_KEY`

| Property    | Value                                          |
| ----------- | ---------------------------------------------- |
| **Used by** | The env-gated real-LLM regression driver       |
| **Format**  | Provider id / model id / API key strings       |
| **Default** | (unset → the provider-backed probes self-skip) |

The answer-model lane for the regression driver. All three must be set for the
provider-backed probes to run; absent any one, the probes self-skip even when
`COMIS_LCD_REGRESSION` is set. The key is forwarded to the provider's typed
`apiKey` option and is never stored, logged, or echoed.

### `COMIS_LCD_LIMIT`

| Property    | Value                                    |
| ----------- | ---------------------------------------- |
| **Used by** | The env-gated real-LLM regression driver |
| **Format**  | Non-negative integer                     |
| **Default** | (unset → the default probe count)        |

Cost bound for the regression driver. Caps the number of real provider calls when
the suite is enabled; set to `0` for a record-only (`$0`) run that still executes
the structural `stream:context` / `assembledShape` assertions without a live call.

## Live-fire testing (operator)

These variables gate the `pnpm test:live` real-provider test tier. They are **never
required for normal operation** — when `COMIS_LIVE` is unset the entire live tier
self-skips, so a default `pnpm test` / `pnpm validate` run makes no provider call
and incurs no cost.

### `COMIS_LIVE`

| Property    | Value                                         |
| ----------- | --------------------------------------------- |
| **Used by** | Live-fire test runner (`test/live/runner.ts`) |
| **Format**  | `1` to enable, unset to skip                  |
| **Default** | (unset — the entire live tier is skipped)     |

Master gate for the real-provider live test tier. When unset, `pnpm test:live` exits 0
immediately with a "Live tier skipped" message — no provider call, no network, no cost.
When set to `1`, the runner dispatches to the scenario specified by the mode argument.

```bash theme={}
export COMIS_LIVE=1
```

### `COMIS_LIVE_BUDGET_USD`

| Property    | Value                                         |
| ----------- | --------------------------------------------- |
| **Used by** | Live-fire cost governor (`test/live/cost.ts`) |
| **Format**  | Decimal USD amount (e.g. `2.00`)              |
| **Default** | `2.00`                                        |

Hard-stop ceiling for a single `pnpm test:live` run. The cost governor accumulates
per-call cost estimates and aborts the run with a non-zero exit when the running
total exceeds this value, preventing runaway spend. Set a lower value for exploratory
runs; raise it only for full suite sweeps.

```bash theme={}
export COMIS_LIVE_BUDGET_USD=5.00   # raise for a full suite sweep
export COMIS_LIVE_BUDGET_USD=0.50   # lower for a single scenario
```

### `COMIS_LIVE_JUDGE_PROVIDER` / `COMIS_LIVE_JUDGE_MODEL` / `COMIS_LIVE_JUDGE_API_KEY`

| Property    | Value                                                                                       |
| ----------- | ------------------------------------------------------------------------------------------- |
| **Used by** | The live-fire judge wrapper (`test/live/judge.ts` `judgeAnswer`) for judged Stage-C scoring |
| **Format**  | Provider name / pinned model snapshot id / API key (the rubric-judge model lane)            |
| **Default** | unset — judged scenarios skip (skip is not fail)                                            |

The judge model lane for behavioral / answer-quality scoring (reuses the `bench-memory`
judge discipline). When all three are present, `judgeAnswer` invokes the real judge at
temperature 0 and returns a scored `pass`/`fail`; when absent, it returns a skip (judged
Stage-C scenarios then skip — never fail). For any **published** readiness claim, use
**cross-judge ≥2** (a second judge model + agreement) to avoid self-preference bias. The
judge prompt, output, and key are never logged or written to a report (secret residency).

```bash theme={}
export COMIS_LIVE_JUDGE_PROVIDER=anthropic
export COMIS_LIVE_JUDGE_MODEL=claude-3-5-haiku-20241022
export COMIS_LIVE_JUDGE_API_KEY=sk-ant-...
```

### `COMIS_LIVE_PROBES`

| Property    | Value                                          |
| ----------- | ---------------------------------------------- |
| **Used by** | Live-fire test runner (`pnpm test:live sweep`) |
| **Format**  | Comma-separated probe IDs or category prefixes |
| **Default** | (unset — all probes run)                       |

Filters which sweep probes run when executing `pnpm test:live sweep`. When unset, all probes
in `PROBE_REGISTRY` are executed. When set, only probes whose `id` or `category` matches one of
the comma-separated values are included.

Parsed by `parseProbeFilter()` in `test/live/sweep/sweep.ts`.

```bash theme={}
# Run only LLM probes
export COMIS_LIVE_PROBES="llm-anthropic,llm-openai,llm-groq"

# Run all search probes
export COMIS_LIVE_PROBES="search"
```

## LLM Provider Keys

At least one provider key is required. Each provider is detected at startup; missing keys disable the matching provider gracefully. All keys can also be stored encrypted via [`comis secrets set`](/reference/cli#comis-secrets) instead of plaintext in `.env`.

| Variable             | Provider                                                                                                                                                                                                                                                | Reference    |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| `ANTHROPIC_API_KEY`  | Anthropic (Claude family; also `image_analyze` when an Anthropic model is the agent's vision-capable main, or via the vision registry fallback)                                                                                                         | `sk-ant-...` |
| `OPENAI_API_KEY`     | OpenAI (GPT family, Whisper, embeddings; native `image_generate` via the OpenAI Images API for an `openai` main; also `image_analyze` when a GPT-4o-class model is the vision-capable main, or via the vision registry fallback)                        | `sk-...`     |
| `GOOGLE_API_KEY`     | Google AI Studio (Gemini family; native `image_generate` via Gemini and native `video_generate` via Veo for a `google` main; also `image_analyze` for a Gemini vision-capable main and `describe_video` raw video, or via the vision registry fallback) | `AIza...`    |
| `GROQ_API_KEY`       | Groq (Llama, Whisper)                                                                                                                                                                                                                                   | `gsk_...`    |
| `MISTRAL_API_KEY`    | Mistral AI                                                                                                                                                                                                                                              | --           |
| `DEEPSEEK_API_KEY`   | DeepSeek                                                                                                                                                                                                                                                | `sk-...`     |
| `XAI_API_KEY`        | xAI (Grok, search; native `video_generate` via Grok Imagine for an `xai` main)                                                                                                                                                                          | `xai-...`    |
| `TOGETHER_API_KEY`   | Together AI                                                                                                                                                                                                                                             | --           |
| `CEREBRAS_API_KEY`   | Cerebras                                                                                                                                                                                                                                                | `csk-...`    |
| `OPENROUTER_API_KEY` | OpenRouter (multi-provider proxy; also the `image_generate` opt-in path)                                                                                                                                                                                | `sk-or-...`  |

```bash theme={}
# .env example: pick the providers you actually use
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
```

Some providers reuse a parent key for sub-services: OpenAI Whisper STT and native `image_generate` (OpenAI Images API, default `gpt-image-1`) use `OPENAI_API_KEY`; a `google` main's native `image_generate` (Gemini, default `gemini-2.5-flash-image`) uses `GOOGLE_API_KEY`; Groq Whisper uses `GROQ_API_KEY`; Grok search uses `XAI_API_KEY`. See [Models](/agents/models) for the wired catalog.

**Image generation reuses the main provider's key.** With the default `integrations.media.imageGeneration.provider: auto`, the `image_generate` tool follows the agent's main provider and reuses **its** credentials — so in the common case **no image-specific key is needed**. A key-auth **`openai`** main generates via the OpenAI Images API with `OPENAI_API_KEY`; a **`google`** main generates via Gemini with `GOOGLE_API_KEY` (the same key the completion path and vision use — **not** `GEMINI_API_KEY`). To opt in to OpenRouter for image generation, set `provider: openrouter` and supply `OPENROUTER_API_KEY`; `FAL_KEY` is still used for the explicit `provider: fal` path.

With a **Codex (ChatGPT-login) main provider** and `provider: auto` (or explicit `provider: openai-codex`), `image_generate` reuses the agent's **OAuth bearer** — bootstrapped from [`OAUTH_OPENAI_CODEX`](#oauth-openai-codex) or `comis auth login --provider openai-codex` — so **no image-specific key** is needed; the token refreshes automatically per call. The bearer is an OAuth credential, never an `OPENAI_API_KEY`.

**`FAL_KEY` also enables video generation.** The `video_generate` tool uses the same `FAL_KEY` for the explicit `provider: fal` path (FAL queue API, `fal-ai/veo3.1/fast`). With the default `integrations.media.videoGeneration.provider: auto`, video generation **follows the agent's main provider and reuses its key**, mirroring image generation: a `google` main generates via **Veo** (default `veo-3.0-fast-generate-001`) reusing `GOOGLE_API_KEY`, and an `xai` main generates via **Grok Imagine** (`grok-imagine-video`) reusing `XAI_API_KEY` (or a SuperGrok login) — so in the common case **no video-specific key** is needed. (The native-Veo path also has a FAL-hosted fallback: `provider: fal` + `model: fal-ai/veo3.1`, which needs `FAL_KEY`, a second credential.) There is **no video-specific env var** beyond reusing these provider keys.

**Vision (`image_analyze`) reuses the main provider's key too.** When the agent's main model is vision-capable (Claude / GPT-4o / Gemini and other image-accepting models), `image_analyze` routes through that main model and reuses **its** credentials — `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GOOGLE_API_KEY` for a key-auth main, or the **OAuth bearer** for a Codex main — so **no separate vision key is needed** in that case. There is **no new vision env var**: if you fall back to the independent vision registry (when the main model can't see images, or you set an explicit `integrations.media.vision.defaultProvider`), it uses the same `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GOOGLE_API_KEY`. `describe_video` (raw video) is Gemini-only and uses `GOOGLE_API_KEY`.

## Voice and Media

**All audio keys are OPTIONAL.** Voice defaults to keyless: speech-to-text to
`auto` (keyless-first — a local whisper engine when available, else honest-
unavailable) and text-to-speech to `edge` (Microsoft Edge TTS, no key). The keyed
audio providers below — and `OPENAI_API_KEY` / `GROQ_API_KEY` for STT — are used
**only** when you set `integrations.media.transcription.provider` /
`.tts.provider` to that provider explicitly, **or** the agent's main provider
already supplies a usable audio key. A **Codex/ChatGPT-OAuth login cannot drive
OpenAI audio** (the bearer is scoped to the Codex Responses backend, not
`/v1/audio/*`); a Codex-only main is steered to keyless and never issues an
empty-bearer 401 — set a separate platform `OPENAI_API_KEY` + `provider: openai`
to use OpenAI audio explicitly.

| Variable             | Used For                                                                     |
| -------------------- | ---------------------------------------------------------------------------- |
| `ELEVENLABS_API_KEY` | ElevenLabs premium TTS voices (explicit `tts.provider: elevenlabs`)          |
| `DEEPGRAM_API_KEY`   | Deepgram Nova-3 speech-to-text (explicit `transcription.provider: deepgram`) |

```bash theme={}
ELEVENLABS_API_KEY=sk_...
DEEPGRAM_API_KEY=dg-...
```

## Web Search Providers

Optional. The agent picks an enabled provider when a `web_search` tool runs. Leave them all unset and the agent falls back to a no-op or the LLM's own search capability.

| Variable             | Provider                        |
| -------------------- | ------------------------------- |
| `SEARCH_API_KEY`     | Brave Search (`BSA...`)         |
| `PERPLEXITY_API_KEY` | Perplexity AI (`pplx-...`)      |
| `TAVILY_API_KEY`     | Tavily AI (`tvly-...`)          |
| `EXA_API_KEY`        | Exa neural search               |
| `JINA_API_KEY`       | Jina reader/search (`jina_...`) |

## Channel Credentials

Required only for channels you enable in `config.yaml`. Each value can also be stored as a SecretRef and referenced via `${VAR_NAME}` substitution.

### Telegram

| Variable                  | Purpose                                          |
| ------------------------- | ------------------------------------------------ |
| `TELEGRAM_BOT_TOKEN`      | Bot token from BotFather (e.g. `123456:ABC-...`) |
| `TELEGRAM_WEBHOOK_SECRET` | Optional webhook signature verification secret   |

### Discord

| Variable            | Purpose                                                 |
| ------------------- | ------------------------------------------------------- |
| `DISCORD_BOT_TOKEN` | Bot token from the Discord developer portal (`MTIz...`) |

### Slack

| Variable               | Purpose                                      |
| ---------------------- | -------------------------------------------- |
| `SLACK_BOT_TOKEN`      | OAuth bot token (`xoxb-...`)                 |
| `SLACK_APP_TOKEN`      | App-level token for Socket Mode (`xapp-...`) |
| `SLACK_SIGNING_SECRET` | Webhook signature verification (HTTP mode)   |

### LINE

| Variable                    | Purpose                              |
| --------------------------- | ------------------------------------ |
| `LINE_CHANNEL_ACCESS_TOKEN` | Long-lived channel access token      |
| `LINE_CHANNEL_SECRET`       | Channel secret for HMAC verification |

### Microsoft Teams

| Variable               | Purpose                                                                                                                                                           |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MSTEAMS_APP_PASSWORD` | Bot app password (client secret) for secret-mode auth — the fallback source for `channels.msteams.appPassword` (also settable inline in config or as a SecretRef) |

The bot app id (`channels.msteams.appId`) and directory/tenant id (`channels.msteams.tenantId`) are plain configuration values, not secrets, and are set in `config.yaml` rather than as environment variables.

### IRC

| Variable                | Purpose                                       |
| ----------------------- | --------------------------------------------- |
| `IRC_NICKSERV_PASSWORD` | NickServ password for nickname identification |

### Email

| Variable                    | Purpose                        |
| --------------------------- | ------------------------------ |
| `EMAIL_PASSWORD`            | IMAP password (basic auth)     |
| `EMAIL_OAUTH_CLIENT_ID`     | OAuth2 alternative to password |
| `EMAIL_OAUTH_CLIENT_SECRET` | OAuth2 client secret           |
| `EMAIL_REFRESH_TOKEN`       | OAuth2 refresh token           |

### Matrix

| Variable              | Purpose                                                                                                                               |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `MATRIX_ACCESS_TOKEN` | Bot access token — the fallback source for `channels.matrix.accessToken` (also settable inline in config or as a SecretRef)           |
| `MATRIX_RECOVERY_KEY` | Secure-backup recovery key — the fallback source for `channels.matrix.recoveryKey` (also settable inline in config or as a SecretRef) |

The bot password (`channels.matrix.password`) has no direct environment-variable fallback — set it inline in config or as a SecretRef (for example `${MATRIX_PASSWORD}`). The homeserver URL (`channels.matrix.homeserverUrl`) and the bot MXID (`channels.matrix.userId`) are plain configuration values, not secrets, and are set in `config.yaml` rather than as environment variables.

WhatsApp, Signal, and iMessage use file-based credentials in their respective auth directories rather than environment variables. See [Channels](/channels) for setup.

## Quick Setup with `comis configure`

Most users never edit `.env` by hand. The interactive flow:

```bash theme={}
comis init                  # First-time setup
comis configure             # Add or rotate credentials later
```

Both commands write to `~/.comis/.env` and (optionally) into `secrets.db` if a `SECRETS_MASTER_KEY` is configured. See [`comis configure`](/reference/cli#comis-configure) for non-interactive flags.

## Config Variable Substitution

Config YAML files support `${VAR_NAME}` substitution for referencing environment variables and stored secrets. Any variable accessible via the SecretManager (including environment variables and values stored with `comis secrets set`) can be referenced this way.

```yaml theme={}
gateway:
  tokens:
    - id: default
      secret: "${COMIS_GATEWAY_TOKEN}"
```

Use `$${VAR_NAME}` (double dollar sign) to produce a literal `${VAR_NAME}` in the output without substitution. The `$include` directive enables composing config from multiple files.

<Info>The `COMIS_*` prefix is reserved for operational variables. During `comis secrets import`, variables starting with `COMIS_` are skipped (treated as infrastructure, not secrets).</Info>

## Local Model Runtime Selection (MLX vs GGUF)

When running qwen3.6 or other local models via Ollama, the runtime format affects
inference latency:

| Platform                    | Recommended Runtime | Ollama Tag Example |
| --------------------------- | ------------------- | ------------------ |
| Apple Silicon (M1/M2/M3/M4) | MLX                 | `qwen3.6:35b-mlx`  |
| Linux / x86-64              | GGUF (llama.cpp)    | `qwen3.6:35b`      |

**Why:** MLX is optimized for the Apple Neural Engine and achieves significantly lower
inference latency on Apple Silicon. GGUF via llama.cpp is the standard format for
Linux deployments and cloud VPS instances.

**Comis does not distinguish between MLX and GGUF tags** — both are treated as opaque
Ollama model IDs. Set the model ID in your `config.yaml` to the tag that matches
your hardware:

```yaml theme={}
providers:
  entries:
    local:
      type: ollama
      models:
        - id: qwen3.6:35b-mlx  # Apple Silicon (MLX runtime)
        # - id: qwen3.6:35b    # Linux / x86-64 (GGUF / llama.cpp)
```

<Tip>
  To measure the latency difference between runtimes on your hardware, run the bench
  harness with both tags:

  ```bash theme={}
  BENCH_MODELS=qwen3.6:35b-mlx,qwen3.6:35b \
    node scripts/bench-small-model/comprehension.mjs
  ```

  The report shows average latency per model section. MLX should be faster on Apple
  Silicon; GGUF should be faster or equivalent on Linux.
</Tip>

## Related

<CardGroup cols={2}>
  <Card title="Config YAML" icon="file-code" href="/reference/config-yaml">
    Full configuration file reference
  </Card>

  <Card title="Secret Manager" icon="lock" href="/reference/secret-manager">
    Encrypted secret storage
  </Card>

  <Card title="CLI Reference" icon="terminal" href="/reference/cli">
    Command-line interface reference
  </Card>

  <Card title="Installation" icon="download" href="/installation">
    Initial setup and configuration
  </Card>
</CardGroup>
