Skip to main content

Configuration reference

Every key the config file accepts, layer by layer: its type, its default, what it means, what it accepts, and what you are told when it is wrong. The config file is the concept page — the three layers, how an agent reaches a model, the layered parameters; this is the lookup table. Facts are as of v0.3.2.

Files, lookup and merge

A run reads, in this order, each file merged over the previous:

TierFileNotes
user~/.iota.yaml, else ~/.iota.ymlthe first of the two that exists; skipped when there is no home directory
project./.iota.yaml, else ./.iota.ymlin the working directory, not the project root; skipped when there is no cwd
explicit-c/--config <path>alone — with -c neither tier is read, whether or not the file exists

Merging is by whole entry: a models.gpt in the project file replaces the user file's models.gpt outright, it does not fill in its missing keys. Within a tier the four top-level maps are independent, so a project file that only declares mcp_servers: leaves every provider, model and agent of the user file in place. ${…} references in providers.<name>.key/url and agents.<name>.system_file are expanded once, at merge time; MCP server values are expanded when the server is started.

What happens to a file that is not right:

FileWhat you get
missingsilence — a missing tier says nothing about what you meant
unreadable (permissions, a directory)Warning: config <path>: <error> (ignored) — the file is dropped, the run goes on
not YAMLWarning: config <path>: <error> (file ignored) — the parser's own message, since it is the only thing that knows what went wrong
a key in the wrong place, misspelled, or unknownconfig <path>: <coordinate>: <message> — the load fails, naming the file and the coordinate (agents.coder.tools.delegate); see Errors
a reference that points nowhere, a protocol the dialect cannot speakthe same failure, unprefixed, once every file is merged (there is no single file to blame)

The whole document is audited before it is decoded, so a mistake is an error with a fix rather than a line that silently does nothing.

The first run writes a starter

A run with no -c and no config file at either tier writes ~/.iota.yaml itself, names it on stderr —

Wrote /Users/you/.iota.yaml (a starter config: openai + gpt-5.2; edit it, or set OPENAI_API_KEY and go)

— and goes on: with OPENAI_API_KEY in the environment that is a working first run, without it the key error says what to set. Nothing is written when a config exists in either spelling, when -c names one, or without a home directory. iota config init writes the same file without running anything. This is the starter, verbatim:

~/.iota.yaml (as written)
# iota config. Three layers, each answering one question.
#
# providers: how do I reach the API? (type, key, url)
# models: which model, and how does its protocol work?
# agents: how is it driven? (prompt, tools, MCP servers, sessions)
#
# `iota` runs the agent named `default`; `iota run <agent>` runs any other.
# See `iota list agents` and https://iota.sh for the full reference.

providers:
openai:
# The key is read from $OPENAI_API_KEY when this is absent — keep it out of the file if you can.
# key: sk-...
type: openai

models:
gpt:
provider: openai
id: gpt-5.2
# context_window: 400k # what /compact accounts against

agents:
default:
models: [gpt] # the candidate set, best first; -M and /model pick from it
# Your own instructions. iota already tells the model what it runs inside and where (the
# built-in harness prompt: identity, environment, and its own command line when `shell` is on).
system: "You are a careful coding assistant."
tools:
code: # read/write/edit/grep over the project
shell: # shell commands, with a sandbox by default
# workspace: true # AGENTS.md overlay, skills, project-scoped sessions

# MCP servers go under a top-level `mcp_servers:` block, which `iota mcp add <name> -- <command>`
# (or `--url <url>`) writes and `iota mcp remove <name>` edits for you.

iota config

CommandDoes
iota config check (the default action)loads exactly what a run would — every warning on stderr, the first hard error as the run's own — then prints the files read and OK: N provider(s), N model(s), N agent(s), N mcp server(s), with a one-line warning when no agents.default exists
iota config pathprints the files this invocation reads, in merge order, each marked (missing) when it is not there — the answer to "why is my change not taking effect"
iota config initwrites the starter above to ~/.iota.yaml (or to the -c path), refusing a file that exists: <path> already exists (use -c <path> to write somewhere else)

