Skip to content
titah

Capabilities

Tools

Every built-in tool, the permission axis it sits on, and the design decisions behind the ones that are easy to get wrong.

The full set

ToolPurposePermission
readRead a text file with line numbers, supports offset/limit—
listRecursive directory listing, skipping node_modules/.git/dist/…—
globFind files by pattern, newest first—
grepSearch contents by regex, results as file:line: text—
editExact text replace; must be unique; fails hard on no matchedit
writeWrite full file contents, creating parent directorieswrite
bashRun a shell command with a timeoutbash
skillLoad a skill’s full instructions on demand—
taskDispatch a sub-agent, one level deep—
planRecord a plan that survives compaction—
webfetchFetch a URL; HTML is stripped to readable textnetwork
websearchSearch the web; backend set by search.backendnetwork
patchSeveral edits to one file, all or nothingedit
moveMove or rename; never overwriteswrite
removeDelete a file, or a directory with recursivedelete
bash_startStart a long-running command in the backgroundbash
bash_outputRead what a background process printed since last look—
bash_stopStop a background process and its whole group—
diagnosticsRun diagnostics.command and return its outputbash
memoryRecord a durable project fact, recalled into every request—
questionAsk the user and wait for their answer—
exit_planOffer to leave Plan mode, and switch if accepted—
githubRun the gh CLI: PRs, issues, releases, runs, code searchnetwork when it only reads, bash when it changes something
artifactPublish a self-contained HTML page to your dashboard, and return its URLnetwork
(from MCP)Every tool an MCP server offers, as <server>_<tool>mcp

That is 24 built-in tools, plus everything your MCP servers contribute.

Two global rules

Filesystem access is confined to the session working directory. Paths that escape it are refused, by every tool, before anything happens.

Tool output larger than 32 KB is written to ~/.local/share/titah/tool-output/, and only the head plus a pointer enters the context. This is the same mechanism that keeps the sessions database small — see Storage & maintenance.

edit fails hard, on purpose

If oldString is missing, or appears more than once, the tool refuses and writes nothing.

Refusing is far better than silently writing in the wrong place. A replaceAll flag covers the case where a multi-replacement is what you actually meant.

patch is all or nothing

patch extends that promise to several edits at once: they apply in order, each seeing the previous one’s result, and any miss leaves the file byte-for-byte untouched.

A half-edited file is worse than an unedited one — it fails to compile in a way that looks like nobody’s fault.

move refuses to overwrite

That refusal is exactly what keeps move on the write axis. If it could overwrite, it would be able to destroy a file without ever passing through delete — which would make that axis a fence with a gate beside it.

Background processes

bash_start runs its command in its own process group, so bash_stop kills the whole tree. A dev server that spawns children is the normal case, and killing only the parent leaves the port held.

Output is kept in a ring buffer, newest kept, and the number of bytes dropped is reported rather than silently lost.

Background processes do not survive Titah exiting.

The web tools

webfetch and websearch sit on their own network permission axis rather than borrowing bash. They touch no files; what they risk is confidentiality — they are the only tools that send repository content off the machine, and none of the older axes says that. network: "deny" turns both off outright.

Private addresses are deliberately not blocked: checking your own dev server on localhost is one of the things a coding agent is for.

webfetch does refuse every scheme except http and https — file: in particular, since it would sidestep the working-directory confinement every filesystem tool relies on.

websearch defaults to the ddg backend, which needs no API key and scrapes HTML — so it can break without warning. titah doctor says so out loud. Set search.backend to brave or tavily with search.apiKey for something stable.

artifact

Publishes one self-contained HTML document to your account dashboard and returns a URL. Use it when the answer is a page rather than a paragraph — a report, a chart, a table worth sorting.

It sits on network for the same reason the web tools do: it sends a document off the machine. It does not set mutates, because nothing in the working directory changes, and marking it so would snapshot the whole repository on every publish to enable an undo of nothing.

Two things the model is told, because they change how it uses the tool. Pages are private, and the CLI cannot make one public — that is a person’s job, in a browser. And passing back the slug from a previous call revises the same page instead of publishing a second one, so the URL you were given stays the URL that works.

Full treatment, including what a published page can and cannot do once it is open in somebody’s browser, is in Artifacts.

diagnostics

diagnostics runs only the command you declare in diagnostics.command. It never guesses one: a wrongly guessed checker fails in ways that read far worse than “not configured”.

A non-zero exit is a finding, not a tool failure — otherwise “three type errors” would look exactly like “the checker is broken”.

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

Language servers give the same information automatically after every write — see MCP & language servers.

memory and plan

These two differ on exactly one axis: how long the fact stays true.

  • plan is intent for the work in progress, and dies with the session.
  • memory is a fact about the project that is still true tomorrow, in a different session.

Both are stored outside model_message, so compaction cannot reach either.

Memory is recalled eagerly — the whole store, every request — rather than retrieved. With a 32-fact cap that is cheaper than a retrieval step that can pick wrong, and when retrieval picks wrong the missing fact leaves no trace.

question

question is the only tool that stops and waits for a human.

With no client connected it does not hang: it returns “nobody answered, continue with your best assumption and say what you assumed”, which is what headless runs and CI need.

github

github wraps the gh CLI — GitHub’s own — rather than calling the API directly. Two reasons, and both are about not owning something Titah has no business owning:

  • Credentials. Calling the API means asking for, storing and refreshing a GitHub token: one more secret on disk, one more OAuth flow, one more place to leak. gh already has all of it, and gh auth status is an answer you can check yourself.
  • Scope. GitHub has hundreds of endpoints. Wrapping gh means every command you can type is a command the model can use, with no list to chase each time GitHub adds something.

Arguments are passed as an array, without the leading gh, straight to the process — never through a shell, so quoting and globbing do not apply.

Why not just tell the model to use bash

Because bash knows nothing about what it is running. This tool does, and the difference is the permission axis:

CallAxisWhy
gh pr list, gh issue view 7, gh run viewnetworkReading from GitHub is network traffic and nothing more
gh pr merge, gh issue close, gh release createbashIt acts on GitHub as you, where /undo cannot reach

The read-only set is chosen per <object> <action> pair, not per first word: gh pr view reads, gh pr merge does not. Anything not recognised is treated as a change — guessing wrong in that direction only raises a dialog, while guessing wrong the other way runs something nobody asked for.

Five commands are refused outright, whatever the permission says: auth token (prints the token in plain text), auth login / auth logout / auth refresh (interactive, or far beyond this repo), and repo delete (no undo exists for it).

github is not snapshotted — it touches no files, so a snapshot would help nothing, and a merged PR is not something titah undo can take back.

exit_plan

Plan mode refuses every file tool. exit_plan is how the model offers to leave it: when you ask for a change while planning, it calls this tool with the plan it would carry out, and you get a choice that actually switches mode.

Refusing an edit and then reporting the refusal — or quietly writing a plan instead of doing what was asked — are both worse than saying which mode you are in and offering to change it. See Modes & custom agents.

Sandboxing bash

The permission axes guard at the tool layer. Once a bash command is allowed it runs with your full rights — rm -rf ~, curl | sh, writes to ~/.ssh — and the delete axis governs the remove tool, which bash never passes through.

sandbox.bash closes that at the process layer instead. It is off by default, and it fails closed. See Sandbox.

MCP tools

Every tool an MCP server offers appears as <server>_<tool> and sits on the mcp permission axis. See MCP & language servers.