The two files
Global config lives at ~/.config/titah/titah.json, merged with the project’s
./titah.json. The project file wins on any key both define.
The format is JSONC — comments and trailing commas are fine. Point
$schema at the schema and your editor will autocomplete every key. Titah keeps
a copy at ~/.config/titah/config.schema.json, refreshed whenever it differs
from the version you are running, and titah init writes the path for you:
{
"$schema": "/Users/you/.config/titah/config.schema.json",
"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" } }
}
}
}
An absolute path, because editors do not expand ~ — and the same line then
works if you paste it into a project’s titah.json. A config written by an older
titah init pointed into the npm install directory, which moves whenever Node or
the npm prefix changes; Titah moves that $schema to the copy on its next run,
leaving comments and everything else as they are. A URL, a file with another
name, or a project titah.json is never touched. titah doctor shows where
$schema points under Config.
titah config path # where the files are
titah config show # the merged result, credentials redacted
Providers
Any provider that speaks OpenAI-compatible is a first-class citizen —
self-hosted endpoints, routers, Ollama, LM Studio. Anthropic is available
through "npm": "@ai-sdk/anthropic".
{
"provider": {
"9router": {
"npm": "@ai-sdk/openai-compatible",
"options": { "baseURL": "https://router.example/v1" },
"models": {
"gapis": { "name": "gemini-3-flash (via gapis)", "contextWindow": 1048576 },
"ocode": { "name": "nemotron-3-ultra (via ocode)", "contextWindow": 131072 }
}
},
"anthropic": {
"npm": "@ai-sdk/anthropic",
"models": { "claude-sonnet-5": { "name": "Claude Sonnet 5", "contextWindow": 200000 } }
}
},
"model": "9router/gapis",
"smallModel": "9router/ocode"
}
A model is always addressed as provider/model.
smallModel is used for cheap side work — summarisation during compaction above
all. When it is unset, the turn’s own model does that work instead.
!!! warning “Declare contextWindow or lose automatic compaction”
Nothing is guessed. A model with no declared contextWindow has automatic
compaction off, because a wrong number is more dangerous than no number.
titah doctor lists every model missing one. See
Context & compaction.
Credentials
Do not put API keys in the config as plaintext. There are two supported paths —
auth.json and ${env:VAR} — both covered in
Credentials & providers.
Permissions
{
"permission": {
"edit": "ask",
"write": "ask",
"bash": "ask",
"network": "ask",
"allowlist": ["git *", "npm test"]
}
}
Each axis takes "ask", "allow" or "deny". The allowlist matches shell
command patterns for bash. Full rules in
Permissions & undo.
Agents
{
"defaultAgent": "build",
"agent": {
"explore": {
"description": "Codebase explorer — read only",
"prompt": "Always verify with tools, never guess. Answer very briefly.",
"tools": { "write": false, "edit": false, "bash": false },
"steps": 5
},
"qc": {
"description": "Run tests and report",
"model": "9router/gapis",
"mode": "subagent",
"permission": { "bash": "allow", "edit": "ask" },
"skills": ["team:project-analyzer"]
}
}
}
See Modes & custom agents and Sub-agents.
External agents
{
"externalAgent": {
"claude": { "enabled": true },
"opencode": { "enabled": true },
"aider": {
"command": "aider",
"args": ["--message", "{prompt}", "--no-stream"],
"format": "text",
"timeout": 600000
}
}
}
Adding a third agent is one config block, no code. See Delegation.
Skills
{
"skills": {
"discover": ["claude", "opencode"],
"paths": ["~/.config/opencode/skills", { "path": "./.titah/skills", "as": "team" }],
"always": ["superpowers:using-superpowers"]
}
}
See Skills.
Custom commands
{
"command": {
"review": {
"template": "Review the code in {{.Input}} and suggest improvements",
"description": "Quick code review",
"agent": "explore"
}
}
}
See Commands & palette.
Keybindings
{
"keybinds": {
"session_interrupt": "ctrl+g",
"tool_details": "none"
}
}
"none" disables an action. Defaults follow opencode — see
The TUI.
MCP and language servers
{
"mcp": { "github": { "command": "mcp-server-github", "args": [] } },
"lsp": {
"ts": {
"command": "typescript-language-server",
"args": ["--stdio"],
"extensions": [".ts", ".tsx"]
}
}
}
Compaction and search
{
"compaction": { "auto": true, "reserved": 8192, "tailTurns": 2, "prune": true },
"search": { "backend": "brave", "apiKey": "${env:BRAVE_API_KEY}" },
"diagnostics": { "command": "npm run typecheck" }
}
diagnostics.command is never guessed. A wrongly guessed checker fails in ways
that read far worse than “not configured”.
Budgets and continuation
{
"limits": {
"turnTokens": 400000, // defaults to 5× the model's contextWindow
"sessionTokens": 2000000,
"continueTurns": 0
}
}
A step is a unit of conversation, not of work, so the real bound is tokens.
Declaring turnTokens also lifts the step ceiling from 40 to 200. See
Cost & limits.
Prices
{
"provider": {
"9router": {
"models": { "ant": { "price": { "input": 3, "output": 15 } } }
}
}
}
Per 1M tokens, and declared rather than guessed — the same rule as
contextWindow. Without a price, titah stats still counts the tokens and names
the model separately instead of counting it as free.
Sandbox, hooks, plugins
{
"sandbox": { "bash": true, "network": true },
"hooks": {
"tool.after": [{ "match": "edit|write", "run": "npm run format" }]
},
"plugin": { "./plugin/lock-migrations.mjs": {} }
}
sandbox fences bash at the process layer and is off by default;
hooks are shell commands at the same hook points plugins use; plugin names
JavaScript modules one by one, never auto-discovered. See
Sandbox, Hooks, Plugins.
Tracking
{ "tracking": { "enabled": true, "exclude": ["~/clients/*"], "git": true } }
Metadata about each folder, reported to your dashboard once you are signed in —
and nothing at all when you are not. Four ways to switch it off, including a
./titah.json in the one project you would rather keep quiet. See
Project tracking.
Account
{ "account": { "server": "https://titah.dev" } }
Only the server address. The token lives in account.json at mode 0600, never
in a config file people paste into issues. See
Account & sign-in.
Delegation and instructions
{
"delegation": "ask",
"instructions": ["AGENTS.md", { "path": "docs/house-style.md" }],
"scaffold": true
}
delegation decides when work is handed to sub-agents; instructions names
extra files to read (and create if missing); scaffold: false stops anything
being written that you did not ask for.
A complete key-by-key listing is in the config reference.