# Configuration

Iva is configured by one file: `.env` in the install directory. The setup wizard fills it in for you — run `iva config` any time to redo a step ([cli.md](./cli.md)). `.env.example` in the repo root is the template. This page documents every variable.

**Model, key and provider changes need only a restart.** Iva reads these `.env` settings at startup. After editing:

```bash
iva restart
```

Swapping a model, key or provider is edit → restart, without a rebuild.

`MEMORY_NIGHT_TIME=HH:mm` sets the local time of the single `memory-night` Schedule in `ASSISTANT_TIMEZONE` (default `04:00`; strict `00:00..23:59`). This is a build setting: after editing `.env`, run `iva update --force`; in a development checkout use `npm run build` and `iva restart`. Editing `.env` alone moves neither the Schedule nor catch-up. `/menu` → ⏰ shows the active schedule and any pending time. Schedule and catch-up read one build artifact; a failed build or rollback preserves its previous time. Older builds without the artifact keep 04:00.

**One exception: the reply language.** The **🌐 Language** button in `/menu` writes `data/settings.json`, which both processes re-read live — the switch takes effect immediately, no restart. See [menu.md](./menu.md).

## Model provider

Six providers. Pick one with `MODEL_PROVIDER` and fill only that block. `ollama`/`opencode`/`openrouter` are OpenAI-compatible API keys; `codex` rides your OpenAI (ChatGPT) subscription via OAuth — no key; `claude` rides your Claude Pro/Max subscription through the `claude` CLI signed in on the same server — no key either; `custom` is any OpenAI-compatible endpoint you point Iva at yourself. Prices and full model lists: [providers.md](./providers.md).

