Skip to content
letmoa.run

Tools

Always registered:

ToolDescription
bashExecute shell commands (streamed, timeout, truncation + spill file). Persists cwd and exported env between calls in a session
bash_statusCheck a background bash job’s status and output
bash_waitBlock until a background bash job finishes and return its result
bash_cancelCancel a running background bash job
readRead text/image files with offset/limit
writeCreate or overwrite files (atomic: temp file + rename)
editExact-text replacement (single match enforced, atomic write)
multieditAtomic batch of edits to a single file
apply_patchApply multi-file unified diffs
grepSearch file content (prefers rg if installed)
findSearch files by glob (prefers fd if installed)
lsList directory contents
fetch_contentFetch a URL and extract readable markdown
memorySearch/read/update persistent cross-session notes
subagentSpawn a child agent (sync or async)
subagent_statusPoll async subagent jobs
subagent_waitBlock until an async subagent job finishes and return its result
subagent_cancelCancel a running async subagent
subagent_steerSend instructions or a correction to a subagent that is still running
tasksTrack implementation tasks
verifyRun the project’s verification checks
load_skillLoad 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_docsRead moa’s own documentation (this page included), embedded in the binary

Conditionally registered:

ToolCondition
web_searchbrave_api_key is configured
ask_userThe 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.

  • Use grep, find, ls for exploration
  • Use read before editing — edit warns if the file wasn’t read first
  • Use edit for surgical changes, multiedit for several changes in one file
  • Use apply_patch for coordinated changes across multiple files
  • Use write for new files or complete rewrites
  • Use bash when you need actual shell behavior

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 precedence
moa_docs(page: "automation") # HTTP API for triggering runs
moa_docs(page: "recipes/linear") # worked end-to-end integration

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 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.

Path-based tools are sandboxed to the workspace directory by default. Escape attempts via .. or symlinks are blocked.

Override with:

  • -yolo flag
  • path_scope: "unrestricted" in config
  • allowed_paths for specific extra directories
  • /path add <dir> at runtime in the web UI

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.

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.

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 through subagent_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 with resume: "<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.

Child agents run with their own, independent limits (they do not inherit the parent’s numbers, and have no budget/$ cap of their own):

LimitDefaultConfig key (config.json)
Max turns100subagent_max_turns
Max run duration10msubagent_max_run_duration (Go duration, e.g. "15m")
Max concurrent async jobs5subagent_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.

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.

Define tools as JSON files in .moa/tools/:

.moa/tools/deploy.json
{
"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 (default 60).
  • 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.

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.

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-worktree

The 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.