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). .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:
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.
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.
| 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. 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 (starts with sk-or-). |
OPENROUTER_MODEL | openai/gpt-5.1 | The model slug from 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), 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). 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.
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.
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.
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. |
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. Why fail-closed matters: 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. |
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.
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.
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.
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.
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). |
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). |
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.