| Variable                    | Default                   | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| --------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MODEL_PROVIDER`            | `ollama`                  | `ollama` (Ollama Cloud), `opencode` (OpenCode Go), `openrouter` (OpenRouter), `codex` (OpenAI ChatGPT subscription), `claude` (Claude Pro/Max subscription via the `claude` CLI) or `custom` (your own OpenAI-compatible endpoint).                                                                                                                                                                                                                                                                                                   |
| `OLLAMA_API_KEY`            | —                         | Key from ollama.com.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `OLLAMA_MODEL`              | `deepseek-v4-pro`         | Any model on your Ollama Cloud plan.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `OLLAMA_VISION_MODEL`       | `gemma4:31b`              | Model that describes incoming photos. Your text model is usually text-only, so vision runs on its own model with the same key. Blank = the default.                                                                                                                                                                                                                                                                                                                                                                                   |
| `OLLAMA_CONTEXT_WINDOW`     | `131072`                  | See warning below.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `OPENCODE_API_KEY`          | —                         | Key from opencode.ai/auth.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `OPENCODE_PROTOCOL`         | `chat-completions`        | Go text wire: `chat-completions` or `responses`, from [Go's endpoint table](https://opencode.ai/docs/go/#endpoints). Uses the same key and `OPENCODE_MODEL`; `iva config` probes Responses over the selected wire. `/model` preserves the protocol. `/messages` is unsupported. Run `iva restart` after a manual edit.                                                                                                                                                                                                                |
| `OPENCODE_VISION_PROTOCOL`  | `chat-completions`        | Separate wire of the fallback `OPENCODE_VISION_MODEL`: `chat-completions` or `responses`. The chat model's image probe uses its own text wire first. `qwen3.7-plus` now needs `/messages`: choose a compatible fallback model; failures explain this limitation.                                                                                                                                                                                                                                                                      |
| `OPENCODE_MODEL`            | `deepseek-v4-pro`         | Any Go model, bare ID (e.g. `kimi-k3`) — `iva config` shows the live list.                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `OPENCODE_VISION_MODEL`     | `qwen3.7-plus`            | Same for OpenCode Go, bare ID. Pick from the models that actually read images — many Go models answer 400 on a picture, and some mix their reasoning into the description.                                                                                                                                                                                                                                                                                                                                                            |
| `OPENCODE_CONTEXT_WINDOW`   | `131072`                  | Same warning.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `OPENROUTER_API_KEY`        | —                         | Key from [openrouter.ai/keys](https://openrouter.ai/keys) (starts with `sk-or-`).                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `OPENROUTER_MODEL`          | `openai/gpt-5.1`          | The model **slug** from [openrouter.ai/models](https://openrouter.ai/models), form `vendor/model` (e.g. `anthropic/claude-sonnet-5.5`). `iva config` sends a live test request so a wrong slug can't slip through.                                                                                                                                                                                                                                                                                                                    |
| `OPENROUTER_VISION_MODEL`   | `google/gemini-2.5-flash` | Same for OpenRouter — any slug that accepts images. `iva config` does not test it live.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `OPENROUTER_CONTEXT_WINDOW` | `131072`                  | Same warning — set the real window of the model you picked.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `CODEX_MODEL`               | `gpt-5.5`                 | Model from your OpenAI plan. `iva config` lists what your subscription actually exposes.                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `CODEX_CONTEXT_WINDOW`      | `272000`                  | Same warning — set the real window of the model you picked.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `CLAUDE_MODEL`              | `claude-fable-5-1`        | One of Fable 5.1 (`claude-fable-5-1`, default), Opus 5.5 (`claude-opus-5-5`), Sonnet 5.5 (`claude-sonnet-5-5`) or Haiku 5.5 (`claude-haiku-5-5`). `iva config` lists the ones the CLI picker actually has.                                                                                                                                                                                                                                                                                                                            |
| `CLAUDE_CONTEXT_WINDOW`     | `1000000`                 | Same warning — 1000000 for Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5.                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `CLAUDE_COMMAND`            | _(empty)_                 | Path to the `claude` binary, or a command line with arguments; a path with a space is not supported. Empty means the `claude` found in the service's `PATH` (node directory, `~/.local/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`).                                                                                                                                                                                                                                                                                                   |     |
| `CLAUDE_SESSION_CWD`        | _(on)_                    | Claude Code CLI runs every step of one conversation in the same private folder (`<tmp>/iva-cwd-<uid>/<hash of the session>`, mode 0700): the CLI writes its working directory into each request, and a folder that changed every step made the prompt cache write the whole history again. `0`, `false`, `no` or `off` turns this off and runs each step in its own temporary folder, as before.                                                                                                                                      |
| `CUSTOM_BASE_URL`           | —                         | **Required for `custom`.** The OpenAI-compatible base in full, including the `/v1`-style suffix — same convention as Ollama's `https://ollama.com/v1`. Empty stops the agent at startup naming this variable; it never falls back to a guess.                                                                                                                                                                                                                                                                                         |
| `CUSTOM_API_KEY`            | —                         | Optional. Empty means no `Authorization` header at all, which is what a self-hosted endpoint usually wants. `iva doctor` does not ask for it.                                                                                                                                                                                                                                                                                                                                                                                         |
| `CUSTOM_MODEL`              | —                         | **Required for `custom`.** There is no default: only you know what the endpoint serves. Empty stops the agent at startup, it does not borrow another provider's model.                                                                                                                                                                                                                                                                                                                                                                |
| `CUSTOM_VISION_MODEL`       | _(empty)_                 | Optional, no default — a chat model that reads images is asked directly and needs nothing here. Empty leaves the fallback path pointing at the chat model itself, so a blind model simply yields no description. The fallback call needs `CUSTOM_API_KEY`; without a key it is skipped.                                                                                                                                                                                                                                               |
| `CUSTOM_CONTEXT_WINDOW`     | `131072`                  | Same warning — set the real window of the model you picked.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `CUSTOM_REASONING`          | _(empty)_                 | Set to `1` when the endpoint understands OpenAI's `reasoning_effort` (Meta Model API, vLLM serving a reasoning model): Iva then forwards `THINKING_EFFORT` to it. Empty sends nothing — an endpoint that does not know the field may answer 400 on every turn.                                                                                                                                                                                                                                                                        |
| `THINKING_EFFORT`           | _(empty)_                 | Reasoning effort: `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Written by `/model` and `/think` in Telegram ([cli.md](./cli.md)), not by hand. `codex` takes it natively; `ollama`/`opencode` (chat-completions) send `low`/`medium`/`high` over the OpenAI-compatible wire and ignore the rest; picking `openrouter` or `custom` in the wizard drops the line altogether, and a value typed in by hand is ignored there — except `custom` with `CUSTOM_REASONING=1`, which forwards `low`/`medium`/`high` like `ollama` does. |

`*_VISION_MODEL` is a fallback, not the main path. On the first incoming picture Iva asks the chat model itself with one tiny request whether it takes images. If it does, the picture goes straight to it — full pixels, no retelling — and the vision model is not called at all. If it answers with a refusal, the picture goes to `*_VISION_MODEL` as before and the model gets its description. The answer is remembered until the process restarts, so after a restart or a model change the question is asked again. One consequence of switching to a model without vision: pictures you sent earlier have no description saved for them, so send the photo again if the answer needs it.

Those six names, spelled exactly. Any other value — a typo, a different case — stops the agent at startup and prints the accepted names, instead of running Ollama under a name nobody configured; `iva doctor` and `/menu` → 📊 Status report the same thing ([troubleshooting.md](troubleshooting.md)). No `MODEL_PROVIDER` line at all still means `ollama`.

For `custom` nothing is guessed: `CUSTOM_BASE_URL` and `CUSTOM_MODEL` are the two lines the provider cannot work without, and a blank one is a startup refusal naming the variable, not a silent default. `THINKING_EFFORT` is ignored on `custom` on purpose — OpenAI compatibility promises nothing about `reasoning_effort`, and sending it blind to an unknown endpoint risks an HTTP 400 on every turn. Full walkthrough: [providers.md](./providers.md#your-own-openai-compatible-endpoint-custom).

For `codex` there is no API key in `.env`: run `iva login` (device code, headless-friendly) or `iva login --browser`. The OAuth token lives in `data/codex-auth.json` (chmod 600, gitignored) and is auto-refreshed before it expires. Full flow: [providers.md](./providers.md#openai-by-chatgpt-subscription-codex).

For `claude` there is no API key either: install the CLI on the server as the service user (`npm install -g --prefix ~/.local @anthropic-ai/claude-code`, no root) and sign it in once (`claude auth login`). Iva never sees a key — it calls that CLI, and `iva doctor` reports the plan from `claude auth status`. Keep `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_FOUNDRY_API_KEY`, `ANTHROPIC_BASE_URL` and any `CLAUDE_CODE_USE_*` out of `.env`: each of them sends the CLI somewhere other than your subscription, so Iva refuses to use this provider and says which variable is in the way rather than quietly ignoring it. Full flow: [providers.md](./providers.md#claude-by-promax-subscription-claude).

If Claude's access token comes from an external synchronization service, Iva can wait through its rotation after HTTP 401 and safely repeat the same call once, before any useful output or tool action in the current turn. Set `claude.authRotationWaitMs` in `data/settings.json` (for example, `{"claude":{"authRotationWaitMs":120000}}`); the default window is 120 seconds. Iva watches `.credentials.json` in `CLAUDE_CONFIG_DIR`, otherwise `~/.claude`, and makes one final call when the access changes or the window ends. Each active attempt retains its usual first-output deadline. A successful repeat stays silent; a final authentication failure with an access-only token asks you to check the external synchronization service. The service that owns the credentials continues to manage refresh.

**Don't inflate the context window.** Iva compacts the conversation between turns once a turn's input reaches 60% of this number (or 110,000 tokens, whichever is smaller), and inside a turn a quarter above that as a safety net. Set it above the model's real window and the compactor fires too late — the request overflows before the conversation gets trimmed. When you switch models, enter the new model's actual window, not a rounder bigger one.

Use `/compact` to compress an idle conversation with at least 30,000 measured input tokens. Iva confirms completion, then reports the before/after size with the next answer using its first measured model input. During an answer, repeat the command after it finishes. After a restart, one completed turn supplies the input measurement.

## Telegram

| Variable                        | Default   | Notes                                                                                                                                                                                                                                                                                                                                                                                                  |
| ------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `TELEGRAM_BOT_TOKEN`            | —         | From [@BotFather](https://t.me/BotFather).                                                                                                                                                                                                                                                                                                                                                             |
| `TELEGRAM_BOT_USERNAME`         | —         | Your bot's username. The wizard verifies the token via `getMe` and detects this itself.                                                                                                                                                                                                                                                                                                                |
| `TELEGRAM_WEBHOOK_SECRET_TOKEN` | —         | Shared secret between the long-poll bridge and the local webhook. Any long random string.                                                                                                                                                                                                                                                                                                              |
| `TELEGRAM_ALLOWED_USER_IDS`     | _(empty)_ | Comma-separated numeric user IDs allowed to talk to Iva.                                                                                                                                                                                                                                                                                                                                               |
| `TELEGRAM_DIGEST_CHAT_ID`       | —         | Chat for nightly memory reports and one-time stable update offers. Usually your own chat ID. Watch, the Brief and a job failure with the Fix button go to your private chat (the first ID of `TELEGRAM_ALLOWED_USER_IDS`) instead: they carry mail and chats, and a button is accepted only there.                                                                                                     |
| `SUPPORT_CHAT_URL`              | _(empty)_ | Invite link to the Telegram support chat the `report-problem` skill offers when a turn fails; empty means Iva says the address is not configured.                                                                                                                                                                                                                                                      |
| `TELEGRAM_RICH_REPLIES`         | `auto`    | `auto` sends a reply as a Telegram rich message when it contains a table, task list, `<details>`, block formula, footnote, media block or a `<tg-*>` tag; `never` keeps every current-chat reply on the ordinary HTML/plain path except replies with buttons, which exist only as rich messages. Also switchable in `/menu` → Rich replies. Any other value stops startup. `iva post` is not affected. |

The allowlist is **fail-closed: empty means Iva answers nobody.** The wizard auto-discovers your ID the moment you message the bot; or ask [@userinfobot](https://t.me/userinfobot). Why fail-closed matters: [security.md](./security.md).

At 10:00 in `ASSISTANT_TIMEZONE` Iva checks Git upstream without using the model. It sends nothing unless a higher stable `MAJOR.MINOR.PATCH` version exists, and offers each version only once. If `TELEGRAM_DIGEST_CHAT_ID` is empty, the first trusted ID is used.

## Voice

| Variable            | Default | Notes                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `DEEPGRAM_API_KEY`  | —       | Optional. From console.deepgram.com. Transcribes voice notes, video circles and audio files; without it recordings are saved but not transcribed. The wizard lets you skip it (Enter); add it later in `/menu` → 🎤 Voice or `iva config`. Free tier: [providers.md](./providers.md).                                                                                                                                          |
| `DEEPGRAM_LANGUAGE` | `multi` | `multi` auto-detects the language per message (ru/uz/en and others). Pin a single code like `en` only if auto-detection trips on your mix.                                                                                                                                                                                                                                                                                     |
| `DEEPGRAM_KEYTERMS` | —       | Optional. Names and terms Deepgram should spell your way, comma-separated: `OJ,Sonnet,Todoist`. Each goes to the request as a `keyterm` (nova-3 Keyterm Prompting); the first ones up to 50 terms and 500 characters in all are used, since Deepgram refuses the whole request over its keyterm limit. `.env` keeps Latin letters only. Deepgram bills keyterm prompting as a per-minute add-on. Set it in `/menu` → 🎤 Voice. |

## Search

| Variable                                                          | Default  | Notes                                                                                  |
| ----------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------- |
| `SEARCH_PROVIDER`                                                 | `tavily` | `tavily`, `exa`, `parallel` or `brave`.                                                |
| `TAVILY_API_KEY` `EXA_API_KEY` `PARALLEL_API_KEY` `BRAVE_API_KEY` | —        | Key for the matching provider. Keys can coexist; switching providers is just the flag. |

No key for the active provider means `web_search` returns a clear error — nothing crashes. Free tiers and the comparison table: [providers.md](./providers.md).

## Memory

| Variable                | Default              | Notes                                                                                                                                                                 |
| ----------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MEMORY_SEARCH_MODE`    | `grep`               | `grep` = BM25 over Node's built-in SQLite FTS5 plus graph rerank. Zero external deps, zero keys, runs on a $4 box. `hybrid` adds dense embeddings — one external key. |
| `JINA_API_KEY`          | —                    | For hybrid. Jina `jina-embeddings-v3`: no-train policy, EU hosting.                                                                                                   |
| `DEEPINFRA_API_KEY`     | —                    | For hybrid. Cheaper, serves `BAAI/bge-m3`. One of the two keys is enough.                                                                                             |
| `MEMORY_EMBED_PROVIDER` | _(auto)_             | Override auto-pick: `jina` or `deepinfra`.                                                                                                                            |
| `MEMORY_EMBED_MODEL`    | `jina-embeddings-v3` | Embedding model name.                                                                                                                                                 |
| `MEMORY_EMBED_URL`      | —                    | Any OpenAI-compatible embeddings endpoint, e.g. a local Ollama at `http://127.0.0.1:11434/v1/embeddings` — then no external key at all.                               |

