titah doctor --probe
Almost every problem below is visible in that output before you go looking for it.
“The TUI will not start”
Cause: you are running from source without a build. Node runs .ts directly
but not .tsx — JSX is not stripped.
npm run build
node dist/cli.js
Headless commands (run, serve, doctor) do run from source.
“Node version” errors
Titah needs Node ≥ 22.6 for node:sqlite.
node --version
“The model answers confidently about things I never said”
Cause: the context overflowed and the provider truncated it silently. This is the single most common cause of an agent that seems to have lost its mind.
Fix: declare contextWindow for the model.
"provider": { "ollama": { "models": { "qwen3:14b": { "contextWindow": 32768 } } } }
titah doctor lists every model missing one. ollama in particular truncates at
num_ctx — 4096 by default — without a warning. See
Context & compaction.
“Compaction fires on every single turn”
Cause: reserved is large relative to the window. On an 8k model the 8192
default would equal the whole window.
That is why reserved is capped at a quarter of the window. If it still
feels aggressive, lower it explicitly:
"compaction": { "reserved": 1024 }
titah doctor reports every model where the cap is biting and the value actually
in effect.
“Nothing happens in CI / the agent hangs and then does nothing”
Cause: with no client connected, any tool needing a permission dialog is auto-denied. That is deliberate — blocking would hang CI, auto-allowing would be a silent security hole.
Fix: be explicit.
titah run --auto "…"
{ "permission": { "bash": "allow", "allowlist": ["npm test", "git *"] } }
“edit keeps failing”
edit fails hard when oldString is missing or appears more than once, and
writes nothing.
That is the feature. Give it more surrounding context so the match is unique, or
use replaceAll if a multi-replacement is genuinely what you want. See
Tools.
“@claude says the agent is unavailable”
The CLI is not on PATH. Titah detects external agents with which at startup
and lists missing ones with the command that was not found rather than
hiding them.
which claude opencode
titah doctor
“The second @claude starts a fresh conversation”
Check sessionMode for that adapter. generate means Titah owns the UUID
(Claude Code’s way); discover means the id is read from the first call’s
output (opencode’s way). The wrong one produces exactly this symptom.
Two things that are easy to get wrong: Claude Code rejects
--output-format stream-json without --verbose, and passing the same
--session-id twice is not how you resume.
“A skill in always does nothing”
An unresolved id is skipped silently at the point it happens — but /skills
and titah doctor list every unresolved id together with where it was
configured.
The usual cause is a bare name. Ids are always namespace:name;
"project-analyzer" never resolves, "team:project-analyzer" does. See
Skills.
“My opencode plugin does not load”
opencode plugins are JavaScript modules against opencode’s runtime API, not markdown. Titah reads opencode skills and cannot run its plugins. This is a supported-scope limit, not a bug.
“websearch returns nothing, or nonsense”
The default ddg backend scrapes HTML and can break without warning — titah doctor says so out loud. Move to a stable backend:
"search": { "backend": "brave", "apiKey": "${env:BRAVE_API_KEY}" }
“webfetch refuses a URL”
It accepts only http and https. file: in particular is refused, because it
would sidestep the working-directory confinement every filesystem tool relies on.
Private addresses are not blocked — localhost works on purpose.
“undo reverted more than I expected”
You almost certainly ran it after a /tim turn whose coordinator wrote nothing
itself. Undo walks back to the most recent message that actually took a
snapshot, which can be an earlier turn, and reverts that too.
Read the worked example in Permissions & undo — there is no
way to undo a /tim turn in isolation when its coordinator wrote nothing.
“A /tim sub-agent ran a shell command in Plan mode”
That was a real hole and it is fixed. A sub-agent’s permission is now derived
from its parent and clamped by the stricter of the two on every axis, so a
plan coordinator whose bash is "deny" cannot dispatch a sub-agent that
runs one — even if that agent’s own block says "bash": "allow".
If you still see it, you are on a build from before that change. Check
titah --version against the changelog, and see
Sub-agents for how the ceiling is computed.
“bash is refused and the reason mentions a sandbox”
You set sandbox.bash: true on a platform with no sandbox available. It fails
closed on purpose: someone who enabled it believes there is a fence, and
running unfenced anyway would leave them believing it.
titah doctor # says which of the three states you are in
Seatbelt on macOS, bubblewrap on Linux. Turn it off, or install the missing one. See Sandbox.
“A background turn did nothing but get denied”
titah run --bg detaches, so no client is attached — and with no client, any
tool needing a dialog is auto-denied. A detached turn that will touch files needs
--auto or an explicit allowlist:
titah run --bg --auto "…"
See Background turns.
“JSON.parse fails on --output-format json”
Nothing human is supposed to reach stdout in json modes, so this should not
happen — if it does, check that you are not merging stderr into stdout (2>&1).
Progress, notices and errors all go to stderr on purpose.
An exit code of 2 is not a parse problem: it means the turn succeeded and the
answer did not match your --json-schema. Retrying will not help; the prompt or
the schema has to change. See Structured output.
“titah stats shows tokens but no cost”
The model has no price declared. Titah ships no price table and never will —
prices change by region and contract, and a stale table produces numbers that
look official and are wrong.
"provider": { "9router": { "models": { "ant": { "price": { "input": 3, "output": 15 } } } } }
Per 1M tokens. Unpriced models are named separately rather than counted as free. See Cost & limits.
“The turn stopped before it finished”
It hit a limit, and it said so — check the notice. limits.turnTokens defaults
to 5× the model’s declared contextWindow, and the step ceiling is 40 until you
declare a token budget, at which point it becomes 200.
Raise the budget, split the work, or set limits.continueTurns to let a bounded
turn be followed by another while the plan still has unchecked items. See
Cost & limits.
“A hook is not firing”
Its match regex is probably invalid, which looks exactly like a pattern that
matched nothing.
titah hooks list # names invalid regexes explicitly
See Hooks.
“Editing my plugin file changes nothing”
Node caches modules by URL, so a plugin edit takes effect on the next Titah
start, not mid-session. titah plugin list actually loads them and reports
what each provides — reading the config alone cannot tell a working plugin from
one that fails to import. See Plugins.
“A side panel is missing, or the side says ⚠ N failed”
⚠ N failed is not No extension. The first means the extensions are
configured and were refused; the second means nothing is configured for that
side. The footer carries the full sentence — ⚠ N side panels failed to load — run: titah extension list — for as long as the state holds, because the notice
at load time is gone in four seconds.
Two commands answer it, and they see different things:
titah doctor # the manifest and engines.titah, WITHOUT running any of it
titah extension list # loaded for real — catches failures at import too
doctor never imports third-party code, so it cannot see a factory that throws
or one that returns no render. That is what extension list is for.
A refused extension prints the reason:
✗ @titah/extension-git
@titah/extension-git needs extension API ^9.0.0, but Titah 0.8.0 provides 0.5.0.
Both numbers are named on purpose — provides 0.5.0 on a 0.8.0 install reads
like breakage otherwise. engines.titah is checked against the extension API
version, which only moves when the contract itself changes, so a Titah minor
bump no longer kills every installed extension. When a range genuinely does not
match, the fix is the package, not titah upgrade:
titah extension update # picks the newest published version this API accepts
See Extensions.
“A side panel folded itself”
Below panel.boxFloor rows per open box, boxes fold from the bottom up and a
notice says how many. Config order is priority order, so the box you put at the
top survives longest. A floor-folded box cannot be reopened with Ctrl+X Z —
the same arithmetic would fold it again on the next render, so Titah says so
instead of offering a key that appears not to work. Give the terminal more rows,
or drop a box from that side.
“Cannot reach the account server”
The default is https://titah.dev. If you run your own titah-web, point
at it:
export TITAH_ACCOUNT_SERVER=https://titah.internal
or { "account": { "server": "…" } } in the config — the environment wins. A
token records which server issued it, so changing servers asks you to sign in
again rather than reusing one that means nothing there. See
Account & sign-in.
“The database is enormous”
It should not be — blobs go to disk, not into SQLite. Sweep everything at once:
titah sessions prune --older-than 30d
That deletes rows, orphaned tool-output/ blobs and orphaned snapshots
together. Deleting rows alone just moves the growth into the directory next door.
“I cannot select text in the terminal”
While Titah tracks the mouse, the terminal stops using clicks to highlight text.
Ctrl+X M turns tracking off; the footer says ✂ mouse off until you turn it
back on. Most terminals also let you hold Shift while dragging.
“Shift+Enter does not insert a newline”
Most terminals cannot distinguish Shift+Enter from Enter. Use Ctrl+J.
Still stuck
Open an issue with the full output of titah doctor --probe:
github.com/titah-dev/titah/issues.