-c is global, so iota -c f.yaml config check and iota config check -c f.yaml are the same invocation.

Top level

Exactly four keys are accepted; each is a map from a name you choose to an entry. A name is any YAML string, and it is what the other layers refer to.

KeyHoldsReference
providers:endpoints — how to reach an API and how to authenticateproviders.<name>
models:configured models — which provider serves a model id, and the properties of the model itselfmodels.<name>
agents:usages — what a run names: a candidate set, a prompt, tools, MCP servers, session switchesagents.<name>
mcp_servers:the MCP servers an agent may selectmcp_servers.<name>

Any other top-level key fails the load:

config ~/.iota.yaml: agnets: unknown top-level key (want providers:, models:, agents:, mcp_servers:)

An empty document, and an empty section (providers: with nothing under it), are both fine.

providers.<name>

The endpoint layer: three keys, all optional, and nothing else. A provider is reached through a model, never named by a run.

KeyTypeDefaultMeaning
typestringthe entry's own namethe built-in provider type behind this entry — one of openai, anthropic, gemini, vertexai, openresponses, imagen, images. Omit it when the entry is named after its type (providers.anthropic:); write it for an alias (providers.deepseek: {type: openai, …}). Any other value fails when the run resolves the endpoint: unknown provider type: opnai (supported: openai, anthropic, gemini, vertexai, openresponses, imagen, images)
keystring""the API key. ${env:VAR} is expanded at merge time, so the file can name a variable rather than hold a secret. It is the second choice: the type's environment variable wins when set (below)
urlstring"" = the type's official endpointthe base URL, for a relay or a self-hosted endpoint. ${…} expanded at merge time

A name that no providers: entry declares but that is a built-in type (models.x: anthropic:claude-… with no providers.anthropic) resolves to that type over an empty entry — the official endpoint, the key from the environment.

Where the key comes from. One rule, used by the run, by iota list providers and by /model's catalog alike: the type's environment variable when it is set, else key:, else nothing — and a run with nothing says API key is required: set OPENAI_API_KEY or providers.<name>.key in your config. An exported-but-empty variable counts as unset. The variable is decided by the type, not the entry name:

TypeVariable
openai, openresponses, imagesOPENAI_API_KEY
anthropicANTHROPIC_API_KEY
gemini, vertexai, imagenGOOGLE_API_KEY

So two type: openai entries — the official one and a relay — both read OPENAI_API_KEY when it is set, whatever their key: says; give a relay its own secret through key: ${env:RELAY_KEY} and keep OPENAI_API_KEY unset, or accept that the variable wins.

The two dialects of the OpenAI protocol are two types: openai is chat-completions, openresponses the Responses API. The three Google types share a dialect: gemini is the Gemini API, vertexai is Vertex AI, and imagen is image generation; images is OpenAI-shaped image generation (see Image generation).

models.<name>

The model layer. An entry is written in one of two forms:

models:
sonnet: anthropic:claude-sonnet-4-20250514 # shorthand: "provider:id", nothing else
gpt5: # the full mapping
provider: openai
id: gpt-5.2
context_window: 400k

The shorthand must name a provider and a model: a bare name has no endpoint (models.x: "sonnet" names no provider (want "provider:model")), and provider:* is a candidate set, which only an agent can hold (models.x: "openai:*" is a candidate set, not a model (use it in agents..models)). Everything after the first colon is the id, so a relay's vendor/model shape survives (openrouter:anthropic/claude-3.5-sonnet).

The mapping form takes these twelve keys; the last four parameters are defaults an agent may override.