The nightly Brain pass builds the hybrid index; to build it now, run `node --env-file=.env scripts/memory/embed-index.ts`. How search actually works: [memory.md](./memory.md).

## Notices

What Iva says on her own lives in `data/settings.json`, not in `.env` — the **🔔 Notices** screen in `/menu` writes these keys, and the report switch is read at the end of each nightly run, the `proactive` key on every half-hourly Watch, Brief and Insight tick — so a tap applies without a restart.

| Key                             | Default              | Notes                                                                                                                                                                                  |
| ------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `memoryReports.enabled`         | `false`              | The nightly memory report (daily 04:00 and weekly Mon 04:15) in Telegram. Off means the vault is still written.                                                                        |
| `proactive.enabled`             | `true`               | Watch, the Brief and Insight (**Writes on her own**). Off stops all three, not failures. `iva proactive` sets the rest.                                                                |
| `proactive.briefTimes`          | `["08:30", "14:00"]` | The Brief, `HH:00` or `HH:30`, at most two; empty — no Brief. `/digest` gives the Brief by hand any time.                                                                              |
| `proactive.insightTimes`        | `[]`                 | Insight: one `HH:00` or `HH:30` (best `HH:30`); empty — off. Whole 3-hour window in the quiet hours: no Insight on that day; also the Insight switch in /menu → Notices.               |
| `proactive.ceilingTokensPerDay` | `0`                  | Ceiling of a day for Watch, the Brief and Insight, in tokens of their turns (`iva usage by-source`); reached — no Insight and no Brief that day, Watch reports failures only. 0 — off. |

