The full set
| Tool | Purpose | Permission |
|---|---|---|
read | Read a text file with line numbers, supports offset/limit | — |
list | Recursive directory listing, skipping node_modules/.git/dist/… | — |
glob | Find files by pattern, newest first | — |
grep | Search contents by regex, results as file:line: text | — |
edit | Exact text replace; must be unique; fails hard on no match | edit |
write | Write full file contents, creating parent directories | write |
bash | Run a shell command with a timeout | bash |
skill | Load a skill’s full instructions on demand | — |
task | Dispatch a sub-agent, one level deep | — |
plan | Record a plan that survives compaction | — |
webfetch | Fetch a URL; HTML is stripped to readable text | network |
websearch | Search the web; backend set by search.backend | network |
patch | Several edits to one file, all or nothing | edit |
move | Move or rename; never overwrites | write |
remove | Delete a file, or a directory with recursive | delete |
bash_start | Start a long-running command in the background | bash |
bash_output | Read what a background process printed since last look | — |
bash_stop | Stop a background process and its whole group | — |
diagnostics | Run diagnostics.command and return its output | bash |
memory | Record a durable project fact, recalled into every request | — |
question | Ask the user and wait for their answer | — |
exit_plan | Offer to leave Plan mode, and switch if accepted | — |
github | Run the gh CLI: PRs, issues, releases, runs, code search | network when it only reads, bash when it changes something |
artifact | Publish a self-contained HTML page to your dashboard, and return its URL | network |
| (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.
planis intent for the work in progress, and dies with the session.memoryis 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.
ghalready has all of it, andgh auth statusis an answer you can check yourself. - Scope. GitHub has hundreds of endpoints. Wrapping
ghmeans 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:
| Call | Axis | Why |
|---|---|---|
gh pr list, gh issue view 7, gh run view | network | Reading from GitHub is network traffic and nothing more |
gh pr merge, gh issue close, gh release create | bash | It 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.