KeyTypeDefaultMeaning
providerstringthe entry's own namethe providers: entry (or built-in type) that serves this model, so models.openai: {id: gpt-5.2} needs no provider: line. A name that is neither fails the load: models.gpt: unknown provider "opnai"
idstring""the wire model id, verbatim. Empty leaves the model unchosen and the run starts in the model picker
context_windowstring"" = the built-in defaultthe budget compaction accounts against. A number with an optional unit suffix — 200000, 200k, 1m, 1.5m, 2b (k/m/b = ×1 000, ×1 000 000, ×1 000 000 000; case-insensitive; decimals allowed; whitespace trimmed). Refused when empty (empty size), when the number is not positive (invalid context window size: "abc"), or when it truncates to zero or overflows (invalid context window size) — reported as config context_window: … when written here, agent context_window: … when written on an agent. Interactive only: -m runs never compact
defer_modestringnormalthe protocol deferred MCP tools are mounted with. Each mode belongs to a dialect and is checked when the config loads: normal (the search_tools wrapper) on every type; reference (Anthropic's deferred-tool protocol) on anthropic only; tool-search (the Responses tool-search protocol) on openresponses only; system-tools (the frozen system-message mount) on openai only. A mismatch fails the load — models.gpt: defer_mode "reference" does not apply to provider type openai (see docs/design/tool-defer.md); an unknown spelling is a warning and normal is used. Set without any server carrying defer:, it is a warning at startup. It lives here and not on an agent because the dialect is fixed by the provider a model points at; two protocols for one model are two entries
effortstring"" = the provider's defaultthe default reasoning effort: low, medium, high, xhigh or max, exactly. Anything else: `config effort "extreme": want low
temperaturenumberunset = the provider's defaultthe default sampling temperature, 0.02.0 inclusive; outside that range the run refuses to start: config temperature 3: want 0.0-2.0. On a provider type that has no temperature (the image types) it is a warning and is dropped
top_pnumberunset = the provider's defaultnucleus sampling, 0.01.0; outside: config top_p 1.5: want 0.0-1.0. Tune this or temperature, not both, and expect reasoning models to reject or ignore it
imagebooleanfalseopts into image generation on a chat provider that needs the request-side switch (openresponses advertises the image_generation tool, the Google types add responseModalities). Redundant on imagen/images (a warning: they always generate) — see Image generation
aspect_ratiostring"" = omittedan image-provider generation default, passed through verbatim ("3:2")
image_sizestring"" = omittedan image-provider generation default, verbatim ("2K", "1536x1024" for images)
negative_promptstring"" = omittedan image-provider generation default, verbatim
json_editsbooleanfalsesend image edits as a JSON body instead of multipart — images type only; on any other type a warning, and the key is ignored

The three verbatim image knobs are warned about and dropped on a type that is not an image provider: Warning: aspect_ratio/image_size/negative_prompt apply only to image providers (ignored for type openai).

agents.<name>

The usage layer, and the only thing a run can name: iota run <name> resolves this map alone, and a bare iota runs agents.default. An entry must have at least one model; everything else is optional.

KeyTypeDefaultMeaning
modelsa model reference, or a list of them— (required: agents.a: models: at least one model is required)the candidate set, best first. Each item is one of three forms: a models: entry name (sonnet); an inline provider:id (anthropic:claude-sonnet-4, everything after the first colon being the id); or a wildcard provider:*, every model that provider lists, fetched when the picker opens. The first item is the model the run starts on; a wildcard first starts in the picker instead. models: sonnet and models: [sonnet] are the same. Errors: models: unknown model "sonet" for an entry name nothing declares, models: unknown provider "opnai" for an inline or wildcard on a provider that is neither configured nor built in — and - anthropic: claude-x (a space after the colon, which YAML reads as a mapping) gets a model reference must be a string like "provider:model", but anthropic: claude-x parses as a YAML mapping — remove the space after the colon (anthropic:claude-x)
systemstring""the system prompt, inline. Wins over system_file when both are set; -s replaces either for one run. What iota puts around it is on The system prompt
system_filestring""a file holding the system prompt, read when the run starts; ${…} expanded at merge time (${appHome}/prompts/reviewer.md). A file that cannot be read fails the run rather than sending an empty prompt: system_file: open /path: No such file or directory (os error 2)
toolsmappingnonethe built-in toolsets this agent gets. The presence of a key enables the set; its value is the set's configuration (nothing or {} = defaults), and any YAML-1.1 false spelling (false, no, off) disables it — the way to switch off ask, which is on by default interactively. The four names are shell, code, skills, ask; anything else fails the load (agents.a.tools.web: unknown toolset (want shell, skills, code, ask)), and the two retired names say what replaced them: agentthe agenttoolset is now calledskills(the wordagent names a config layer), delegatethe delegate toolset was removed — run child agents from bash instead (see README). A set whose value does not decode is a startup warning (toolset "shell": … (ignored)), not an error. Naming at least one set here is also what makes iota send its harness prompt
mcp_serverslist of nameskey absentwhich of the top-level mcp_servers: this agent loads. Absent = all of them; [] = none; a list = exactly those. A name the top-level map does not define fails the run at startup: mcp_servers: "gh" is not defined under the top-level mcp_servers
workspacebooleanfalseagent mode: the AGENTS.md chain and the skills catalog in the system prompt, the skills toolset (load_skill) enabled on its own, and sessions stored per project. It is the only way in — there is no flag. A run in agent mode that cannot resolve its working directory fails (failed to resolve working directory: …)
no_savebooleanfalsestart ephemeral, as --no-save does: nothing is written until /save. An explicit iota resume outranks it
notifybooleanabsent = onthe desktop notification sent when a reply lands or the model needs you while the terminal is unfocused. Three states: absent means on, so only an explicit notify: false silences it (notify: true is the default spelled out)
descriptionstring""what the agent is for — documentation of the entry, printed beside the name by iota list agents; the model never sees it
context_windowstring""overrides the model's own, same spelling and same errors (labelled agent context_window: …). One level: the agent over the model, no deeper
effortstring""overrides the model's default; same five values
temperaturenumberunsetoverrides the model's default; same range
top_pnumberunsetoverrides the model's default; same range

The four layered parameters

context_window, effort, temperature and top_p may be written in both models: and agents: — the only keys that may — and the rule is one line: the agent's value when it has one, else the model's, else the built-in default, evaluated when a session starts and again whenever /model switches model. It is exactly one level of inheritance, deliberately not a chain: an agent that knows it keeps long conversations says context_window: 400k once instead of forking a models: entry per usage. How a value you set by hand in /model fits in is on the config file.

Booleans

Every boolean key (workspace, no_save, notify, image, json_edits, and the toolsets' network, auto_run, auto_write, read_only) takes the YAML 1.1 spellings in any case, quoted or plain: true/yes/on and false/no/off. A null value (workspace: with nothing after it) reads as false — or, for notify, as absent. Anything else fails the decode with invalid value: expected a boolean (true/yes/on/false/no/off).

mcp_servers.<name>

One MCP server the agents may select. iota mcp add writes these entries and iota mcp remove deletes them, rewriting only this block of the file (see MCP servers); every key is also fine to write by hand. A server is either stdio (command, with args and env) or streamable HTTP (url, with headers and the OAuth keys).

KeyTypeDefaultMeaning
commandstring""stdio transport: the program to spawn. ${…} expanded when the server starts
argslist of strings[]stdio transport: its arguments, each expanded (["-y", "@modelcontextprotocol/server-filesystem", "${workspaceFolder}"])
urlstring""streamable-HTTP transport: the endpoint URL, expanded
envmap of string to string{}extra environment for the child process; values are expanded, names are not
headersmap of string to string{}extra HTTP headers; values expanded, names not. An Authorization header (any case) makes the entry auth: none in effect — you wrote the credential, so a 401 means that one is wrong, not that a login is missing
deferstringabsent = advertise fullydeferred loading: the presence of the key defers the server, and the value is the one-line summary of its tools shown in the manifest — which is why it is a string, not a bool. An empty string is a loud warning and the server is not deferred: Warning: mcp server <name>: defer needs a one-line summary of the server's tools (not deferred)
authauto | oauth | noneautohow an HTTP server is authenticated. auto discovers it — a token file when there is one, else the server's own 401 at the handshake; oauth forces the login (no token = not logged in, no bare attempt); none forbids it (a 401 is a failed connect, and iota mcp login refuses the entry). On a stdio server auth: oauth fails the load: mcp_servers.<name>: auth: oauth needs a url (a stdio server has nothing to log in to)
client_idstring"" = dynamic registration, else the Client ID Metadata Documentan OAuth client registered with the authorization server out of band
client_secretstring"" = a public clientthe secret paired with client_id, written as a ${env:VAR} reference and expanded like every other value. Needs a client_id: mcp_servers.<name>: client_secret needs a client_id
redirect_portintegerabsent = 17801with a client_id: the loopback port the registered redirect URI (http://127.0.0.1:<port>/callback) was registered with — a pre-registered client's URI must match exactly, so the port is fixed, where a client registered on the spot gets a random one. Needs a client_id: mcp_servers.<name>: redirect_port needs a client_id (only a pre-registered client has a fixed redirect URI)

The three client keys describe a login, so any of them on a stdio server (mcp_servers.<name>: client_id/client_secret/redirect_port describe an OAuth login, and a stdio server has nothing to log in to) or beside auth: none (… describe an OAuth login, which auth: none rules out) fails the load. An unknown key here is mcp_servers.fs.commadn: unknown key (want command, args, url, env, headers, defer, auth, client_id, client_secret, redirect_port).

Project scope holds references, not secrets. ./.iota.yaml is shared with everyone who clones the project, so iota mcp add --scope project refuses a header or environment value that is not a ${…} reference: mcp: a project-scope value must reference an environment variable (${NAME}), not the secret itself: headers.Authorization. Nothing stops you writing a secret there by hand; the command will not do it for you.

Variable expansion

${…} references are substituted in these fields and no others: providers.<name>.key and .url, agents.<name>.system_file (all three at merge time), and every MCP server's command, args, url, env values, headers values, client_id and client_secret (when the server starts). Map keys — an env variable's name, a header's name — are never expanded.

VariableExpands to
${env:VAR}the environment variable VAR; always substituted, an unset or empty one becoming the empty string
${workspaceFolder}, ${cwd}the working directory
${userHome}the home directory
${appHome}iota's own directory, ~/.iota
${pathSeparator}, ${/}the OS path separator

One pass, left to right: the result of a substitution is never rescanned, so a variable whose value is itself ${cwd} stays literal. A name the table does not know — or one whose lookup failed (no home, no cwd) — is left untouched as written, and ${} is not a reference at all. Names are case-sensitive.

Errors

The key audit runs on the raw document, before it is decoded, so every message carries the coordinate the key was written at; the loader prefixes it with the file. In order of precedence:

CaseExampleMessage
unknown top-level keyagnets:agnets: unknown top-level key (want providers:, models:, agents:, mcp_servers:)
a retired keyproviders.p.modelproviders.p.model: modelis now amodels:entry — writemodels.: :and list it inagents..models``
providers.p.agentproviders.p.agent: agentis nowworkspace:on anagents: entry
a key of another layerproviders.p.systemproviders.p.system: systembelongs underagents: (see README, "The three layers")
agents.a.urlagents.a.url: urlbelongs underproviders: (see README, "The three layers")
an unknown keyproviders.p.kyeproviders.p.kye: unknown key (want type, key, url)
a retired toolsetagents.a.tools.agentagents.a.tools.agent: the agenttoolset is now calledskills(the wordagent names a config layer)
agents.a.tools.delegateagents.a.tools.delegate: the delegate toolset was removed — run child agents from bash instead (see README)
an unknown toolsetagents.a.tools.webagents.a.tools.web: unknown toolset (want shell, skills, code, ask)

The four layered parameters are valid in both models: and agents:, so neither layer reports the other for them. A models: shorthand and a tools: value are not mappings the audit owns, so their contents are checked by the code that decodes them — the messages above under each key.

There is no migration layer and no compatibility shim. A key that silently did nothing is the failure this audit exists to close.