Skip to content
titah

Reference

Config reference

Every top-level key in `titah.json`, what it accepts, and what happens when it is missing.

The schema is generated from src/core/schema.ts into config.schema.json — do not edit that file by hand. Point $schema at it for editor autocomplete:

{ "$schema": "./node_modules/titah-code/config.schema.json" }

Global config is ~/.config/titah/titah.json, merged with the project’s ./titah.json. The project file wins.

Top level

KeyTypeDefaultMeaning
$schemastring—Path to config.schema.json, for autocomplete
modelstringnoneDefault model as provider/model. No default is guessed — a picker is shown instead
smallModelstringunsetCheap model for side work, above all compaction summaries
defaultAgentstring"build"Which agent a session starts in
providerobject{}Provider definitions, keyed by name
agentobject{}Agent definitions, keyed by id
externalAgentobjectbuilt-insExternal CLI adapters
permissionobjectask-by-defaultPermission axes, argument rules, and the allowlist
commandobject{}Custom slash commands
keybindsobjectopencode defaultsKey overrides; "none" disables
skillsobjectclaude + opencodeSkill discovery, paths, always-loaded ids
compactionobjectsee belowContext compaction tuning
searchobject{ backend: "ddg" }websearch backend and key
diagnosticsobjectunsetThe project checker command
mcpobject{}MCP servers, stdio only
lspobject{}Language servers, per extension
accountobjectunsetTitah account server. The token never lives here
pluginobject{}Plugins, named one by one — never auto-discovered
panelobjectsee belowSide-panel geometry: widths, the history floor, the box floor
extensionobject{}Side-panel extensions, named one by one — never auto-discovered
hooksobjectemptyShell commands at tool.before / tool.after
sandboxobject{ bash: false, network: true }OS-level fence around bash
limitsobjectsee belowToken budgets, and automatic continuation
delegationenum"ask"When work is handed to sub-agents
instructionsstring | object | array[]Extra instruction files to read, and create if missing
scaffoldbooleantrueCreate declared instruction files, skill folders and a starter AGENTS.md
trackingobject{ enabled: true, exclude: [], git: true }What is reported to your account dashboard
artifactsobject{ enabled: true }Whether the artifact tool may publish pages
logLevelenum"INFO"DEBUG | INFO | WARN | ERROR

provider

"provider": {
  "<name>": {
    "npm": "@ai-sdk/openai-compatible",
    "options": { "baseURL": "https://…/v1", "apiKey": "${env:VAR}" },
    "models": {
      "<model-id>": { "name": "Human label", "contextWindow": 131072 }
    }
  }
}
KeyNotes
npm@ai-sdk/openai-compatible or @ai-sdk/anthropic
optionsPassed to the SDK provider. baseURL, apiKey, headers
models.<id>.nameWhat appears in /model and titah models
models.<id>.contextWindowRequired for automatic compaction. Never guessed
models.<id>.price{ input, output, cacheRead } per 1M tokens. Required for cost in titah stats. Never guessed
cacheControlSend cache_control in the request body. Only for OpenAI-compatible gateways that forward it to Anthropic — ask your gateway operator first. Default false

permission

"permission": {
  "edit": "ask", "write": "ask", "bash": "ask",
  "network": "ask", "delete": "ask", "mcp": "ask",
  "external_directory": "deny",
  "doom_loop": "ask",
  "allowlist": ["git *", "npm test"],
  "rules": { "bash(git *)": "allow", "bash(git push *)": "deny" }
}
KeyDefaultNotes
edit write bash network delete mcp"ask"The six harm classes
external_directory"deny"Paths outside the working directory. Opened per path through rules, never with a blanket allow
doom_loop"ask"A repeating call. It can only tighten — "allow" means “never interrupt me”
allowlist[]Shell command patterns always allowed
rules{}Argument-level rules, "<axis>(<pattern>)": policy

A rule-level deny is absolute; among ask and allow the most specific pattern wins, measured in non-wildcard characters rather than file order. Full rules in Permissions & undo.

agent

"agent": {
  "<id>": {
    "description": "Shown in /agents and to the coordinator",
    "prompt": "Extra system prompt for this agent",
    "model": "provider/model",
    "mode": "primary | subagent | all",
    "delegate": "<externalAgent name>",
    "tools": { "write": false },
    "permission": { "bash": "allow" },
    "skills": ["namespace:name"],
    "effort": "low | medium | high",
    "steps": 40
  }
}
KeyNotes
modeDefault "primary". Only "subagent"/"all" are dispatchable
delegateMutually exclusive with model. Config setting both is rejected at load
toolsAn exception list, not an allowlist. Unlisted tools stay enabled
permissionOverrides the global block. Unmentioned axes inherit
skillsFully-qualified ids only. A bare name never resolves
effortHow much closing analysis an answer ends with. Unset means the model decides. Ctrl+R cycles it live
stepsTool-calling iterations per turn. Default 40, or 200 once limits.turnTokens is set; the last runs with no tools

