Skip to content
titah

Getting started

Credentials & providers

Two supported ways to hold an API key, the exact resolution order, and why an unset variable does not break config loading.

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:

  1. provider.<name>.options.apiKey in the config (including an expanded ${env:…})
  2. auth.json
  3. A conventional environment variable — ANTHROPIC_API_KEY, OPENAI_API_KEY, or TITAH_<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.