Skip to content
letmoa.run

Configuration

Moa loads config from two levels, merged together:

  1. Global: ~/.config/moa/config.json
  2. Project: <cwd>/.moa/config.json

CLI flags override both at runtime. Project config extends global config; some fields are global-only (noted below).

Both files describe what you configured. What moa decides on its own while you work — the approvals you granted with “always allow”, the MCP servers you switched off for a project — is kept separately, in your project state, and never written into the project.

{
"permissions": {
"mode": "ask",
"allow": ["Bash(git:*)", "read"],
"deny": ["Bash(curl:*)", "Read(**/.env)"],
"model": "haiku",
"rules": ["Deny writes outside repository"]
},
"pinned_models": ["claude-sonnet-5", "gpt-5.3-codex"],
"max_budget": 2.00,
"max_turns": 100,
"brave_api_key": "...",
"mcp_servers": {
"docs": {
"command": "uvx",
"args": ["my-mcp-server"],
"env": { "API_KEY": "..." }
}
}
}
FieldTypeDescription
permissions.modestringyolo, ask, auto (default yolo)
permissions.allow[]stringGlob patterns auto-approved in ask mode
permissions.deny[]stringGlob patterns always denied
permissions.modelstringModel for auto mode evaluator
permissions.rules[]stringNatural-language rules for the evaluator

