Skip to content
titah

Getting started

Configuration

Where config lives, how the two files merge, and the blocks you will actually edit.

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"]
    }
  }
}

See MCP & language servers.

{
  "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.