Configuration reference¶
Every config.toml key Kokua reads, what it accepts, and what it does. This is the long form of
config.example.toml, which is the same set of keys with a line
of description each; read that file to see the shape, and this one when a key's behavior is not obvious
from its name.
Two rules run through the whole file and explain most of what follows:
- A capability is declared, never defaulted. An agent holds exactly the toolsets its
[agents.<name>].toolslist names. No code path adds one it did not name, and no flag can disagree with the declaration. See set up a toolset. config.tomlis the single source of settings, and the app writes it too. There is no second store, no dotfile, no database. The assistant's ownupdate_configtool and the runtimeadd_mcp_servertool write back to this file, preserving your comments, which is why[security].locked_config_keysexists, and why it is the one key that list cannot unlock.
Contents¶
- Where the file lives
- Precedence
- What happens when a key is wrong
- Who may change which key
- Which keys apply live
- Sections:
[assistant]·[assistant.generation]·[security]·[agents.<name>]·[[mcp.server]]·[paths]·[frontend]·[web]·[logging]·[email]·[scheduling]·[scheduling.task.<name>]·[planning]·[capabilities]·[github_backup]·[compute]· toolset sections in general - Environment variables
- Command-line flags
Where the file lives¶
Kokua reads the first location that is specified, and only that one:
--config PATH$KOKUA_CONFIG$KOKUA_HOME/config.toml, the default, where$KOKUA_HOMEitself defaults to~/.kokua
A file missing from the default location is not an error at this stage; a file missing from a location
you named explicitly is, so a typo in --config fails loudly instead of silently running on defaults.
Scaffold one with:
kokua config init # writes the documented example to the config location
kokua config init --force # overwrite an existing file
kokua config init --path PATH # write somewhere else
The file is required, and Kokua will not start without one. Every individual key has a built-in
default, so you set only what you want to change, but the [agents.*] tables exist nowhere else and an
assistant with no agent cannot work. There is no useful "no config" state to fall back to, so startup
fails with a message pointing at kokua config init.
Precedence¶
Highest wins:
command-line flag > config.toml > built-in default
For the reasoning-effort and generation tiers, AIMU adds precedence below the config file. Resolved
lowest first: the model client's own fallbacks, then the model card's tuned sampling profile, then what
Kokua writes from [assistant.generation] and [agents.<name>.generation], then a per-call override.
The practical consequence is in [assistant.generation]: a key you set replaces
the card's recommendation for that one parameter, and a key you leave out keeps the card's value.
What happens when a key is wrong¶
An unknown key, an unknown section, or a wrong-typed value is a hard startup error naming the key. Kokua does not skip a line it cannot parse. A few keys that existed in earlier layouts are recognized specifically so the error can say where they moved to, rather than reporting them as unknown.
update_config runs the same validation before it writes, so the assistant cannot save a value that
would prevent the next startup. [assistant].model gets one extra check there that the file itself does
not do: the string must resolve to a client this process could build, because a startup-only key written
by a tool outlives the conversation that wrote it.
Who may change which key¶
[security].locked_config_keys decides this, and it is yours to set. Kokua ships with:
[security]
locked_config_keys = [
"security.*",
"email.to",
"paths.data_dir",
"agents.*",
"scheduling.task.*",
"compute.command_env_passthrough",
]
A pattern takes one of three forms:
| Form | Matches |
|---|---|
* |
every section and key |
<section>.* |
that section, every section under it, any key |
<section>.<key> |
exactly that key in exactly that section |
Keys never contain dots and sections do, so the last segment of the third form is always the key. To
lock one task's contents, write "scheduling.task.morning-brief.*", not
"scheduling.task.morning-brief". Anything that is none of the three forms fails startup naming the
pattern, and so does a pattern whose shape is right but whose section or key does not exist: agnets.*,
Agents.*, and security.confirm_tool are all refused. What no check can catch is a name only you
bring into being. agents.resercher.* and scheduling.task.mornin-brief.* name an agent and a task
that could be created tomorrow, so there is no closed set to test them against, and each one loads while
locking nothing. See locked_config_keys for the full list of what is refused.
One key is always locked: [security].locked_config_keys itself, whatever the list says, including
when it is empty. Otherwise an assistant holding update_config would need a single call to disable
every other lock. Note how narrow that is: only this key, not the [security] section around it. Set
locked_config_keys = ["email.to"] and [security].confirm_tools becomes writable by the assistant,
because the shipped security.* pattern was the only thing that was locking it.
Everything else is yours to remove, and here is what removing each shipped pattern actually permits:
| Pattern | Removing it |
|---|---|
security.* |
lets the assistant change confirm_tools, and so remove its own approval gate |
email.to |
lets the assistant mail someone other than you |
paths.data_dir |
lets the assistant move its own state out from under you |
agents.* |
lets the assistant rewrite any agent's tools, model, thinking, max_iterations, system_message, description, and delegates_to, set its [agents.<name>.generation] parameters, and create new agents (under a name of letters, digits, hyphens, or underscores). It can widen its own reach, effective on the next restart. |
scheduling.task.* |
changes the error message only. update_config still cannot write a task: the scheduling tools are the write path, because a task write has to be paired with the scheduler arming or disarming to match, and a bare config write would leave the running scheduler firing the old schedule. |
compute.command_env_passthrough |
lets the assistant name its own credentials for a run_command child to see; because the key is cold, that exposure is persistent, surviving into every later session rather than one command |
A flat agent key (tools, model, thinking, system_message, description, delegates_to) is
checked before it is saved by the same validate_agents that runs at startup, so an unknown toolset
name, an unresolvable model, an unknown delegate, or a delegation cycle is refused at the tool rather
than breaking your next launch. That check reads config.toml itself, not the copy this session started
from, and reads both halves of it that startup reads: the agent tables and the [assistant].agent
naming the entry agent. So two writes that each looked fine alone cannot add up to a file your next
launch refuses. A [agents.<name>.generation] parameter gets a different check instead:
the same type-and-range check [assistant.generation] gets, not a validate_agents dry run, since a bad
temperature cannot break startup the way a bad delegate can. An agent's max_iterations is the
exception: the agents.* write path checks it the same way it checks a flat key, by dry-running
validate_agents, and that dry run does not repeat the range check parse time applies. So a 0 or a
negative value passes the tool and is only refused at the next startup, when config/file.py parses it,
and reaching that write at all means agents.* has already been removed from locked_config_keys,
since the table is locked by default. Every other way in, the write is checked before it is saved; no
check tells you the result is one you wanted, only that it starts.
[assistant].max_iterations is not locked, so the assistant can raise or lower its own tool-loop cap
there. That is deliberate, for the reason [assistant].model is unlocked: the key is startup-only, so
no write escalates the session doing the writing, and what it moves is cost rather than reach.
[[mcp.server]] is not in the list and is not writable by update_config either way; it is appended by
add_mcp_server, which connects the server as well as recording it.
Which keys apply live¶
A hot key takes effect on the next turn, with no restart, and is written back to config.toml
immediately. Everything else is startup-only: change it, then restart.
| Hot | Startup-only |
|---|---|
[assistant].generate_titles |
the rest of [assistant], including [assistant.generation], and [planning].review_rounds |
[planning].plan_review, plan_review_agent, result_review, show_reasoning |
|
[capabilities].max_depth |
all of [agents.*], [mcp] (including [[mcp.server]]), [security], [paths], [frontend], [web], [logging], [email] |
[scheduling].max_task_conversations |
The model, the reasoning effort, and the tool-loop cap read like runtime settings and are not. Nothing
rebinds a live model client, nothing re-caps a live agent's loop, and no runtime writer can reach
[agents.*], so a hot [assistant].model or [assistant].max_iterations could only ever report a
change it had not made, or disagree with an agent's own declaration. Generation parameters are
startup-only for a second reason as well: a live control always holds some value, so it would write
that tier whether or not you asked for anything, shadowing every model card's tuned profile. A key absent
from the file has to stay absent from the request.
A toolset declares its own settings and says which of them are hot. See toolset sections.
[assistant]¶
Process-wide defaults. Everything here is read at startup, except generate_titles, which is hot.
model¶
The default model every agent runs on. An agent that declares its own model runs on that instead.
Unset, Kokua resolves $AIMU_LANGUAGE_MODEL, and failing that the first already-running local model it
finds: a running Ollama server first, then a local OpenAI-compatible server. A cloud model is never
auto-selected. If nothing resolves, startup fails with an actionable message.
The string has two optional suffixes for servers AIMU does not know about:
| Form | Meaning |
|---|---|
provider:model |
the ordinary case |
provider:model@<base_url> |
override the endpoint (llama.cpp llama-server, vLLM, LM Studio, SGLang, HF-Serve) |
provider:model@<base_url>;<flags> |
also declare capabilities for a model id not built into AIMU |
<flags> is comma-separated from tools, thinking, vision, audio, structured.
model = "llamaserver:qwen3-8b.gguf@http://gpu-box:8080/v1"
model = "openai-compat:my-model@http://gpu-box:9000/v1;tools,thinking"
Both suffixes survive the $AIMU_LANGUAGE_MODEL route as well, which is worth stating because it is the
natural way to share one config.toml across machines that serve different models:
Whichever route the default arrives by, it is resolved once and every agent gets the same string:
sub-agents spawned through spawn_subagent, sub-agents built by compose_subagent, the prebuilt
orchestrators in the aimu_agents toolset, and both /plan reviewers. An endpoint set here reaches all
of them. (Before Kokua 0.1.0 it did not: with model unset, the endpoint was dropped for everything
except the entry agent, so a remote default sent every sub-agent to the local server instead. It failed
loudly only when nothing was listening there.)
Note this pins the client, not discovery. The probe that picks a default when nothing is set at all
looks at default endpoints only, so export OLLAMA_HOST as well when a remote server's models should be
considered there too.
thinking¶
The default reasoning effort every agent runs at. An agent that declares its own thinking runs at that
instead.
| Value | Effect |
|---|---|
| unset (default) | send nothing; the model keeps its own behavior |
false |
do not reason |
true |
reason, at the model's own default effort |
"low", "medium", "high" |
reason at this effort |
Two caveats. A level is advisory on a model whose card does not declare effort-level support: AIMU
warns once, and reasoning is still on, just not at the level asked for. And false additionally selects
the model card's instruct-mode sampling profile where the card specifies one (the Qwen 3.5, 3.6, and 3.8
cards do; most models have a single profile and are unaffected), because a model's thinking-mode and
non-thinking-mode sampling defaults are usually different.
An agent's declaration is resolved on "is it unset", not on truthiness, so thinking = false on an agent
genuinely overrides a "high" default rather than being swallowed by it.
This key is not the last word: a single turn can ask for its own effort from the web composer's picker
or the CLI's /think, beating whatever this key and an agent's own declaration resolve to for that turn
alone. Nothing about the request is stored in config.toml; see
Architecture for
the full resolution order.
system_message¶
Fallback opener for any agent that sets no system_message of its own. The --system flag overrides the
entry agent's opener for one run, and leaves a worker's declared opener alone.
agent¶
Which [agents.<name>] table is the agent you talk to, and the root of the delegation graph.
Default "assistant".
concurrent_tools¶
Run independent tool calls in one turn concurrently, so several spawn_subagent calls overlap.
Default true.
agent_cache_cap¶
Maximum per-conversation agents kept live in memory. Default 8. An evicted agent rebuilds from
persisted state on next access, so the cap bounds memory, not correctness.
max_iterations¶
The tool-loop cap every agent runs under unless its own [agents.<name>].max_iterations overrides it:
the most model calls one turn may make before AIMU stops the loop. Default 10, which is also AIMU's
own default, so leaving this unset changes nothing.
The count is model calls, not tool calls: the initial turn, plus every tool-follow-up turn, plus any continuation nudge the loop injects after an empty turn. The one call it does not count is the forced wrap-up, which runs after the cap is reached with tools disabled, so an agent that hits the cap still gets to answer from what it gathered. That is why hitting the cap is not an error and nothing in the transcript is marked truncated: you get a thinner answer, not a failure. The turn loop shows what that looks like in a real run.
Must be an integer of 1 or more. A 0 would be a loop that makes no model call at all, so it is a
startup error rather than a silently useless setting.
Raise it for work that legitimately needs more rounds, which in practice means search: a worker that spends every round calling tools is the one still mid-investigation when the cap lands. Prefer raising it on the one agent that needs it, via the per-agent key below, over raising it here for everyone, since every agent's worst-case turn cost scales with this number.
This key is not locked, so the assistant can change it too: see who may change which key.
generate_titles¶
Have the model write each new conversation's title from the message that opened it. Default true.
Hot, and the only hot key in [assistant]: the title is written per conversation, so a change
applies to the next one with no restart.
The first message truncated to 40 characters is the title the instant a conversation has one; the
written title replaces it a moment later, in the background, on the model that conversation runs on
(no separate model to configure). false stops at the truncation, which is also what stands whenever
the call fails: an endpoint that is down, or an answer that cleans up to nothing, is silent and leaves
the placeholder. A conversation a scheduled task minted keeps the task's name either way.
[assistant.generation]¶
Sampling and length parameters for every agent, each overridable per key by an agent's own
[agents.<name>.generation] table. Startup-only.
| Key | Type | Range |
|---|---|---|
temperature |
number | 0.0 to 2.0 |
top_p |
number | 0.0 to 1.0 |
top_k |
integer | at least 1 |
min_p |
number | 0.0 to 1.0 |
presence_penalty |
number | -2.0 to 2.0 |
repetition_penalty |
number | greater than 0.0 |
max_tokens |
integer | at least 1 |
context_length |
integer | at least 1 |
There is no default for any of them, and that is the design rather than an omission. Only the keys you set are sent. This tier sits above the model card's own tuned profile in AIMU's precedence chain, so a value set here replaces the card's recommendation for that one parameter and leaves the rest of the profile in force. Setting nothing keeps the whole profile.
Three things to know:
- A parameter the backend cannot take is dropped, with a warning naming where to set it instead
(Ollama has no
min_p; the Anthropic API has no penalties). A value that works locally therefore does not break a cloud model, it just stops applying. That warning goes only to the rotating log atdata/logs/kokua.logunder your Kokua home, not to the chat or the terminal, so look there when a parameter you set has no effect. max_tokensandcontext_lengthare different knobs and they interact.max_tokenscaps generated tokens;context_lengthsizes the whole window, which the prompt and the generated tokens share. With the window at 32768 and the cap at 4096, roughly 28k is left for the system prompt, the tool block, and the conversation. Note also that AIMU's own fallback setsmax_tokens = 1024on Anthropic, the OpenAI-compatible family, and llama.cpp, which is low for a turn carrying tool results.context_lengthis set per request only on Ollama's native API. Everywhere else the window is fixed at model load time, at server launch (--ctx-size,--max-model-len), or by the vendor, and the key is dropped with a warning naming that remedy.
Because this is a TOML sub-table, it must come last in [assistant]: any plain [assistant] key
written after the [assistant.generation] header would belong to the sub-table instead. The same is true
of an agent's own generation table.
[security]¶
confirm_tools¶
Tools that require interactive confirmation before each call: a terminal y/N, or Allow/Deny in the web
UI. These are the tools that run with full machine access.
confirm_tools = ["add_skill_script", "add_mcp_server", "execute_python", "run_command", "update_config"]
Set to [] to disable approval entirely. Proactive turns (scheduled tasks, anything the assistant starts
unprompted) auto-deny these regardless of the setting, since there is no one at the keyboard to ask.
update_config is in the default list because it lets the assistant rewrite this file, except for the
locked keys it can never change. This key is itself locked by default too, for the obvious reason.
Gating is by tool name, so it applies to a worker's call as much as the entry agent's: a worker's
gated call is routed to you. Note the flip side, which is easy to misread: there is no privilege tier
among agents. An agent whose table declares config really does get update_config, and one declaring
compute really does get execute_python. Your hand-edit is the consent, and this list is the gate at
call time.
A name no configured agent provides fails startup, naming every entry that matched nothing and
suggesting the close names. A gate is a plain name match, so a misspelled entry holds nothing back, and
the only sign of it is a prompt that never comes, which is not a thing anyone notices:
confirm_tools = ["execute_pythn"] once loaded clean and then let execute_python run unattended for
as long as the file stood. The vocabulary is every tool this config builds, which is wider than the
entry agent's own: execute_python is there because [agents.coder] declares compute, and
spawn_subagent and a skill's {skill}__{script} tools count as well. An empty list names nothing, so
there is nothing for the check to refuse.
The limit is anything that does not exist when startup ends. A tool from an MCP server the assistant
connects later with add_mcp_server has no name to match yet, and neither does a tool only
compose_subagent would build, out of a toolset no [agents.*] table names. Listing either one is
refused, which will look wrong if you were deliberately gating ahead of the connection. To gate an MCP
tool, give its server a [[mcp.server]] table here and name that server in an agent's tools; to gate
a tool from a toolset nothing declares, name that toolset in an agent's tools. Both are known from the
next start.
Four names worth considering adding, all ungated by default: read_conversation,
search_conversations, rename_conversation, and export_conversation. A saved transcript is
untrusted text, since a worker may have pasted web content into it, so an injection landing in one
conversation can influence another. The two reads are ungated by default because gating a read would
make an unattended scheduled run that reads history fail silently. The rename writes, but it writes one
field: the worst an injection gets from it is a conversation misleadingly named in the sidebar, with no
message touched and nothing lost. The export is the one whose reason to gate is not about the
conversation at all: it changes nothing and can only write inside downloads_path under a name taken
from the conversation's own id, but what it writes there is a whole transcript in the clear, in a folder
/download/{name} serves unauthenticated. Gate it if that folder is the part you are protecting.
locked_config_keys¶
A list of patterns naming which keys update_config refuses. Default:
["security.*", "email.to", "paths.data_dir", "agents.*", "scheduling.task.*",
"compute.command_env_passthrough"]. Startup-only, and
always locked against update_config regardless of its own value. See
Who may change which key for the pattern forms and what each shipped
pattern is holding back. A pattern that could never match anything is a hard startup error rather than a
line that silently locks nothing, and two checks decide that.
The first is the pattern's shape: a bare display with no dot, whitespace at either end of the pattern,
an empty segment (agents.), a * sharing a segment with other characters (agent*.tools), or a *
anywhere but the last segment (*.*, agents.*.*) all fail. The second is its vocabulary, read off the
schema this install actually has, so it knows the sections your installed toolsets contribute as well as
Kokua's own. The first segment must be a real section, which is what refuses agnets.* and, since TOML
keys are case-sensitive, Agents.*. An exact <section>.<key> in a section whose keys are known must
name one of them, which is what refuses security.confirm_tool while accepting
security.confirm_tools.
Neither check can see a name that does not exist yet, and neither tries to. The [agents.<name>] and
[scheduling.task.<name>] sections are yours to create, so agents.resercher.* is accepted and locks
nothing until an agent by that name exists. Locking a section you are about to add is a legitimate thing
to write; a misspelling of one is indistinguishable from it.
[agents.<name>]¶
Every agent, declared whole. The table name is the agent's name: it is what [assistant].agent selects,
what another agent's delegates_to names, and what spawn_subagent takes as its agent_type.
A name you write by hand is any TOML key, quoted where it has to be. A name update_config writes is
narrower, letters, digits, hyphens, and underscores, so the section the tool reports is the section that
reaches the file: anything else would be quoted on the way in and land under a name you could not name
again. agents.* is refused for that reason and one more, being the wildcard a lock pattern covers the
whole section with rather than an agent.
This whole section is locked by default. See who may change which key
for what removing agents.* from [security].locked_config_keys actually permits.
| Key | Type | Meaning |
|---|---|---|
description |
string | the label a delegator sees in its worker menu |
system_message |
string | this agent's opener; falls back to [assistant].system_message |
tools |
list of strings | this agent's capability, named from the one toolset namespace |
delegates_to |
list of strings | agent names this agent may spawn as workers |
model |
string | overrides [assistant].model for this agent alone |
thinking |
bool or level | overrides [assistant].thinking for this agent alone |
generation |
sub-table | overrides [assistant.generation], per key |
max_iterations |
integer >= 1 | overrides [assistant].max_iterations for this agent alone |
tools¶
An agent's tools list is its capability. Nothing is added in code, so a toolset you delete from a
list is gone, and one you add is there on the next start.
There is one namespace for every capability, so a name may be an AIMU built-in tool group (web, fs,
compute, time, misc, audio, speech, transcription), one of Kokua's own (memory,
documents, skills, capabilities, config, conversations, mcp, planning, scheduling),
an installed plugin toolset (aimu_agents, benchmark, github_backup, image), a skill in your skills folder
named by its own name, or an MCP server configured under [[mcp.server]], named by its name. The list
does not say which kind a name is. Run kokua --list-toolsets for every name this install accepts,
grouped by what provides it.
An unknown name is a startup error listing the valid ones, so a typo can never quietly leave an agent
with a smaller toolset than you wrote. Nothing is implicit, including the clock: time is added to no
agent in code, which is why every shipped agent lists it. An installed plugin toolset and a configured
MCP server likewise do nothing until some agent names them, and startup warns about one that nothing
names.
The full inventory of what each toolset contains is in set up a toolset.
delegates_to¶
A non-empty delegates_to is what makes an agent a delegator. There is no separate switch that could
disagree with it. The agent gets a spawn_subagent(agent_type, task) tool offering exactly the agents it
names, each built from its own table. The graph must be acyclic.
A worker's gated tool calls (see [security]) are routed to you for approval, not run
unattended.
model, thinking, generation, max_iterations¶
All four are resolved per agent and never inherited down the delegation graph: a delegator that pins
a big model, reasons hard, runs cold, or loops longer does not drag its workers along. A worker declaring
nothing runs on the [assistant] defaults like every other undeclared agent.
An agent's model is the same string [assistant].model is, suffixes included: a worker can
be pinned to its own endpoint, or to the same remote server the assistant uses. Worth stating because the
two are checked by different code (a worker's is parsed at startup, [assistant].model by building a
throwaway client), so a reader has no way to tell from the code alone that they accept the same thing.
generation merges per key rather than table for table, so an agent that wants only a colder temperature
keeps the default's context length.
[agents.researcher]
description = "Research specialist: gather and verify information from the web."
model = "ollama:qwen3:32b" # this worker alone runs on a bigger model
thinking = "high" # ...and reasons harder than the rest
tools = ["web", "misc", "time"]
system_message = "You are a research sub-agent. ..."
[agents.researcher.generation] # last in the table, like [assistant.generation]
temperature = 0.2
context_length = 131072
A model string AIMU cannot resolve fails startup naming the table it came from.
max_iterations follows that rule too, and it is worth spelling out. An assistant that declares
max_iterations = 40 does not hand 40 to the workers it delegates to; each of them gets its own
declaration if it has one, and [assistant].max_iterations otherwise. So raising a delegator's cap
buys the delegator more rounds of delegating, not more rounds inside each worker. To give a worker a
longer leash, declare it on that worker.
[agents.researcher]
tools = ["web", "misc", "time"]
max_iterations = 25 # a search-heavy worker: every round spends a tool call
[[mcp.server]]¶
Remote MCP servers to connect at startup. An array of tables: one [[mcp.server]] header per server.
| Key | Required | Meaning |
|---|---|---|
url |
yes | the server's endpoint |
name |
yes | how the server enters the toolset namespace |
token_env |
no | environment variable holding a bearer token, read at startup |
name is what an agent lists in its tools, so a server no agent can name reaches no agent. Omit
token_env for an unauthenticated server, or one using the OAuth flow, which triggers automatically on
an auth challenge. The token stays in the environment, never in this file.
[[mcp.server]]
url = "https://api.githubcopilot.com/mcp/"
name = "github"
token_env = "GITHUB_MCP_TOKEN"
[[mcp.server]]
url = "https://broker.example.com/mcp"
name = "stocks"
Servers added at runtime with the add_mcp_server tool are appended here automatically, with a name
derived from the host and a numeric suffix if that name is taken, so the file always loads. That name
reaches no agent until you add it to a tools list and restart, since the toolset namespace and the
agent tables are both read only at startup. See add an MCP
service.
The OAuth callback¶
Two scalar keys sit in [mcp] itself, alongside the server array. They decide where an OAuth provider
sends your browser once you approve, which is also where Kokua listens for the code.
| Key | Default | Meaning |
|---|---|---|
oauth_callback_host |
"localhost" |
host in the registered redirect URI, and the interface the callback server binds |
oauth_callback_port |
unset (a free port, chosen per process) | port for both |
The defaults assume the browser runs on the same machine as Kokua. If it does not, the provider sends
your approval to your loopback, Kokua never receives it, and the connection fails only when the flow
times out. Pin the port and forward it (ssh -L 8765:localhost:8765 kokua-host), or set the host to
Kokua's own name if your provider accepts a non-loopback redirect URI. Pinning the port also protects a
re-authorization in a later session, since the client registration is cached across restarts while a
random port is not. Add an MCP service
covers both setups.
[paths]¶
data_dir¶
Absolute override for where all transient and user content lives. Default: $KOKUA_HOME/data.
Locked by default.
Every leaf derives from this one setting, so overriding it moves all of them together:
$KOKUA_HOME/
config.toml
data/ <- data_dir
sessions.json conversations
memory/ the memory store
documents/ the document store
skills/ installed skills
images/ uploaded and generated images, served at /images
downloads/ generated artifacts (PDFs, exported conversation Markdown), served at /download
logs/kokua.log the rotating diagnostic log
To move the whole tree including config.toml, set $KOKUA_HOME instead. That one is an environment
variable rather than a setting because the config file lives inside it, so it has to resolve before the
file can be read.
[frontend]¶
name¶
Which front end kokua runs: "cli", "web", or any installed plugin. Default "cli". kokua-web is
a convenience for --frontend web. List what is installed with kokua --list-frontends.
[web]¶
Bind address for the web front end. Ignored by other front ends.
| Key | Type | Default |
|---|---|---|
host |
string | "127.0.0.1" |
port |
integer | 8000 |
The default binds to loopback only. Kokua is a single-user assistant with no authentication layer of its own, so putting it on a routable address exposes an unauthenticated assistant holding your machine's tools. Front it with something that authenticates if you need remote access.
[logging]¶
level¶
Level for the rotating diagnostic log at <data_dir>/logs/kokua.log (5 files, 2 MB each). One of
DEBUG, INFO, WARNING, ERROR. Default "INFO".
The log records the turn lifecycle (submitted, lock acquired, done, error), so a hung turn is visible
after the fact. It is also where a dropped generation parameter reports itself. For live state, the
/diag chat command reports turn and lock state and dumps a wedged turn's async stack, and
kill -USR1 <pid> dumps all thread stacks.
[email]¶
Lets the assistant email information to you (digests, summaries, reports) over SMTP, through the
email-report skill. Install it with kokua skills install email-report, then name it in an agent's
tools alongside fs and compute so that agent can run its script.
| Key | Type | Default | Meaning |
|---|---|---|---|
host |
string | unset | SMTP server |
port |
integer | 587 |
587 with STARTTLS, or 465 with use_ssl = true |
username |
string | falls back to from, then to |
SMTP login user |
from |
string | falls back to to |
From: header |
to |
string | unset | the only address the assistant can send to; locked by default |
use_ssl |
bool | false |
false is STARTTLS on connect, true is implicit TLS (SMTP_SSL) |
Leave everything unset to disable email.
Three properties worth understanding before enabling it:
- The recipient is locked. Kokua passes these settings to the script's environment and the script has
no recipient flag, so it can only ever mail
to. That is also whytois locked by default: it is the whole guarantee. - Sending is ungated. There is no per-send confirmation, because a scheduled or proactive turn has no one to ask and auto-denies gated tools. The recipient lock is what makes that safe.
- The password is never in this file. It is read from
$KOKUA_EMAIL_PASSWORD, and apasswordkey here is an unknown-key error rather than a working shortcut. For Gmail or Google Workspace, use an App Password. The script sends nothing, and says so, unlesshostandtoare set and that variable is present.
[scheduling]¶
This section exists because the scheduling toolset declares it. See toolset
sections.
max_task_conversations¶
How many conversations a scheduled task keeps. Default 3. Hot.
Every firing runs in its own conversation. Once a task has more than this many, its oldest are deleted
after the next firing succeeds. 1 means each run replaces the one before it; 0 keeps every run
forever. A task can override this for itself through the max_conversations argument of schedule_task,
and this is what the tasks that do not override it follow.
[scheduling.task.<name>]¶
Scheduled tasks, one table per task. The table name is the task's identity: it is what the sidebar shows,
what cancel_scheduled_task takes, and what is stamped on every conversation the task runs in.
Changed through the scheduling tools or by hand, never by update_config. See who may change which
key.
| Key | Type | Meaning |
|---|---|---|
prompt |
string | what the assistant is asked when the task fires |
schedule |
inline table | when it fires (below) |
enabled |
bool | omit to leave it enabled; written as false when disabled or retired |
max_conversations |
integer | omit to follow [scheduling].max_task_conversations |
schedule is one of:
schedule = { type = "once", at = "2026-08-20T09:00:00" } # ISO-8601 local datetime
schedule = { type = "interval", seconds = 3600 }
schedule = { type = "daily", at = "09:00" }
schedule = { type = "weekly", day = "fri", at = "16:00" } # mon/tue/wed/thu/fri/sat/sun
A fired one-shot is not deleted. It is left in the file with enabled = false and a fired_at
stamp, so the run stays on the record and re-running it is a one-character edit.
A task's turn is proactive, which means gated tools auto-deny inside it. Give a task work its agent can finish unattended.
[scheduling.task.morning-brief]
prompt = "Summarize my calendar and any unread mail, and flag anything that needs a reply today."
schedule = { type = "daily", at = "09:00" }
enabled = true
max_conversations = 3
[planning]¶
Deep planning is a toolset: an agent's tools must list planning for the /plan <task> command to
exist at all. It drafts an explicit plan, naming which tools, skills, and MCP services to use or build,
before doing the work. Every key here belongs to that toolset and is read only when an agent declares it.
| Key | Type | Default | Hot | Meaning |
|---|---|---|---|---|
plan_review |
bool | false |
yes | pause the planned turn for your Approve / Edit / Reject; off runs the plan autonomously |
plan_review_agent |
bool | false |
yes | an independent, context-free agent critiques the plan, and Kokua re-plans on rejection |
result_review |
bool | false |
yes | an independent agent checks the final answer before it is shown, and Kokua revises on rejection |
review_rounds |
integer | 2 |
no | bounds each replan or revise loop |
show_reasoning |
bool | false |
yes | stream every LLM call in a planned turn under labeled phase headers |
result_review runs the answer non-streamed, since it has to exist in full before it can be checked.
show_reasoning shows the planner, each reviewer's prose reasoning and verdict, the executor, and every
revision, including each intermediate version, which overrides result_review's hide-until-vetted gate.
[capabilities]¶
Capability discovery is a toolset: an agent's tools must list capabilities for the agent to see what
is installed beyond its own tools. It gets two tools. list_capabilities reads the registry, every
installed capability except this one, which the agent reading it already holds. compose_subagent builds a
sub-agent holding exactly the capabilities one task needs, and runs it.
Unlike the sub-agents in [agents.*], a composed one is not declared anywhere: its capabilities are
chosen per task from everything installed, except two names it can never be given. skills works only
on the agent Kokua constructs directly, and capabilities itself would hand it a fresh
composition budget. It runs on [assistant].model with the [assistant] thinking
and generation defaults, and its tools still go through [security].confirm_tools, so execute_python
and add_mcp_server still ask you first. Composing itself is not in the shipped confirm_tools, so it
does not ask; add compose_subagent there to gate that too.
max_depth¶
How far composition may nest. Default 3. Hot.
At 3 a chain reaches three sub-agents, and the last of them holds neither compose_subagent nor
list_capabilities, since a compose_subagent with no way to look up capability names is useless to
whatever holds it. 0 switches composing off entirely. The cap is read when compose_subagent is called,
so an update_config change applies to the next composition with no restart, though a chain already
running keeps the count it started with.
[github_backup]¶
Declared by the github_backup toolset: an agent's tools must list github_backup for
backup_kokua_state to exist. The tool takes no arguments; everything it needs comes from here.
| Key | Type | Default | Hot | Meaning |
|---|---|---|---|---|
repo |
string | "" |
no | owner/name of the backup repository. Required: with it blank the toolset offers no tool at all, the same gate the image toolset applies to its model env var |
branch |
string | "main" |
no | the branch backups are pushed to |
Both keys need a restart to apply. Neither is hot, so an update_config write reaches the file without
reaching the AssistantConfig the running process holds; repo is additionally read only once, in
build, when the toolset's tools are assembled for an agent. (branch is read per call, but off that
same unchanged in-memory copy, which is why it needs the restart too.)
Changing repo after a backup has run takes one more step: delete the data/backup working tree.
Its origin is whatever the first run recorded, and Kokua refuses to push to that while checking a
different repository's privacy, so a repointed key fails with a message naming both until the tree is
gone. The next backup then clones the new repository from scratch, carrying none of the old
repository's history with it.
The push token is not a config key. It is read from the GITHUB_BACKUP_TOKEN environment variable,
fixed rather than named in config.toml, so that repointing repo (which update_config can do unless
you lock the key yourself with "github_backup.repo" in locked_config_keys, as
no toolset section is locked by default) can never widen the capability past whatever repository that one
token already writes. Scope the token, a fine-grained GitHub PAT with contents: write, to the backup
repository alone.
The repository must be private. Kokua checks before it ever pushes and refuses to back up into a public
one. A backup copies config.toml, the memory store, saved documents, authored skills, and the
conversation transcripts, as a single git commit.
[compute]¶
Declared by the compute toolset: an agent's tools must list compute to get calculate,
execute_python, and run_command. The shipped [agents.coder] is the only agent that declares it.
| Key | Type | Default | Hot | Meaning |
|---|---|---|---|---|
command_env_passthrough |
string | "" |
no | comma-separated environment variable names a run_command child may see, on top of the allowlist |
run_command runs a command line through /bin/sh -c and returns its exit code with stdout and stderr
labelled separately. A nonzero exit is reported rather than treated as a failure, since pytest exits 1
with the answer on stdout. The timeout is per call, defaulting to 30 seconds and clamped to 600.
Combined stdout and stderr are capped at 20,000 bytes; past that, the larger of the two streams is
trimmed first, so a talkative command loses output before a quiet one does. run_command also takes a
cwd argument; left unset, it defaults to Kokua's own process working directory, which for a
launcher-started service is wherever the launcher happened to be, not anything under $KOKUA_HOME.
The child's environment is an allowlist, and this key is the opt-in half of it. A command sees
PATH, HOME, the locale and temp variables, and SHELL, TERM, TZ, USER, LOGNAME. Nothing
else, API keys included, which is what stops run_command("env") lifting a credential into the model's
context on a machine whose launcher sources a .env. That default also makes gh, ssh, and
git push over ssh fail, which is a policy rather than a bug, so naming SSH_AUTH_SOCK or GH_TOKEN
here is how you grant one back. Whitespace around a name is ignored and empty segments are dropped, so a
trailing comma is harmless. A name that is unset in Kokua's own environment produces no variable at all
rather than an empty one.
Not hot: the value is read when an agent is built, so a change needs a restart. run_command is in the
shipped [security].confirm_tools, so a command reaches you for approval before it runs, including one
a sub-agent asked for, since a worker's gated calls route to the parent's gate rather than running
unattended.
Neither execution tool is a sandbox. They buy a hard timeout, crash isolation, and no parent
environment variables. The process-group kill only fires on timeout or on interruption: a command that
returns normally is not waited on beyond its own exit, so run_command("./build.sh > log 2>&1 &") can
report exit 0 in milliseconds while the build keeps running untouched, its captured output going to a
temp directory already deleted by the time you read the answer. run_command also carries no memory
cap, unlike execute_python's 512 MB address-space limit: that limit would break compilers and test
suites, and applying one to a shell child needs preexec_fn, which is neither portable nor safe
alongside threads. Neither tool confines the filesystem, the network, or process signalling: a command
runs as you, reads what you can read, and can signal Kokua's own process. The approval gate is the
control; reach for a container if you need containment.
Toolset sections¶
[scheduling], [planning], and [capabilities] are not special cases in the config layer. A section
named after a toolset is how any toolset ships its settings, including a third party's:
- The section name is always the toolset's own name, so two toolsets cannot claim one section. A toolset named after a section Kokua's core already parses is refused at startup.
- Each key is one
Settingdeclared on the toolset, carrying its type, its default, and whether it is hot. Being flagged hot is what makes it hot; there is no second list. - Keys are read only when some agent declares the toolset, but they are validated whichever way, so a typo in a section for a toolset no agent holds still fails startup rather than waiting to surprise you.
- A section whose toolset is installed but which no agent declares produces a startup warning: its settings are read by nobody, which is otherwise silent until you notice a flag doing nothing. A section no installed toolset owns at all is the ordinary unknown-key error.
Writing one is covered in set up a toolset.
Environment variables¶
Kokua reads these directly; none of them has a config.toml key.
| Variable | Meaning |
|---|---|
KOKUA_HOME |
root for all state. Default ~/.kokua. Holds config.toml and data/, so it must resolve before the file is read, which is why it is not a setting |
KOKUA_CONFIG |
path to the config file, below --config and above the default location |
KOKUA_EMAIL_PASSWORD |
SMTP password for [email]. Never read from the file |
GITHUB_BACKUP_TOKEN |
push token for [github_backup]. Fixed here rather than a config key, so a repointed repo can't widen the capability past what the token already writes |
AIMU_LANGUAGE_MODEL |
the model to use when [assistant].model is unset, before the running-local-server search |
AIMU_IMAGE_MODEL |
required by the image toolset, which offers no tool at all without it. For example gemini:nano-banana, or a HuggingFace diffusers hf:<repo> |
AIMU_AUDIO_MODEL, AIMU_SPEECH_MODEL, AIMU_TRANSCRIPTION_MODEL |
required by the audio, speech, and transcription built-in groups respectively |
The generative model variables have no default on purpose: the tool raises if the variable is unset, rather than downloading weights you did not ask for. Accepted values and formats are in AIMU's env var reference.
Reading images needs no variable, only a vision-capable model. The assistant reads what you attach
through the web UI (upload or paste) or the CLI's /attach <path>. Uploaded and generated images are
stored under data/images and served by the web UI at /images; a conversation keeps only a small
reference, not inline data.
Command-line flags¶
Flags override the file for one run and are never written back.
| Flag | Overrides |
|---|---|
--config PATH |
which file is read |
--frontend NAME |
[frontend].name |
--model STRING |
[assistant].model |
--system TEXT |
the entry agent's opener, leaving a worker's alone |
--confirm-tools NAMES |
[security].confirm_tools, comma-separated; empty string disables |
--mcp URL |
adds a server for this run, unauthenticated or OAuth |
--host, --port |
[web].host, [web].port |
Introspection, which exits without running the assistant:
kokua --list-frontends # installed front ends
kokua --list-toolsets # every name a tools list may use, grouped by provider
See also¶
config.example.toml: the same keys, one line each, and whatkokua config initwrites.- Set up a toolset: declaring
toolsanddelegates_to, and writing a toolset of your own. - Add a skill, add an MCP service.
- Design principles: why the config layer is shaped this way,
particularly "
config.tomlis the single source of settings" and "a capability is declared, never defaulted". - Architecture: how the file is layered, parsed, and written.