externalAgent

"externalAgent": {
  "<name>": {
    "command": "aider",
    "args": ["--message", "{prompt}", "--no-stream"],
    "resumeArgs": [],
    "format": "stream-json | json | text",
    "sessionMode": "generate | discover",
    "specialist": "deep architectural reasoning, cross-module refactors",
    "timeout": 600000,
    "enabled": true
  }
}
KeyDefaultNotes
args[]Arguments for the first call. {prompt} and {session} are substituted
resumeArgs[]Arguments for resuming. Empty means reuse args
format"stream-json"How to read the CLI’s output
sessionMode"discover"Who owns the session id — see Delegation
specialistunsetWhat this agent is best at, written for the model to read. /tim uses it to decide who gets which part, and skips a super agent that has none — naming who it skipped
timeout600000Milliseconds
enabledtruefalse removes it from /consensus and the @ popup

skills

"skills": {
  "discover": ["claude", "opencode"],
  "paths": ["~/.config/opencode/skills", { "path": "./.titah/skills", "as": "team" }],
  "always": ["superpowers:using-superpowers"],
  "learn": { "enabled": false, "minTools": 8, "max": 30 }
}

Set discover to [] in tests or anywhere that must not read ~/.claude and ~/.config/opencode.

learn keyDefaultMeaning
enabledfalseLet Titah write reusable skills after long turns. Costs one smallModel call per qualifying turn
minTools8Tool calls a turn needs before it is considered
max30Cap on learned skills; at the cap existing ones are still updated

Full behaviour in Skills → Learned skills.

compaction

"compaction": { "auto": true, "reserved": 8192, "tailTurns": 2, "prune": true }
KeyDefaultMeaning
autotrueAutomatic compaction on/off
reserved8192Headroom for the next answer plus the summary call. Capped at a quarter of the window
tailTurns2Recent turns kept verbatim
prunetrueDrop old tool output before summarising

Full behaviour in Context & compaction.

"search": { "backend": "ddg | brave | tavily", "apiKey": "${env:BRAVE_API_KEY}" }

ddg needs no key and scrapes HTML, so it can break without warning — titah doctor says so.

diagnostics

"diagnostics": { "command": "npm run typecheck" }

Never guessed. A non-zero exit is a finding, not a tool failure.

mcp and lsp

"mcp": { "github": { "command": "mcp-server-github", "args": [] } },
"lsp": {
  "ts": { "command": "typescript-language-server", "args": ["--stdio"], "extensions": [".ts", ".tsx"] }
}

MCP is stdio only, tools only, namespaced <server>_<tool>. See MCP & language servers.

keybinds

"keybinds": { "session_interrupt": "ctrl+g", "tool_details": "none" }

Defaults follow opencode. "none" disables an action. See The TUI.

command

"command": {
  "review": {
    "template": "Review the code in {{.Input}} and suggest improvements",
    "description": "Quick code review",
    "agent": "explore"
  }
}

{{.Input}} follows opencode; $ARGUMENTS is accepted too. Built-in commands cannot be overridden.

account

"account": { "server": "https://titah.dev" }

Only the server address belongs here. The token does not — it lives in ~/.local/share/titah/account.json at mode 0600, for the same reason as provider keys: the config is the file people paste into GitHub issues.

Resolution order is $TITAH_ACCOUNT_SERVER, then this key, then the built-in default https://titah.dev. See Account & sign-in.

limits

"limits": {
  "turnTokens": 400000,
  "sessionTokens": 2000000,
  "continueTurns": 5
}
KeyDefaultMeaning
turnTokens5× the model’s contextWindowToken budget for one turn, summed across steps. Setting it also lifts the step ceiling from 40 to 200
sessionTokensunsetBudget across every turn in a session. Checked before a turn starts, never mid-turn
continueTurns0How many times to continue automatically after a limit, while the plan still has unchecked - [ ] items

Full behaviour in Cost & limits.

delegation

"delegation": "ask"

"ask" (default), "auto", "always", or "never". "never" does not even send the roster to the model, so no tokens go on a list nobody uses. See Modes & custom agents.

hooks

