Skip to main content

The config file

iota supports YAML config files for persistent settings, models and agents. This page is the shape of the file — the three layers, how an agent reaches a model, what the loader refuses. Every key, one by one, with its type, default and errors, is on the configuration reference.

Config lookup order​

  1. ~/.iota.yaml or ~/.iota.yml (global)
  2. ./.iota.yaml or ./.iota.yml (project-local, merges over global)
  3. -c/--config <path> (explicit, highest priority, used alone)

Same-name entries in later files override earlier ones, whole entry at a time.

Priority​

The API key is env var > providers.<name>.key — never a flag, so it stays out of the shell history and out of ps. The two per-run flags that overlap the config (-M, -s) win over it for that one invocation.

The three layers​

The config has three top-level maps, each answering one question:

MapAnswersKeys
providers:how do I reach the API?type, key, url
models:which model, and what does its protocol look like?provider, id, context_window, defer_mode, image knobs, effort/temperature/top_p defaults
agents:how do I use it?model, choices, system/system_file, tools, mcp_servers, mode, no_save, notify, description, and overrides for context_window/effort/temperature/top_p

A run names an agent. iota run <name> resolves agents: and nothing else: the agent decides which model it drives, and the model decides which endpoint it talks to. A models: or providers: entry is reached through an agent, never named directly — one name meant four things once, and a collision silently changed what ran.

tip

agents.default is what a bare iota runs. With no name iota takes the agent called default; a name always wins over it, and without such an agent the invocation asks for one. A models.default or a providers.default says which model or endpoint it is, never how to drive one, so neither is an entry point.

Referring to a model​

Wherever a model is named — agents.<name>.model, agents.<name>.choices, a models: shorthand, -M — three forms are accepted:

FormMeans
sonnetthe models: entry called sonnet
anthropic:claude-sonnet-4that model id, on that provider (everything after the FIRST colon is the id, so openrouter:anthropic/claude-3.5-sonnet works)
anthropic:*every model the provider lists, fetched when the picker opens
warning

No space after the colon. - anthropic: claude-x is a YAML mapping, not a string; iota says so rather than failing with a type error.

Example​

~/.iota.yaml
providers: # endpoints: how to connect, how to authenticate
openai:
key: sk-official
anthropic:
key: ${env:ANTHROPIC_KEY}
deepseek: # a custom endpoint
type: openai # the underlying provider type
key: ${env:DEEPSEEK_KEY} # key/url expand ${…} variables
url: https://api.deepseek.com/v1

models: # configured models: provider + id + protocol + defaults
sonnet: anthropic:claude-sonnet-4-20250514 # shorthand: provider:id
gpt5:
provider: openai
id: gpt-5.2
context_window: 400k # context window for compaction accounting (an agent may override it)
defer_mode: system-tools # protocol for deferred MCP tools: normal|reference|tool-search|system-tools
effort: high # default reasoning effort: low|medium|high|xhigh|max
temperature: 0.7 # default sampling temperature, 0.0-2.0 (/model overrides)
top_p: 0.9 # nucleus sampling, 0.0-1.0 (advanced: tune this OR temperature, not both;
# reasoning models reject/ignore it — omit to use the provider default)
chat: deepseek:deepseek-chat

agents: # usage: how a model is driven
default:
model: gpt5 # the model the run starts on; absent = start in the picker
choices: [gpt5, sonnet, "deepseek:*"] # what /model and -M offer; absent = model: alone
system: "You are a helpful coding assistant"
tools:
code:
shell:
mcp_servers: [github] # load only these MCP servers; [] = none; key absent = all
mode: agent # project overlay (AGENTS.md) + skills + project-scoped sessions

reviewer:
model: sonnet
system_file: ${appHome}/prompts/reviewer.md # prompt from a file (inline `system` wins)
description: Reads a diff and reports what is wrong with it # documentation of the entry
effort: high # overrides the model's default (one level, no deeper)
context_window: 200k # …as may the window, for an agent that knows how long its chats run

scratch:
choices: ["openai:*"] # no model: — the run starts in the picker, over what openai lists
no_save: true # start ephemeral (like --no-save); an explicit `iota resume` outranks it
notify: false # no desktop notification while the terminal is unfocused (default: on)

With this config:

# The agent named "default": its model (gpt5 → openai/gpt-5.2), prompt, tools and MCP subset
iota # …and with no argument at all, that is what runs
iota run default -m "hello"

# Another agent: its own model, prompt, tools and MCP subset
iota run reviewer -m "what is wrong with this diff?"

# -M picks another model, usually from the choices (outside them is a warning — choices are advice)
iota run default -M sonnet -m "hi"

# -M also takes provider:id, which moves the run to that endpoint
# (and brings the models: entry serving that pair, if there is one — otherwise nothing:
# the parameters of the candidate it replaces do not follow it)
iota run default -M "deepseek:deepseek-reasoner" -m "hi"

# …and provider:* starts in the model picker
iota run default -M "deepseek:*"

-M accepts a models: entry's name, a bare model id, or provider:id. A bare word is the models: entry of that name whether or not it is among the choices — -M fast is the model fast stands for, never a model id spelled fast; only a word no entry has is a model id on the current provider. A model outside the agent's choices: is a warning, not a refusal — the list is advice about what works well here, not a whitelist.

The system prompt: what iota adds to yours​

An agent with tools: does not go out with your system: alone: iota puts a short harness paragraph of its own ahead of it — identity, an <environment> block and, with the shell set, an <iota_cli> block — and your prompt follows inside <instructions>, the AGENTS.md overlay after that. It is composed at send time, never stored, under 1.5 KB, with no configuration key; an agent without tools: sends nothing extra. The structure and the full text are on The system prompt.