Insight: once a day Iva builds and tries one plugin draft for something you keep doing by hand and asks before installing — [ADR-0022](./adr/0022-insight-brings-one-capability-a-day.md).

Alerts — problems (memory not backed up, a failed nightly pass) and new versions — always arrive: they cannot be switched off, but each one says what to do and repeats at most once a week per problem — [ADR-0007](./adr/0007-notices-are-opt-in.md).

## System

| Variable                   | Default                        | Notes                                                                                                                                                                                                                                                                                                          |
| -------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AGENT_LANGUAGE`           | `ru`                           | `en` or `ru`. Sets Iva's reply language, date locale, and which CORE.md seed `init-vault` uses. The **🌐 Language** button in `/menu` overrides it at runtime via `data/settings.json` (read fresh every turn) and mirrors the choice back here, so the switch is instant — no restart ([menu.md](./menu.md)). |
| `ASSISTANT_TIMEZONE`       | `Asia/Almaty`                  | IANA name. Sets daily-transcript dates, two systemd watchdog timers, seven in-process eve schedules, and the date/time Iva sees each turn. Exported as `TZ`.                                                                                                                                                   |
| `ASSISTANT_VAULT_DIR`      | `vault`                        | The live memory: a separate private git repo, opens in Obsidian.                                                                                                                                                                                                                                               |
| `ASSISTANT_DATA_DIR`       | `data`                         | Runtime data: `tasks.json`, token log `usage.jsonl`.                                                                                                                                                                                                                                                           |
| `IVA_PORT`                 | `8723`                         | Local eve server port. Deliberately unfashionable — 3000/8000/8080 are usually taken on a stock VPS by docker and friends. Change it via `iva config`, not by hand: the systemd unit pins the port literally and must match ([deploy.md](./deploy.md)).                                                        |
| `ASSISTANT_HOST`           | `http://127.0.0.1:${IVA_PORT}` | Where the poll bridge and memory scripts reach the server. Change only if the agent runs on another host.                                                                                                                                                                                                      |
| `ASSISTANT_BEARER`         | _(generated)_                  | Shared secret required by Eve session routes. Setup/upgrades create it; local clients read it automatically. Keep it private.                                                                                                                                                                                  |
| `AGENT_BROWSER_MAX_OUTPUT` | `24000`                        | Character cap on agent-browser output, so one page dump can't eat the context window.                                                                                                                                                                                                                          |

OpenCode Responses does not forward `THINKING_EFFORT`: arbitrary Go models have no verified reasoning contract; `/think` offers no levels in this mode. Existing installations without either protocol variable retain chat/completions, model IDs, keys and data.

## Plugin service ownership

Plugin services belong to the canonical data directory. Two code checkouts using the
same data directory share one plugin installation, including `plugins.json`, ports and
the existing lock. Reconciliation updates scoped MCP executors to the selected checkout. An active
retained legacy keeps its executor during trust/sync; an explicit plugin update
may replace its payload only when that plugin source changed.
A different canonical data directory gets a different scope in new service names.

A proven legacy service keeps its existing name. Adding ownership metadata alone keeps
a running process uninterrupted. Ownership checks include the loaded data directory;
foreign content, links and unknown ownership are preserved and reported by `iva doctor`.
Moving the data directory changes the scope: review preserved old services explicitly.
Older CLI versions lack these ownership checks, including during a rollback.