"hooks": {
  "tool.before": [{ "match": "^bash$", "run": "scripts/guard.sh", "timeout": 30000 }],
  "tool.after":  [{ "match": "edit|write", "run": "npm run format" }]
}

match is a regex over the tool name; omit it to match every tool. timeout defaults to 30000 ms. tool.before refuses the call on a non-zero exit; tool.after cancels nothing but appends its stderr to the tool output. See Hooks.

sandbox

"sandbox": { "bash": false, "network": true }
KeyDefaultMeaning
bashfalseRun bash inside the OS sandbox. Refuses to run bash at all if no sandbox exists on this platform
networktrueAllow network from inside the sandbox. npm and git need it

See Sandbox.

plugin

"plugin": {
  "@acme/titah-audit": { "options": { "file": "audit.log" } },
  "./plugin/lock-migrations.mjs": {},
  "market:prettier": {}
}

Named one by one, never auto-discovered from node_modules — plugins run unsandboxed inside Titah’s process, and “installed” is not “trusted”. "enabled": false means the module is not imported at all. See Plugins.

panel and extension

"panel": {
  "floor": 40,
  "boxFloor": 2,
  "left":  { "width": 20 },
  "right": { "width": 20 }
},
"extension": {
  "@titah/extension-git":   { "side": "right" },
  "@titah/extension-tests": { "side": "right", "rows": 6 },
  "./panel/notes.mjs":      { "side": "left", "collapsed": true, "key": "n" }
}
panel keyTypeDefaultMeaning
floorinteger40Columns of history that must survive. Below it, side panels close themselves rather than squeeze the conversation
boxFloorinteger2Rows an open box needs. Below it, boxes fold themselves from the bottom up, and a notice says how many
left.width / right.widthinteger (min 8)20Starting width of that side, in columns
extension keyTypeMeaning
enabledboolean (true)false means the module is not imported at all — top-level code in it never runs
side"left" | "right"Overrides the side the extension asked for
widthinteger (min 8)Overrides that side’s width while this box is on it
rowsintegerRequested height. A request, not a guarantee: what is left after the other boxes is still the ceiling, and with no number the remaining height is split evenly
collapsedbooleanThe starting state. Folding with a key does not write back here — a config that changes itself because of a keypress stops reading as a statement of intent
optionsobjectPassed to the extension’s factory
keystringThe leader key this box answers to, resolved at install time when two extensions ask for the same one

A side is a stack: extensions listed for the same side are drawn as separate boxes, top to bottom in config order — the only order you can see and change. Extensions run unsandboxed in the TUI process, so like plugin they are named one by one and never auto-discovered. See Extensions.

instructions and scaffold

"instructions": "titah-instruction.md"
"instructions": { "path": "titah-instruction.md" }
"instructions": ["a.md", { "path": "b.md" }]

All three forms mean the same thing. The single form is accepted because it is what people write first, and the object form follows skills.paths, which already takes string | { path } — two axes pointing at files should be written the same way.

Declared files that do not exist are created on the first turn of a session, and a project with no instruction file at all gets a starter AGENTS.md.

"scaffold": false

turns all of that off — nothing is written that you did not ask for. See Project instructions.

tracking

"tracking": {
  "enabled": true,
  "exclude": ["~/clients/*"],
  "git": true,
  "sync": false
}
KeyDefaultMeaning
enabledtrueReport project metadata. Nothing is ever sent when you are not signed in, whatever this says
exclude[]Paths never reported. Same glob dialect as permission.allowlist; ~ is expanded first
gittrueInclude the remote URL and branch. A remote URL often names a client
syncfalseUpload transcripts. Needs this and the per-project toggle on the server. Prompts, answers and tool names only

Per-project opt-out needs nothing here — put {"tracking": {"enabled": false}} in that project’s ./titah.json. Full behaviour in Project tracking.

artifacts

"artifacts": {
  "enabled": true
}
KeyDefaultMeaning
enabledtrueAllow the artifact tool to publish pages to your dashboard. Nothing is ever published when you are not signed in, whatever this says

On by default, unlike tracking.sync, and the difference is not an inconsistency. sync uploads automatically, with nobody in the loop, so it has to be switched on deliberately. Publishing a page happens only when the model asks and the network permission dialog — naming the host, the title and the byte count — is approved. You are in the loop every single time, so a switch that started off would add a step without protecting anything.

Set it to false to remove the tool from the roster entirely. Full behaviour in Artifacts.

Checking your config

titah config path    # where the files are
titah config show    # the merged result, credentials redacted
titah doctor         # plus what resolved, what did not, and what is missing