Never put a key in the config as plaintext
The config file is the thing people paste into issues, commit by accident, and
sync between machines. Titah gives you two places to put a key instead, and
titah config show redacts credentials in its output.
Path 1 — auth.json
titah auth set 9router # the key is read from stdin
titah auth list # which providers have a key, never the key itself
titah auth remove 9router
The file is ~/.local/share/titah/auth.json, created with mode 0600, kept
separate from the config on purpose — it is state, not something you commit,
so it sits with the sessions database rather than beside titah.json.
titah config path prints its exact location. Reading the key from stdin rather than
an argument keeps it out of your shell history and out of ps.
Path 2 — an environment variable, referenced from the config
{
"provider": {
"9router": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "https://router.example/v1",
"apiKey": "${env:ROUTER_API_KEY}"
}
}
}
}
The ${env:VAR} form is expanded when the config is loaded. The config can be
committed; the variable is set wherever the process actually runs.
Resolution order
For a given provider, in this order:
provider.<name>.options.apiKeyin the config (including an expanded${env:…})auth.json- A conventional environment variable —
ANTHROPIC_API_KEY,OPENAI_API_KEY, orTITAH_<PROVIDER>_API_KEY
The first one that resolves wins. titah doctor prints which source each
provider’s key came from, without printing the key.
An unset variable is not a fatal error
An unset ${env:...} does not fail config loading. The key is dropped, the
fact is recorded, and it is only complained about if the model you actually use
needs it.
This matters because a config that names five providers while you use one is
completely ordinary — on a laptop with only OPENAI_API_KEY set, a config that
also mentions Anthropic and a router should still start.
titah doctor # lists every key that did not resolve, and where it was expected
Titah never forwards your keys
When Titah spawns an external agent CLI — claude, opencode, anything in
externalAgent — that subprocess runs with its own credentials, the ones
you configured for it. Titah does not inject its provider keys into the child
environment.
The same applies in reverse: an external agent’s cost is reported separately in the footer and never summed with Titah’s own, because mixing them makes the number a lie. See Delegation.
Local providers need no key at all
{
"model": "ollama/qwen3.5:27b",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"options": { "baseURL": "http://localhost:11434/v1" },
"models": { "qwen3.5:27b": { "name": "Qwen3.5 27B", "contextWindow": 32768 } }
}
}
}
titah init probes Ollama and LM Studio on their conventional ports precisely
so this block does not have to be written by hand.
!!! warning “ollama truncates silently”
ollama truncates at num_ctx — 4096 by default — without a single warning,
and then the model answers confidently about the part of the conversation it
can no longer see. Declare contextWindow for every local model and read
Context & compaction.