Tools
Built-in tools
Section titled “Built-in tools”Always registered:
| Tool | Description |
|---|---|
bash | Execute shell commands (streamed, timeout, truncation + spill file). Persists cwd and exported env between calls in a session |
bash_status | Check a background bash job’s status and output |
bash_wait | Block until a background bash job finishes and return its result |
bash_cancel | Cancel a running background bash job |
read | Read text/image files with offset/limit |
write | Create or overwrite files (atomic: temp file + rename) |
edit | Exact-text replacement (single match enforced, atomic write) |
multiedit | Atomic batch of edits to a single file |
apply_patch | Apply multi-file unified diffs |
grep | Search file content (prefers rg if installed) |
find | Search files by glob (prefers fd if installed) |
ls | List directory contents |
fetch_content | Fetch a URL and extract readable markdown |
memory | Search/read/update persistent cross-session notes |
subagent | Spawn a child agent (sync or async) |
subagent_status | Poll async subagent jobs |
subagent_wait | Block until an async subagent job finishes and return its result |
subagent_cancel | Cancel a running async subagent |
subagent_steer | Send instructions or a correction to a subagent that is still running |
tasks | Track implementation tasks |
verify | Run the project’s verification checks |
load_skill | Load a skill by name. Skills are discovered per call, so one written mid-session is loadable. Most skills return their content; context: fork runs an isolated subagent instead |
moa_docs | Read moa’s own documentation (this page included), embedded in the binary |
Conditionally registered:
| Tool | Condition |
|---|---|
web_search | brave_api_key is configured |
ask_user | The web UI is active (not headless) |
In a moa serve session the agent also gets send_file, which publishes a file
to the conversation’s Artifacts as well
as showing its download card in the chat. Custom
script tools and MCP
tools are registered on top of all of this.
Tool selection guidance
Section titled “Tool selection guidance”- Use
grep,find,lsfor exploration - Use
readbefore editing —editwarns if the file wasn’t read first - Use
editfor surgical changes,multieditfor several changes in one file - Use
apply_patchfor coordinated changes across multiple files - Use
writefor new files or complete rewrites - Use
bashwhen you need actual shell behavior
Self-documentation
Section titled “Self-documentation”moa_docs returns the pages of this documentation set from inside the binary.
Installing moa gets you a single static binary, with no copy of the repository
and nothing next to it on disk. Without this the agent could only answer
questions about moa from whatever it happened to remember — which is how
confident, wrong answers about flags and config files get produced. So a user
who is working on their own project can ask moa to write them a
.moa/verify.json, or explain how to trigger a run from a webhook, and get an
answer from the real documentation without cloning anything or opening a
browser.
Because the pages are embedded at build time, they always describe the exact version being run: updating the binary updates its documentation with it.
The page names are listed in the tool description, which is the only cost this carries in the system prompt — page content is read on demand, never preloaded.
moa_docs(page: "configuration") # config.json fields and precedencemoa_docs(page: "automation") # HTTP API for triggering runsmoa_docs(page: "recipes/linear") # worked end-to-end integrationMemory
Section titled “Memory”memory stores single-fact notes that survive across sessions. Each fact
declares its scope: global facts are visible in every project, project
facts are scoped to the current repository —
every git worktree of one repo reads and writes the same project facts, and
removing a worktree no longer strands what was learned in it. Files written
before 0.26 carry the older four-value type (user/feedback → global,
project/reference → project); they are still read, and a rewrite records
scope while keeping a compatible type.
Only an index — one line per fact — is injected into the prompt; full bodies are read on demand. The index has a byte budget, and each scope gets a reserved share of it with the leftover rolling over in both directions. Without that reservation the more numerous project facts crowd out the global ones entirely, which is the wrong trade: a small cross-project fact should not disappear only because one repository has many project facts.
Memory is a last resort for a small, non-secret fact that is costly to
reconstruct and has no discoverable canonical source. A short pointer to an
external canonical source can be appropriate; copying that source into memory
is not. Memory is not a task tracker, scratchpad, instruction file, workflow
library, or secret store. Every write must declare either
invalidate_when, a single-line natural-language condition under which the
fact stops being true, or durable: true for a genuinely permanent fact. The
condition must be independently checkable now against a concrete source, not a
judgment about relevance: for example, “when issue #84 is closed”, “when git log shows branch X is merged”, or “when port 3306 on that host responds
again”. “When it is no longer relevant” is not a valid condition. Use
durable: true only for an otherwise eligible fact with no identifiable
invalidating event. Lifecycle is required, but it does not make rules,
preferences, procedures, task state, secrets, or copied project knowledge
eligible. If an identifiable event could make a fact false, use
invalidate_when instead of durable.
When reading a fact with an invalidation condition, delete it if you can verify
that the condition has occurred.
The write parameters are name, description, scope, content, and exactly
one of invalidate_when or durable. The description is a one-line hook of at
most 180 bytes — it is paid for on every turn, so the detail belongs in
content. invalidate_when is stored with the
fact and is shown by memory read; it is intentionally omitted from the
always-in-context list index. A successful write reports how much of the
index budget is in use, and how many facts overflow it and therefore never
reach the prompt.
memory search finds facts by text across names, descriptions and bodies in
both scopes, returning ids and a short snippet around each match rather than
whole bodies. It takes query, an optional regex flag (Go/RE2), limit
(default 10, max 25) and offset.
Other memory rules:
- Anything with a discoverable canonical source does not belong here. Read or reference AGENTS.md, skills, code, tests, configuration, documentation, scripts, tool definitions, issues, or other versioned artifacts instead of restating them.
- Standing rules, preferences, conventions and prohibitions belong in
AGENTS.md. Reusable workflows belong in skills; executable procedures belong in scripts or registered tools. - Credentials, tokens, keys, private signed URLs and other secrets belong in protected configuration, environment variables or a secret manager, never in memory.
- Prefer updating an existing fact over adding a near-duplicate, and delete facts that have become wrong, expired, or gained a canonical source.
- Current task state, progress notes and handoffs are not memories. Use the task tracker, or the session checkpoint only for active, non-reconstructible pre-compaction state.
Project facts written before 0.25 lived under ~/.config/moa/projects/<hash>/,
keyed by the path of the working directory. moa copies them into the
repository-keyed store the first time it opens each project, and leaves the old
files alone so an older binary still finds them. A store whose directory no
longer exists cannot be matched to a repository — git cannot say which repo a
deleted worktree belonged to — so it is listed once in
~/.config/moa/codebases/orphaned-memory.json with its path and fact count
instead of being guessed at. Starting a session in that directory again, if it
comes back, migrates it.
Bash: persistent state & background jobs
Section titled “Bash: persistent state & background jobs”bash persists working directory and exported environment between calls within
a session: a cd or export in one call is visible in the next (an EXIT trap
captures pwd and env -0 after each command). A few variables are never
persisted (PWD, OLDPWD, SHLVL, _, BASH_ENV, ENV, and exported bash
functions) because a real interactive shell regenerates them. Subagents get an
isolated copy seeded from their parent (subshell semantics: a child’s cd/env
changes never propagate back).
Set async: true to launch long-running work in the background and get a job
ID: block on bash_wait when you need the result, peek with bash_status, or
stop it with bash_cancel. Background jobs do not persist cwd/env
changes. A synchronous call can’t be promoted after launch — cancel and
relaunch with async: true.
Sandbox
Section titled “Sandbox”Path-based tools are sandboxed to the workspace directory by default. Escape attempts via .. or symlinks are blocked.
Override with:
-yoloflagpath_scope: "unrestricted"in configallowed_pathsfor specific extra directories/path add <dir>at runtime in the web UI
Dangerous-command confirmation
Section titled “Dangerous-command confirmation”As a heuristic mitigation against prompt injection, bash commands that
download and immediately execute remote code (the curl … | sh shape, and its
bash <(curl …) / sh -c "$(curl …)" variants) always require explicit user
confirmation, even in permissive modes. This is not a sandbox — it only forces
a prompt — but it stops smuggled remote code from running unattended.
Subagents
Section titled “Subagents”subagent(task: "...", model?: "...", thinking?: "...", tools?: [...], async?: bool)Async flow: call with async: true → get a job ID → block on subagent_wait (preferred) or poll with subagent_status → optionally subagent_cancel.
Live sub-conversations
Section titled “Live sub-conversations”A subagent is a full agent with its own streaming conversation, not just a black box that returns text. While one runs, its activity (thinking, tool calls, output) streams to the UI as it happens:
- Web: an agent tray appears above the input bar showing how many agents
are working. Drag it up (or tap) to expand the list, then tap an agent to
open its sub-conversation — rendered exactly like the main chat, updating
live. A back arrow (or
Ctrl+G) returns to the parent conversation. Async agents can be cancelled from the tray. The tray only lists live agents; finished ones drop off. At completion the parent timeline receives exactly one terminal outcome card keyed to the job: a completed child has Result (an explicitly bounded excerpt is labelled as such), a failed child has Error, and a cancelled child has no result. Conversation always opens the full child transcript. This card is independent of whether the parent model got the text through the normal async notification or throughsubagent_wait. The parent agent still receives the subagent’s final text as the tool result, so its own context is unchanged — the streaming view is purely for the user. When a child fails and its transcript can be reopened, its failure message preserves the underlying error and any partial output, and tells the parent it can continue the saved job withresume: "<job-id>"rather than starting over.
A resumed subagent keeps the model and thinking level it already ran under, so
continuing a sol or fable child does not silently switch it to the parent’s
current model. Passing model or thinking explicitly always wins, both on a
fresh subagent and on a resume. Transcripts saved before those fields were
recorded — or pointing at a model that no longer exists — fall back to the
parent’s model/thinking instead of failing the resume.
Guardrails
Section titled “Guardrails”Child agents run with their own, independent limits (they do not inherit
the parent’s numbers, and have no budget/$ cap of their own):
| Limit | Default | Config key (config.json) |
|---|---|---|
| Max turns | 100 | subagent_max_turns |
| Max run duration | 10m | subagent_max_run_duration (Go duration, e.g. "15m") |
| Max concurrent async jobs | 5 | subagent_max_concurrent_async |
Context compaction is enabled for children, with the same threshold as the main
session, so a long-running child won’t fail by exhausting its turn budget. What
a child never gets is the pre-compaction step (compact_strategy): it has
neither memory nor the ephemeral checkpoint to write to, so a warning could
only produce stray files, and its findings already travel back in its report.
Children cannot spawn their own subagents, use memory, call ask_user, or
use checkpoint. A child started by a forked skill has that same denylist.
Cost & persistence
Section titled “Cost & persistence”subagent_status reports a running/finished job’s token usage and cost
(computed with the child model’s pricing, which may differ from the parent).
The web UI shows each agent’s cost separately from the session total.
Finished subagent transcripts are persisted to a side directory next to the
parent session (<session-id>.subagents/<job-id>.json), so they survive
restarts and can be reopened. They are removed when the parent session is
deleted.
Custom script tools
Section titled “Custom script tools”Define tools as JSON files in .moa/tools/:
{ "name": "deploy", "description": "Deploy to staging", "command": "bash scripts/deploy.sh staging"}Each file defines one tool that runs a shell command. The tool is registered
automatically when Moa starts in that project — but only for directories the
user has explicitly trusted (like .mcp.json and repo-local config), so an
untrusted repo can’t register shell-executing tools that auto-run at the first
prompt.
Optional fields and parameters:
timeout— max seconds the command may run before it’s killed (default60).args— a runtime tool parameter (string). When supplied, it’s passed positionally to the command, which can reference it as"$1","$@", etc. Passing it positionally (not interpolated into the command) avoids shell injection.
Verify
Section titled “Verify”Define project checks in .moa/verify.json:
{ "checks": [ { "name": "build", "command": "make build" }, { "name": "test", "command": "make test" }, { "name": "lint", "command": "make lint" } ]}Run with /verify, or automatically after changes if auto_verify is enabled in config.
Verifying another repository or worktree
Section titled “Verifying another repository or worktree”Checks run in the session’s working directory by default. When a session’s work
spans several checkouts — the conversation starts in one repository and the code
being changed lives in another worktree — point verify at the other directory
instead of editing .moa/verify.json to reach across:
/verify ../other-worktreeThe agent can do the same through the tool’s cwd parameter. Relative paths
resolve against the session directory, and the target’s own .moa/verify.json
is the one that runs.
The directory must be one the session is allowed to touch: running a
.moa/verify.json means running the shell commands inside it, so the sandbox
applies here as it does everywhere else. If it is refused, allow it with
/path add <dir>. Sessions running unrestricted (YOLO) can target any
directory.
The verify tool is available even when the session’s own directory has no
.moa/verify.json — otherwise it would be missing from exactly the multi-repo
sessions that need it. Called with nothing to run, it says so.