The choices are what /model offers​

agents.<name>.choices is the row list of the /model picker. Without the key, an agent is offered its model: alone; one that names no model: either is offered every models: entry. Entries and inline provider:ids are rows on the spot; every provider:* in the list is a listing request, and several of them go out at once — the wait is the slowest endpoint, not the sum of them, and Esc cancels all of them together.

A source that cannot answer costs only its own rows: provider: <what went wrong> appears as the panel's dim subtitle and everything else is listed as if that source had never been asked. Nothing takes the picker away from you, because the picker is a combo box — its input row is open from the first frame, filters the list as you type, and commits what you typed through a use "…" as typed row. A provider that does not implement a model listing at all (most relays) is therefore an ordinary case, not a failure mode.

A row is named the same wherever the chat is: provider:model, or just the model when every choice is on one provider, followed in brackets by the models: entries that name it (relay:vendor/fast [fast]), and (current) on the one the chat is running. A row does not say what picking it does; the pick reports itself. A row on another provider moves the chat: before the first reply, picking it moves the chat there at once; once a model has replied, iota saves the chat, tells you how many replies carry provider-specific payload the target will not replay, that iota resume <id> continues on the new model afterwards and that picking the old row moves it back, and asks before anything changes. A chat that is not saved is told to /save first, and a model of another kind (an image model for a chat) is refused. A bot never moves — its config does — so there the pick is refused.

One layer per key​

Every key belongs to exactly one layer, and writing it in another is an error naming the layer that owns it. The whole document is audited before it is decoded, and five things are fatal:

WhatExampleWhat you get
A key of another layerproviders.p.modelconfig ~/.iota.yaml: providers.p.model: `model` is now a `models:` entry
A retired keyagents.a.tools.delegatethe toolset was removed — run child agents from bash instead
An unknown keyagents.a.sytemthe coordinate and the file it is in
An unknown top-level keyagent:the same
An unknown toolsettools: {web: {}}the same

There is no migration layer and no compatibility shim: a key that silently does nothing is exactly the failure this audit exists to close. The full table of messages is under Errors in the reference. (A YAML syntax error still drops the file with a warning, because the parser is the only thing that knows what went wrong.)

Two keys changed name when the layers split: a provider's agent: true is an agent's mode: agent (since v0.6.0; it was workspace: true before), and the agent toolset is now called skills. The delegate toolset was removed outright — a child agent is a bash subprocess now (see the shell set).

# what a single-layer config used to look like — every key of it is refused today
providers:
deepseek:
type: openai # ✓ the endpoint
key: ${env:DEEPSEEK_KEY} # ✓
url: https://api.deepseek.com/v1 # ✓
model: deepseek-chat # ✗ → a `models:` entry
system: "You are terse" # ✗ → `agents.<name>.system`
tools: {code: {}} # ✗ → `agents.<name>.tools`
agent: true # ✗ → `agents.<name>.mode`

# the same thing, in three layers
providers:
deepseek: {type: openai, key: "${env:DEEPSEEK_KEY}", url: https://api.deepseek.com/v1}
models:
deepseek: deepseek:deepseek-chat
agents:
deepseek:
model: deepseek
system: "You are terse"
tools: {code: {}}
mode: agent

iota config check loads the files and reports the three layers, warning when no agents.default exists; iota config path prints which files a run reads.

The five layered parameters​

context_window, effort, temperature, top_p and max_tokens can be written in TWO layers, and all but max_tokens changed a third way, in /model. Which one wins is one rule, and it holds at every moment of a session:

TierSays
1. agents.<name>the agent's override — highest, whatever model it drives
2. models.<name>what the model the chat is RUNNING declares
3. the session's current valuewhat the chat is running under right now — lowest

When the config speaks the config decides; when the config is silent your hand-set value stands. Adjusting a knob in /model is the answer for what no declaration covers, not a permanent override of one — to change a config for good, edit it.

That third tier is why iota remembers not just each value but where it came from, and what happens at each of the four moments follows from it:

MomentWhat happens
A new session startsthe two config layers are evaluated: agent, else model, else the built-in default
/model switches modelevaluated again against the NEW model — and a value the chat had INHERITED from the model it is leaving is dropped rather than carried over, while one you set by hand is kept
/model changes a knobno evaluation: the value is yours, it applies at once, and it survives a later switch that finds no declaration
iota resumeno evaluation: the bundle's values and their origins come back as they were. Only a parameter the bundle never recorded is evaluated

So switching from a model with a 400k window to one that declares none puts you back on the default rather than silently keeping 400k — but a window you chose in the Context tab follows you. And resuming an old session after editing agents: continues that session as it was: the new declaration reaches it the first time you switch models inside it, so a conversation never changes shape halfway through because a file on disk did.

Sessions written by older builds have no record of where their values came from; those values are treated as yours, which is the reading that cannot lose something you chose.

note

defer_mode is not one of the five. It lives in models: alone and does not follow the model: the deferring wrapper is built once, at startup, so switching to a model that asks for a different mode prints a note saying the session keeps the mode it started with — deferred MCP tools are mounted once.

Variable expansion​

Provider values (key, url), an agent's system_file and MCP server values (command, args, url, env, headers) support VS Code-style variable expansion:

VariableExpands to
${workspaceFolder} / ${cwd}Current working directory
${userHome}User home directory
${appHome}iota's global directory (~/.iota)
${pathSeparator} / ${/}OS path separator (/)
${env:VAR}Value of environment variable VAR

Unknown variables are left untouched. Which fields expand, and the rules of a pass, are on the reference.