Pattern format: Tool(argPattern) — e.g. Bash(npm:*), Write(*.go), Edit(pkg/*). Case-insensitive tool names, glob-like arguments. Arg scoping now applies to grep/find/ls/multiedit (matched on their path), fetch_content (on its url), and apply_patch (matched against every file the patch touches).

Bash deny is not a security boundary. Bash(...) rules match the literal command string by prefix/glob. A rule like Bash(rm -rf:*) does not reliably block recursive deletes — it is trivially evaded by flag reordering (rm -fr, rm -r -f), absolute paths (/bin/rm -rf), a leading space, or shell aliases. Use deny to reduce accidents, not to contain an adversarial command. For real containment use mode: ask/auto (a human or model approves each call) and the path sandbox (path_scope).

FieldTypeDescription
path_scopestringworkspace or unrestricted
allowed_paths[]stringExtra directories allowed outside workspace
disable_sandboxboolDeprecated — use path_scope: "unrestricted"
FieldTypeDescription
max_budgetfloatMax USD per run (0 = unlimited)
max_turnsintMax agent turns per run (0 = unlimited)
max_tool_calls_per_turnintMax tool calls per turn (0 = unlimited)
max_run_durationstringGo duration, e.g. "30m" (empty = unlimited)
FieldTypeDefaultDescription
memory_enabledbooltrueCross-session project memory
auto_verifyboolfalseRun verification checks automatically after changes
brave_api_keystringEnables the web_search tool
cache_ttlstring"1h"Interactive prompt-cache TTL. Only "5m" changes behavior; any other value falls back to the 1h default
stt_languagestring"en"Speech-to-text language hint (ISO-639-1, e.g. "es", "en"). Avoids mis-detection on short clips. Use "auto" to let the model detect
stt_modelstring"gpt-transcribe"Speech-to-text model. "gpt-4o-mini-transcribe" costs half as much per minute; "whisper-1" is the older, slower model it replaced
stt_vocabularystring[][]Words the transcriber keeps getting wrong (names, jargon, product names). Accumulates across scopes: a project adds its terms to your global ones. Keep it short — long lists make transcription worse (max 50 terms)
persistent_shellbooltrueWhether bash persists working directory and exported env between calls in a session
compact_atint0Default auto-compaction threshold in tokens for sessions that set none of their own. 0 = automatic: compaction waits for the model’s window. A session’s own limit always wins, and a subagent inherits its parent’s, falling back to this. Values under the engine’s floor (reserve + kept context + margin, ≈41k) are raised, since a lower threshold would compact on every turn. Editable from the web Settings sheet
compact_strategystringnotifyWhat the agent gets before an automatic compaction, which otherwise arrives mid-task with no warning: anything it had worked out but not written down is replaced by the summary. plain = no warning. notify = a reminder as the threshold nears, so it can persist unfinished work first; costs nothing, since the notice rides on the next request. prepare = a full turn to write things down before summarizing; more thorough, but costs an extra request. Subagents are never warned under any setting: they have no memory or checkpoint to write to, and their findings already travel back in their report. Editable from the web Settings sheet
compact_modelstringsessionModel that writes compaction summaries. session (or unset) = the session’s own model, which is how compaction has always behaved. Any model spec (terra, sonnet…) makes that model summarize instead. The summarizer request shares no cached prefix with the conversation. The chosen model needs its provider’s credentials; when they are missing the session’s model summarizes instead and the transcript says so. Editable from the web Settings sheet
update_checkbooltrueCheck GitHub for a newer stable Moa release (six-hour ETag cache); set false to opt out
eventsobjectabsentWake-on-event sources (events.sources.<name>). Each source is a webhook with its own secret, a target ({project:dir}, {session:id}, or "inbox"), when_none (inbox/create), when_many (inbox/latest), create (model, thinking, yolo, title), autorun (default false; set true to start a turn), and rate (auto-deliveries and session creations per rolling hour, default 10). Global-only — secrets live in ~/.config/moa/config.json. See Event hooks and moa hooks
previewobjectabsentWhere the browser reaches the Live Preview proxy: public_url and port. Written by the web UI when you confirm the address the first time, so it is never asked again — you rarely edit it by hand. Only the address is stored: whether the proxy is running is decided per use, in memory, so restarting moa serve never reopens the port on its own. Global-only — a repository must not be able to tell Moa where to publish a proxy

Start stt_vocabulary empty and add words only once you catch the transcriber getting them wrong. It is a hint, not a substitution: listing a word biases spelling toward it, so a long list starts forcing your terms onto words that merely sound similar. Because it accumulates, your own name belongs in the global config and a project’s jargon in its .moa/config.json:

{ "stt_vocabulary": ["goreleaser", "Preact", "esbuild"] }
FieldTypeDefaultDescription
subagent_max_turnsint100Max agent turns per subagent run (0 = package default)
subagent_max_run_durationstring"10m"Max subagent wall-clock duration, Go duration (empty = package default)
subagent_max_concurrent_asyncint5Max concurrent async subagents (0 = package default)
subagent_allowed_models[]string[]Model IDs a subagent may run under. Empty = no restriction. Excluded models are hidden from the agent and refused if requested; a change applies to sessions already open, without a restart. The list governs the model a delegation explicitly asks for: a child that inherits the parent’s model still runs when that model is not listed. Editable from the web Settings sheet. Global-only.
FieldTypeDescription
pinned_models[]stringModels shown as shortcuts in the web selector. Global-only.
auto_title_modelstringModel for automatic session titles: auto (default), off, or a valid model spec/alias. After a new conversation accepts its first prompt, Auto sends that prompt to the selected available auxiliary provider; it does not retitle conversations restored with history. If the process stops while generation is in flight, the provisional title remains after restart. OpenAI Luna is selected when normal OpenAI completion credentials exist, otherwise Anthropic Haiku. Thus an Anthropic/xAI session can be sent to OpenAI, or an OpenAI/xAI session to Anthropic when Haiku is the fallback. Privacy-sensitive users should choose an explicit same-provider model or off.
session_brief_modelstringModel for web/Pulse session status briefs: auto (default), off, or a valid model spec/alias. Auto has the same cross-provider behavior as titles: a snippet of any session transcript can be sent to Luna, or to Haiku when it is the available fallback. Choose an explicit same-provider model or off when that is not acceptable.

The xAI models are grok-4.6 (also available as grok) and grok-4.5. They always reason: see Thinking levels. A provider-qualified custom model such as xai/<model-id> is accepted, but Moa has no context-window or pricing metadata for it unless it is in the built-in model registry.

The Meta models are muse-spark-1.3 (also available as muse) and muse-spark-1.3-contributor. The contributor variant is much cheaper because Meta uses its prompts to improve their products. Muse Spark always reasons, so off resolves to its lowest effort.

The OpenAI model gpt-6-astra is also available as astra (or gpt-6). Its five thinking positions map to low, medium, high, xhigh, and max, so the off position selects Astra’s lowest reasoning effort rather than disabling reasoning.

FieldTypeDescription
mcp_serversmapMCP server definitions (see example above)
disabled_mcp_servers[]stringServer names vetoed at this config level: the server stays configured but is never started. In a project file this is legacy — moa now records your vetoes in your project state
trusted_mcp_paths[]stringProject dirs whose .mcp.json is trusted. Global-only.
trusted_project_paths[]stringProject dirs whose .moa/config.json and .moa/tools/* are auto-loaded without a trust prompt. Global-only.

Moa also loads .mcp.json files (Claude Code-compatible format):

  • ~/.config/moa/.mcp.json — always loaded
  • <cwd>/.mcp.json — loaded only when the path is trusted

A server entry declares exactly one transport:

  • command (+ optional args, env) — stdio: Moa spawns the server as a local subprocess.
  • url (+ optional headers) — streamable HTTP: Moa connects to a remote endpoint. Only http and https are accepted.
{
"mcpServers": {
"local": { "command": "uvx", "args": ["my-mcp-server"] },
"relay": {
"url": "https://relay.example.com/mcp",
"headers": { "Authorization": "Bearer ..." }
}
}
}

Setting both command and url — or neither — is a configuration error and the file is rejected. headers are the only supported authentication mechanism for a remote server: they are sent on every request to that endpoint, so it is where credentials go; they are stored in plain text in the config file like any other key there. Credentials embedded in the URL itself (https://user:pass@host/mcp) are rejected — the URL is shown in the MCP panel and written to logs, so it is not a place for secrets.

Redirects are never followed: headers would be re-sent to whatever origin a 30x points at, so a redirecting endpoint fails the request instead.

A remote server has no process to supervise: it shows up in the MCP panel like any other server, and enable/disable/restart just drop and re-dial the connection. If the connection is lost the server is reported as exited and can be restarted. The endpoint is an outbound connection to an address you configured — Moa applies no network policy beyond the scheme check, exactly as with automation callback_urls.

Project-specific files live in <cwd>/.moa/:

PathPurpose
config.jsonProject config (merged with global)
verify.jsonVerification commands for the verify tool
tools/*.jsonCustom script tools
skills/<name>/SKILL.mdProject skills

config.json and tools/*.json are only loaded for directories listed in the global trusted_project_paths; .mcp.json needs trusted_mcp_paths. Until a path is trusted, moa uses the global config alone. In the web UI, the MCP banner offers Trust & Load to add the current directory to trusted_mcp_paths; trusted_project_paths is edited by hand in ~/.config/moa/config.json.

~/.config/moa/projects/<hash>/state.json holds what moa records about a project on your behalf, kept out of the repository:

FieldWritten when
permission_allowYou approve a tool call with “always allow”
disabled_mcp_serversYou switch a server off with the project scope
configNever — this one is yours to edit (see below)

The split follows who the decision belongs to. .moa/config.json describes the project — which MCP servers it uses, what its limits are — and is meant to be committed and shared. Approving a command on your machine is not that: it used to be appended to the project’s config file, so a click produced a diff nobody wanted to commit, and it followed the repository to everyone who cloned it.

This also keeps moa from writing into the checkout at all during a normal session, which is what previously made a shared checkout unusable: the file was created with private permissions, so whoever approved something first owned it.

Consequences worth knowing:

  • A project-scoped MCP veto is now yours alone; it no longer travels with the repository. To keep a server out of a project for everybody, don’t declare it in .moa/config.json.
  • The project scope no longer requires trusting the project, because nothing is written there. Trust still governs whether the project’s own config, script tools and .mcp.json are loaded.
  • allow patterns already present in a .moa/config.json keep working, and so does a disabled_mcp_servers entry left there by an older moa. Moa cannot tell a deliberate team policy from a leftover click, so nothing is migrated or removed — move them yourself if you want them gone.
  • The state is per workspace path, hashed the same way memory scopes its project facts. Moving a checkout starts a fresh state.

Settings for one project, without touching the repository

Section titled “Settings for one project, without touching the repository”

The config block takes the same fields as config.json, for settings you want in this project but not in its repository — for example, a turn limit you prefer here:

{
"config": {
"max_turns": 40
}
}

Moa never writes this block; it is yours to edit. It is applied after global config and after the project’s own, so it wins over both — but through the same merge rules, so max_turns, max_tool_calls_per_turn and max_run_duration can only be tightened: if your global config caps runs at 50 turns, asking for 500 here still gives you 50. max_budget is the exception and takes any explicit value, matching how the project’s own config has always behaved. No trust prompt is involved, because the file is yours rather than the checkout’s.

It takes the same fields as config.json, which includes the ones that widen what moa may do — permissions, path_scope, allowed_paths, mcp_servers. That is intended, since the file is your own and no repository can write to it, but it does mean a stray "path_scope": "unrestricted" here applies with no prompt. It cannot grant trust: trusted_project_paths and trusted_mcp_paths are global-only and ignored at this level, so a project cannot come to trust itself through it.

Use it when the setting is about how you work here, and .moa/config.json when it is about the project itself and should reach everyone who clones it.

VariablePurpose
ANTHROPIC_API_KEY / OPENAI_API_KEY / XAI_API_KEY / META_API_KEYProvider credentials (see Quickstart). An environment key takes precedence over the stored credential for that provider; in particular, XAI_API_KEY selects the metered api.x.ai API-key route and eclipses a stored xAI OAuth login, and META_API_KEY eclipses a stored Muse subscription login.
MOA_CONFIG_DIRMoves everything moa stores about itself (default ~/.config/moa): config.json, credentials, sessions, skills, memory, attachments. See Running two instances
MOA_SERVE_TOKENShared secret for moa serve opt-in authentication; equivalent to --token (see Web UI)
MOA_AUTOMATION_TOKENShared secret enabling the inbound Automation API; equivalent to --automation-token. Separate from MOA_SERVE_TOKEN
MOA_NO_UPDATE_CHECK=1Disables the best-effort GitHub release check for this process
MOA_ATTACHMENTS_DIRBase directory for moa serve attachment staging (default /tmp/moa-<uid>); detailed in Web UI
MOA_SERVE_STATIC_DIRServe the web UI from this directory instead of the build embedded in the binary; for frontend development (see Web UI)

Everything moa keeps about itself lives in one directory, ~/.config/moa by default. MOA_CONFIG_DIR moves all of it — config, credentials, sessions, skills, memory, attachments — so two instances can run on one machine without sharing anything:

Terminal window
MOA_CONFIG_DIR=~/.config/moa-work moa serve --port 8081

Each instance keeps its own API keys, model defaults, MCP servers and history. The one thing outside it is the temporary staging area for web-UI attachments, shared per user account and overridable with MOA_ATTACHMENTS_DIR.

The variable points moa at a different directory; it does not move what is already in the old one. A fresh MOA_CONFIG_DIR therefore starts empty — no credentials, no history, no skills — and the previous state stays untouched in ~/.config/moa. Copy across whatever you want to keep:

Terminal window
cp ~/.config/moa/auth.json ~/.config/moa-work/

If you were already setting MOA_CONFIG_DIR before moa 0.24, note that it used to move only part of the state: credentials and attachments followed it, while config.json, sessions and skills stayed in the home directory. Now that all of it moves together, that leftover state is no longer read — move the files you still want into the override directory.

Give each person a Unix account instead. Separate accounts already separate everything above, and the isolation is enforced by the operating system rather than by moa: one account cannot read the other’s credentials even if moa has a bug.

Give each account its own checkout as well. A single checkout shared through group permissions does not currently work: moa writes <project>/.moa/ with private permissions, so whoever triggers the first write owns it and the other account can no longer read the project’s verify.json, skills or script tools.

Moa is a single-user tool by design. It has no notion of who is asking: no per-user permissions, no ownership on sessions, no way to scope what one person’s agent may touch. Sharing one moa between two people means sharing one identity and one set of credentials, so keep the accounts separate rather than expecting moa to tell them apart.

Moa discovers AGENTS.md files from the working directory upward and from ~/.config/moa/. Their content is injected into the system prompt as project instructions. This is the main way to give the agent persistent rules, preferences, conventions and prohibitions. Reusable workflows belong in skills and executable procedures in scripts or registered tools; do not put either category in memory.