moat.yaml reference
The moat.yaml file configures how Moat runs your agent. Place it in your workspace root directory.
Backwards compatibility:
agent.yamlis still supported as a fallback. Ifmoat.yamlis not found, Moat looks foragent.yamlin the same directory. New projects should usemoat.yaml.
Complete example
# Metadata
name: my-agent
agent: my-agent
version: 1.0.0
# Runtime
dependencies:
- node@22
- postgres@17
- redis@7
# Custom base image (optional, must be Debian-based)
# base_image: ghcr.io/myorg/my-project-deps:latest
# Service overrides
services:
postgres:
env:
POSTGRES_DB: myapp
# Credentials
grants:
- github
- anthropic
- ssh:github.com
# Environment
env:
NODE_ENV: development
DEBUG: "true"
# External secrets
secrets:
OPENAI_API_KEY: op://Dev/OpenAI/api-key
DATABASE_URL: ssm:///production/database/url
# Workspace mode
workspace:
mode: bind # or 'volume' for an isolated copy in a Docker named volume
# Mounts
mounts:
- ./data:/data:ro
# Persistent volumes
volumes:
- name: state
target: /home/moatuser/.myapp
# Endpoints
ports:
web: 3000
api: 8080
# Network policy
network:
policy: strict
rules:
- "api.openai.com"
- "*.amazonaws.com"
# Execution
command: ["npm", "start"]
interactive: false
# Hooks
hooks:
post_build_root: apt-get update -qq && apt-get install -y -qq figlet
post_build: git config --global core.autocrlf input
pre_run: npm install
# Sandbox (Docker only)
# sandbox: none # Uncomment to disable gVisor
# Runtime (optional - auto-detects if not specified)
# runtime: docker # Force Docker runtime (useful for docker:dind on macOS)
# Container resources (applies to both Docker and Apple)
container:
memory: 16384 # 16 GB (default: 8192 for AI agents on Apple, 4096 otherwise)
cpus: 8 # CPU count (default: 4 for Apple, no limit for Docker)
dns: ["8.8.8.8", "8.8.4.4"] # DNS servers (default: Google DNS)
# Claude Code
claude:
sync_logs: true
plugins:
"plugin-name@marketplace": true
marketplaces:
custom:
source: github
repo: owner/repo
mcp:
my_server:
command: /path/to/server
args: ["--flag"]
env:
VAR: value
cwd: /workspace
# Codex
codex:
sync_logs: true
mcp:
my_server:
command: /path/to/server
args: ["--flag"]
env:
VAR: value
grant: openai
cwd: /workspace
# GitHub Copilot CLI
copilot:
model: gpt-5.4
context: long_context
reasoning_effort: high
experimental: false
autopilot: false
# Gemini CLI
gemini:
sync_logs: true
mcp:
my_server:
command: /path/to/server
args: ["--flag"]
env:
VAR: value
grant: github
cwd: /workspace
# Language servers
language_servers:
- go
# Remote MCP servers
mcp:
- name: context7
url: https://mcp.context7.com/mcp
auth:
grant: mcp:context7
header: CONTEXT7_API_KEY
# Snapshots
snapshots:
disabled: false
triggers:
disable_pre_run: false
disable_git_commits: false
disable_builds: false
disable_idle: false
idle_threshold_seconds: 30
exclude:
ignore_gitignore: false
additional:
- node_modules/
- .git/
retention:
max_count: 10
delete_initial: false
# Tracing
tracing:
disable_exec: false
Metadata
name
Human-readable name for the run. Used in moat list and hostname routing.
name: my-agent
- Type:
string - Default: Directory name
- CLI override:
--name
When using moat wt or --worktree, the name field is used to generate the run name as {name}-{branch}. If name is not set, the run is named after the branch.
agent
Agent identifier. Used internally for tracking.
agent: my-agent
- Type:
string - Default: Same as
name
version
Version number for the agent configuration.
version: 1.0.0
- Type:
string - Default: None
Container runtime
runtime
Force a specific container runtime (Docker or Apple containers).
runtime: docker # Force Docker runtime
- Type:
string - Values:
docker|apple - Default: Auto-detected (Apple containers on macOS 26+ with Apple Silicon, Docker otherwise)
- CLI override:
--runtime
Force Docker when dependencies require privileged mode (e.g., docker:dind).
Runtime dependencies
dependencies
List of runtime dependencies. The first dependency determines the base image.
dependencies:
- node@22
- python@3.11
- Type:
array[string] - Default:
[](usesdebian:bookworm-slim)
When git is listed as a dependency, the host’s git identity (user.name and user.email) is automatically imported into the container. This can be overridden with a post_build hook.
Supported dependencies
| Dependency | Base image |
|---|---|
node@18 | node:18-slim |
node@22 | node:22-slim |
node@22 | node:22-slim |
python@3.10 | python:3.10-slim |
python@3.11 | python:3.11-slim |
python@3.12 | python:3.12-slim |
go@1.21 | golang:1.21 |
go@1.22 | golang:1.22 |
| (none) | debian:bookworm-slim |
Service dependencies
Service dependencies start sidecar containers that run alongside your agent. Moat generates credentials automatically and injects connection details as environment variables.
dependencies:
- node@22
- postgres@17
- redis@7
| Dependency | Service | Default port |
|---|---|---|
postgres@16 | PostgreSQL 16 | 5432 |
postgres@17 | PostgreSQL 17 | 5432 |
mysql@8 | MySQL 8 | 3306 |
mysql@9 | MySQL 9 | 3306 |
redis@7 | Redis 7 | 6379 |
ollama@0.18.1 | Ollama | 11434 |
Each service injects MOAT_* environment variables into the main container. See Service environment variables for the full list.
docker
The docker dependency provides Docker access inside the container. You must specify a mode explicitly:
| Syntax | Mode | Description |
|---|---|---|
docker:host | Host | Mounts the host Docker socket |
docker:dind | Docker-in-Docker | Runs an isolated Docker daemon |
docker:host
dependencies:
- docker:host
Host mode mounts /var/run/docker.sock from the host. Fast startup, shared image cache, full Docker API access. The agent can see and interact with all host containers.
docker:dind (Docker-in-Docker)
dependencies:
- docker:dind
DinD mode runs an isolated Docker daemon inside the container. Complete isolation from host Docker, clean slate on each run. Requires privileged mode (set automatically), ~5-10 second startup, vfs storage driver.
BuildKit sidecar (automatic with docker:dind)
When using docker:dind, Moat automatically deploys a BuildKit sidecar container to provide fast image builds:
- BuildKit sidecar: Runs
moby/buildkit:latestin a separate container - Shared network: Both containers communicate via a Docker network (
moat-<run-id>) - Environment:
BUILDKIT_HOST=tcp://buildkit:1234routes builds to the sidecar - Full Docker: Local
dockerdin main container providesdocker ps,docker run, etc. - Performance: BuildKit layer caching,
RUN --mount=type=cache, multi-stage build support
This configuration is automatic and requires no additional setup. The main container receives BUILDKIT_HOST=tcp://buildkit:1234; when unset or unreachable, builds fall back to the Docker SDK.
Example:
agent: builder
dependencies:
- docker:dind # Automatically includes BuildKit sidecar
# Your code can now use:
# - docker build (uses BuildKit for speed)
# - docker ps (uses local dockerd)
# - docker run (uses local dockerd)
Runtime requirements
Both docker modes require Docker runtime:
- docker:host - Apple containers cannot mount the host Docker socket
- docker:dind - Apple containers do not support privileged mode (required for dockerd)
# Force Docker runtime on macOS
moat run --runtime docker ./my-project
base_image
Use a custom base image instead of the default image selection. Moat layers its infrastructure (non-root user, entrypoint, CA certs, etc.) on top of this image.
base_image: ghcr.io/myorg/my-project-deps:latest
- Type:
string - Default: Auto-selected based on
dependencies(see table above)
The base image must be Debian-based (Debian, Ubuntu) because Moat uses apt-get to install its dependencies. Alpine, Fedora, and other distributions are not supported.
When base_image is set, it overrides the automatic image selection from dependencies. Runtime dependencies are still installed on top of the custom base image.
# Pre-built image with project tooling, plus TypeScript on top
base_image: ghcr.io/myorg/my-project-deps:latest
dependencies:
- typescript
Credentials
grants
Credentials to inject into the run.
grants:
- github
- anthropic
- openai
- ssh:github.com
- Type:
array[string] - Default:
[] - CLI override:
--grant(additive)
Grant formats
| Format | Description |
|---|---|
github | GitHub API |
anthropic | Anthropic API |
openai | OpenAI API |
gemini | Google Gemini API |
npm | npm registries |
ssh:HOSTNAME | SSH access to specific host |
oauth:NAME | OAuth credentials for a service |
Credentials must be stored first with moat grant.
Environment
env
Environment variables set in the container.
env:
NODE_ENV: development
DEBUG: "true"
PORT: "3000"
- Type:
map[string]string - Default:
{} - CLI override:
-e KEY=VALUE(additive)
Values must be strings. Quote numeric values.
secrets
Environment variables resolved from external backends.
secrets:
OPENAI_API_KEY: op://Dev/OpenAI/api-key
DATABASE_URL: ssm:///production/database/url
CUSTOM_API_KEY: env://CUSTOM_API_KEY
- Type:
map[string]string - Default:
{}
Secret URL formats
| Format | Backend | Example |
|---|---|---|
op://VAULT/ITEM/FIELD | 1Password | op://Dev/OpenAI/api-key |
ssm:///PATH | AWS SSM (default region) | ssm:///prod/db/url |
ssm://REGION/PATH | AWS SSM (specific region) | ssm://us-west-2/prod/db/url |
env://VAR_NAME | Host environment | env://MY_API_KEY |
Workspace
workspace.mode
Controls how the host working tree is presented to the container.
workspace:
mode: volume # or 'bind' (default)
- Type:
string - Values:
bind|volume - Default:
bind - CLI override:
--workspace-mode
| Mode | Behavior |
|---|---|
bind | Bind-mounts the host directory at /workspace (default) |
volume | Copies the host directory into an ephemeral Docker named volume at /workspace |
Volume mode
In volume mode, Moat copies the host working tree into a fresh named Docker volume (moat-ws-<run-id>) before the run starts. The host directory is bind-mounted read-only at /mnt/host-workspace for the copy-in and remains mounted there for the container’s lifetime (Docker cannot unmount mid-run). The agent works against the volume at /workspace and cannot write to the host, but it can still read the host tree at /mnt/host-workspace — so exclude: keeps paths out of the writable /workspace copy, but is not a confidentiality boundary. Keep secrets out of the workspace directory entirely rather than relying on exclude:.
The volume is created fresh for every run. Changes the agent makes inside /workspace are isolated to the volume and do not affect the host.
Copy-in semantics and excludes
The full working tree is copied in, except paths listed in the /workspace mount’s exclude: field. Use exclude: to skip directories that are large or irrelevant (e.g., node_modules):
workspace:
mode: volume
mounts:
- source: .
target: /workspace
exclude:
- node_modules
- .venv
- dist
Exclude patterns may contain letters, digits, /, ., -, _, and *. Shell metacharacters (?, [, ]) and characters outside that set are rejected.
.git cannot be excluded in volume mode. The copy-in always includes the full .git directory — a partial .git is a broken repository.
Constraints
- Docker only. Volume mode requires the Docker runtime. Runs on the Apple container runtime fail with a clear error; use
workspace.mode: bindor pass--runtime docker. - Git worktrees and submodules are rejected. When
.gitis a file rather than a directory (as in a git worktree or submodule checkout), volume mode fails. Use the main checkout orworkspace.mode: bind.
A mounts: entry targeting /workspace is allowed in volume mode and is consulted only for its exclude: list — the named volume always provides /workspace, so no duplicate mount is created.
Extraction
The volume is the only copy of the agent’s work. Extract changes with moat snapshot before destroying the run:
moat snapshot <run>
moat snapshot restore <run> --to ~/output
git -C ~/myrepo fetch ~/output
See Volume-mode workspaces for the full extraction workflow.
Lifecycle
The volume is named moat-ws-<run-id> and removed when the run is destroyed. Volumes left by crashed runs are reclaimed by the daemon when its idle timer fires (after 5 minutes with no active runs). Destroying a volume-mode run that has no extraction snapshot requires --force.
Mounts
mounts
Additional directories to mount in the container. Accepts a mixed array of strings and objects.
mounts:
- ./data:/data:ro
- source: .
target: /workspace
exclude:
- node_modules
- .venv
- Type:
array[string | object] - Default:
[] - CLI override:
--mount(additive, string form only)
String format
<host-path>:<container-path>:<mode>
| Field | Description |
|---|---|
host-path | Path on host (relative or absolute) |
container-path | Path inside container (absolute) |
mode | ro (read-only) or rw (read-write, default) |
Object format
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
source | string | yes | Host path (relative or absolute) | |
target | string | yes | Container path (absolute) | |
mode | string | no | rw | ro or rw |
exclude | []string | no | [] | Paths relative to target to overlay with tmpfs |
Excluded paths are overlaid with tmpfs (in-memory) mounts inside the container. The host files at those paths are hidden. This prevents VirtioFS file descriptor accumulation from large dependency trees on Apple Containers. See Excluding directories for details.
The workspace is always mounted at /workspace unless an explicit mount targets /workspace, in which case it replaces the automatic mount.
Volumes
volumes
Named volumes that persist data across runs for the same agent name.
name: my-agent
volumes:
- name: state
target: /home/moatuser/.myapp
- name: cache
target: /var/cache/myapp
readonly: true
- Type:
array[object] - Default:
[] - Requires:
namefield must be set (volumes are scoped by agent name)
Unlike mounts: (bind mounts with a host-side source path), volumes are managed by moat. They have no host-side source — moat handles the storage.
Volume fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Volume name, scoped to agent. Must match [a-z0-9][a-z0-9_-]*. |
target | string | yes | Absolute path inside the container. |
readonly | bool | no | Mount as read-only. Default: false. |
type | string | no | Backing store: bind (default) or volume. See Storage. |
Storage
A volume’s type selects how it is backed:
bind(default) — a host directory at~/.moat/volumes/<agent-name>/<volume-name>/, bind-mounted into the container. The data is visible and editable on the host. On runtimes that run containers inside a VM, a bind mount crosses the host↔VM filesystem-sharing layer, which adds per-operation latency for directories with many small files and can differ from a native filesystem in its file-locking and memory-mapping behaviour. Works on both Docker and Apple container runtimes.volume— a Docker named volume (moat_<agent-name>_<volume-name>) stored on the container engine’s native filesystem (inside the VM where applicable). Not visible on the host. It bypasses the host-sharing layer, so it is faster for I/O- and metadata-heavy directories and behaves like an ordinary local filesystem. Docker runtime only (rejected on the Apple container runtime).
Choose volume for large, container-only working directories — build caches, dependency trees, tool state — where host visibility is not needed and throughput or filesystem semantics matter. Choose bind when you want to read or edit the data from the host.
volumes:
- name: build-cache
target: /workspace/.cache
type: volume
Volume lifecycle
| Event | Behavior |
|---|---|
| First run | Volume created automatically |
| Stop/Destroy | Volume persists |
| Next run (same agent name) | Volume reattached |
moat volumes rm <agent> | Volume deleted |
moat clean | Volumes not deleted |
Managing volumes
moat volumes ls # List managed volumes
moat volumes rm <agent-name> # Remove volumes for an agent
moat volumes prune # Remove all managed volumes
Note:
moat volumescommands managetype: bindvolumes (the host directories under~/.moat/volumes/). Atype: volumeentry is a native Docker named volume — remove it withdocker volume rm moat_<agent-name>_<volume-name>.
For examples of using volumes to cache dependencies across runs, see Recipes.
Endpoints
ports
Endpoint ports to expose via hostname routing.
ports:
web: 3000
api: 8080
- Type:
map[string]int - Default:
{}
Endpoints are accessible at https://<endpoint>.<name>.localhost:<proxy-port> when the routing proxy is running.
Network
network.policy
Network policy mode.
network:
policy: strict
- Type:
string - Values:
permissive,strict - Default:
permissive
| Mode | Behavior |
|---|---|
permissive | All outbound HTTP/HTTPS allowed |
strict | Only allowed hosts + grant hosts |
network.rules
Per-host access rules. Each entry is either a plain hostname string or a map of hostname to a list of method+path rules.
network:
policy: strict
rules:
- "api.openai.com"
- "*.github.com"
- "*.*.amazonaws.com"
- Type:
array[string | map[string]array[string]] - Default:
[]
Hostname patterns support * (matches any single segment).
Hosts from granted credentials are automatically allowed regardless of this list.
Per-host request rules
Each host entry can include a list of method+path rules that filter requests to that host:
network:
policy: strict
rules:
- "api.github.com":
- "allow GET /repos/**"
- "deny * /**"
- "api.openai.com"
Rule format: "<allow|deny> <method> <path-pattern>"
method: HTTP method (GET,POST,PUT,DELETE,PATCH, etc.) or*for any methodpath-pattern: URL path pattern where*matches a single path segment and**matches zero or more segments
Rules are evaluated in order — the first matching rule wins. If no rule matches, the request falls through to the policy default (permissive allows it, strict blocks it).
Examples
Read-only access to a REST API:
network:
policy: strict
rules:
- "api.example.com":
- "allow GET /**"
- "deny * /**"
Block administrative endpoints while allowing everything else:
network:
policy: permissive
rules:
- "api.example.com":
- "deny * /admin/**"
- "deny DELETE /**"
network.keep_policy
Keep policy rules for HTTP requests passing through the proxy. Works alongside network.rules — the network policy controls which hosts are reachable, while keep_policy controls what operations are allowed on those hosts.
Accepts the same three formats as mcp[].policy: starter pack name, file path, or inline rules.
# File-based rules
network:
policy: strict
rules:
- "api.example.com"
keep_policy: .keep/api-rules.yaml
# Inline rules
network:
policy: strict
rules:
- "api.example.com"
keep_policy:
deny: [DELETE]
mode: enforce
- Type:
stringorobject - Default: none (no Keep policy enforcement)
See also: MCP servers: Policy enforcement for the same rule format applied to MCP tool calls
Request-body rules (params.body)
File- or pack-based policies can match on the parsed JSON request body via params.body. (The inline deny: [...] shorthand matches the operation path only and cannot inspect bodies.)
# .keep/api-rules.yaml
scope: http
mode: enforce
rules:
# Block a request whose JSON body carries a secret. hasSecrets scans the whole
# body recursively (every string leaf). Match the host in `when` (see the
# operation-matching note below) rather than an `operation:` glob.
- name: deny-secret-in-body
match: { when: "params.host == 'api.example.com' && params.body != null && hasSecrets(params.body)" }
action: deny
# Match an exact body field, scoped to a host + path.
- name: deny-destructive
match: { when: "params.host == 'api.example.com' && params.path == '/graphql' && params.body != null && params.body.operation == 'delete'" }
action: deny
Behavior and limits:
- Operation matching (important). Keep matches a rule’s
operationcase-insensitively against the runtime operation string"<method> <host><path>"(e.g."post api.example.com/repos"— the method is lowercased) usingpath.Match, where*does not cross/. Sooperation: "POST api.example.com/*"never matches an HTTP request (uppercase method, and*stops at the first/). For HTTP body rules, match in thewhenclause on the lowercasedparams.host/params.method/params.pathand omitoperation(an emptyoperationis a catch-all), as shown above. - Authoring guard:
has(params.body)is always true for body-carrying requests, so test for a populated body withparams.body != null. An empty/whitespace body is passed asnull. A rule that compiles can still evaluate falsy and fail open — validate that your rule actually denies a matching request, not just that the policy loads. - JSON only, fail-closed. Bodies are inspected only when
Content-Typeisapplication/json. Once any rule in thehttpscope referencesparams.body, every non-JSON,Content-Encoding-compressed (e.g. gzip), duplicate-key, malformed, or oversized body is denied — for all hosts in the scope, not just the host a body rule targets. Adding one body rule effectively makes the wholehttpscope JSON-only. Scope body rules deliberately. - HTTPS only. Body inspection runs on intercepted HTTPS (CONNECT) requests. Plain
http://requests are not inspected — rely onnetwork.policy/network.rulesto disallow plaintext egress to sensitive hosts. - Not covered.
paramsexposesmethod,host,path, andbodyonly — not URL query parameters, request headers, response bodies, or non-HTTP egress. Pair body rules with strict host/path rules and a restrictivenetwork.policyas the primary exfil control; body inspection is a narrow opt-in hardening primitive, not a complete DLP control. - Daemon upgrade. Request-body rules require a proxy daemon built with body-inspection support. If the running daemon is older,
moat runfails with a clear error — runmoat proxy restartto replace it with a fresh daemon.
network.host
TCP ports on the host machine that the container may access.
network:
host:
- 11434 # Ollama
- 5432 # Postgres running on host
- Type:
array[int] - Default:
[](all host traffic blocked)
By default, containers cannot reach any service running on the host machine — even in permissive mode. network.policy controls outbound internet access; network.host controls access to the host independently. You must list each port explicitly.
Use the MOAT_HOST_GATEWAY environment variable (automatically set in every container) to reach the host:
curl http://$MOAT_HOST_GATEWAY:11434/api/tags
MOAT_HOST_GATEWAY is a synthetic hostname (moat-host) that resolves to the host gateway address regardless of runtime (Docker, Apple containers, Rancher Desktop). Always use $MOAT_HOST_GATEWAY rather than hardcoding addresses like host.docker.internal or 127.0.0.1.
The value is a hostname, not a numeric IP. It works transparently with HTTP(S) clients that go through the proxy, but if a container script uses it with tools that expect an address literal — ping, nslookup, iptables, or raw socket code — resolve it via /etc/hosts or getent hosts "$MOAT_HOST_GATEWAY" first.
Example: agent with local Ollama
network:
policy: strict
rules:
- "api.github.com"
host:
- 11434
Inside the container:
OLLAMA_HOST=http://$MOAT_HOST_GATEWAY:11434 ollama run llama3
Execution
command
Default command to run.
command: ["npm", "start"]
- Type:
array[string] - Default: None (uses image default)
- CLI override:
-- command(replaces)
For shell commands:
command: ["sh", "-c", "npm install && npm start"]
interactive
Enable interactive mode.
interactive: true
- Type:
boolean - Default:
false - CLI override:
-i
When true, allocates a TTY and connects stdin. The session owns the terminal. Press Ctrl-/ k to stop the run; Ctrl+C is forwarded to the container process.
When false (default), output streams to the terminal. Press Ctrl+C to stop. Use moat logs <id> to review output after the run.
Required for shells, REPLs, and interactive tools.
clipboard
Enable host clipboard bridging.
# Disable clipboard bridging
clipboard: false
- Type:
bool - Default:
true - CLI override:
--no-clipboard
When enabled, moat intercepts Ctrl+V during interactive sessions, reads the host clipboard, and makes the data available inside the container via a headless X server. This allows coding agents to paste images and text from the host clipboard.
Requires xvfb and xclip in the container image (added automatically to moat-built images).
Hooks
Lifecycle hooks that run at different stages of the container lifecycle.
hooks.post_build_root
Command to run as root during image build, after dependencies are installed. Baked into image layers and cached.
hooks:
post_build_root: apt-get update -qq && apt-get install -y -qq figlet
- Type:
string - Default: None
Use for system-level setup: installing system packages, kernel tuning, modifying /etc files.
hooks.post_build
Command to run as the container user (moatuser) during image build, after dependencies are installed. Baked into image layers and cached.
hooks:
post_build: git config --global core.autocrlf input
- Type:
string - Default: None
Use for user-level image setup: configuring tools, setting defaults.
Build hooks run during image build, before your workspace is mounted. They can only use commands available in the image — not files from your project directory. For multi-step setup, chain commands with &&:
hooks:
post_build: git config --global core.autocrlf input && git config --global pull.rebase true
hooks.pre_run
Command to run as the container user (moatuser) in /workspace on every container start, before the main command.
hooks:
pre_run: npm install
- Type:
string - Default: None
Use for workspace-level setup that needs your project files: installing dependencies, running codegen, building assets. This runs on every start, but workspace-aware package managers like npm install and pip install are fast no-ops when dependencies are current.
pre_run runs before any command, including when moat claude or moat codex overrides command.
Execution order
Build hooks (post_build_root, post_build) run during image build and are cached as Docker layers — they cannot access workspace files. pre_run runs at container start after the workspace is mounted and is not cached.
Order: dependencies installed -> post_build_root (root) -> post_build (moatuser) -> container start -> pre_run (moatuser) -> command. Use --rebuild to force re-running build hooks.
Sandbox
sandbox
Configures container sandboxing mode. Only affects Docker containers (Apple containers use macOS virtualization).
sandbox: none
- Type:
string - Values:
""(empty),none - Default:
""(gVisor sandbox enabled) - CLI override:
--no-sandbox
| Value | Description |
|---|---|
| (empty/omitted) | gVisor sandbox enabled (default) |
none | Disable gVisor sandbox |
Setting sandbox: none is equivalent to running with --no-sandbox. Use this when your agent requires syscalls that gVisor doesn’t support.
Note: Disabling the sandbox reduces isolation. Only use when necessary for compatibility.
Container
Container resource limits and settings that apply to both Docker and Apple container runtimes.
container.memory
Memory limit in megabytes.
container:
memory: 8192 # 8 GB
- Type:
integer - Default:
8192MB (8 GB) formoat claude,moat codex, andmoat geminion Apple containers;4096MB (4 GB) for other Apple container workloads; no limit for Docker
Apple containers have a system default of 1024 MB which is insufficient for AI coding agents. Moat defaults to 8 GB for agent runs on Apple containers. Docker containers have no default memory limit regardless of the agent. Setting container.memory explicitly always takes precedence.
container.cpus
Number of CPUs available to the container.
container:
cpus: 8
- Type:
integer - Default: System default (Apple: typically 4, Docker: no limit)
container.dns
DNS servers for both runtime containers and builders.
container:
dns: ["192.168.1.1", "1.1.1.1"]
- Type:
array[string] - Default:
["8.8.8.8", "8.8.4.4"](Google DNS)
Applies to both Docker and Apple containers. Used for both build-time dependency installation and runtime name resolution.
container.ulimits
Resource limits (ulimits) for the container process. Applies to both Docker and Apple containers.
container:
ulimits:
nofile:
soft: 1024
hard: 65536
nproc:
soft: 4096
hard: 4096
memlock:
soft: -1
hard: -1
- Type:
map[string, {soft: integer, hard: integer}] - Default: Runtime defaults (inherited from host/daemon)
Each key is a ulimit name. Values must include both soft and hard limits. Use -1 for unlimited. The soft limit must not exceed the hard limit.
Supported ulimit names: core, cpu, data, fsize, locks, memlock, msgqueue, nice, nofile, nproc, rss, rtprio, rttime, sigpending, stack.
Apple containers require CLI version 0.9.0 or later for ulimit support.
Service dependencies
services
Customize service behavior for dependencies declared in dependencies:.
dependencies:
- postgres@17
- redis@7
services:
postgres:
env:
POSTGRES_PASSWORD: op://vault/postgres/password
POSTGRES_DB: myapp
wait: false
- Type:
map[string]object - Default:
{}
Each key matches a service name from dependencies: (e.g., postgres, mysql, redis).
Service fields
| Field | Type | Default | Description |
|---|---|---|---|
env | map[string]string | {} | Environment variables for the service container. Supports secret references. |
image | string | (auto) | Override default image (Docker runtime only) |
memory | integer | (runtime default) | Memory limit for the service container in MB. Useful for memory-intensive services like Ollama. |
wait | boolean | true | Block main container start until service is ready |
Setting wait: false starts the main container without waiting for the service health check to pass.
memory sets the limit for the service sidecar container, independent of container.memory (which limits the main agent container).
Service-specific lists
Some services accept additional list configuration beyond env and wait. These keys are defined by the service’s registry entry:
| Service | Key | Purpose |
|---|---|---|
ollama | models | Models to pull during startup |
Example:
services:
ollama:
memory: 4096 # 4 GB — size to match your largest model
models:
- qwen2.5-coder:7b
- nomic-embed-text
Service environment variables
Moat injects MOAT_* environment variables into the main container for each service dependency. Credentials are auto-generated per run.
Postgres
| Variable | Description | Example |
|---|---|---|
MOAT_POSTGRES_URL | Full connection URL | postgresql://postgres:pass@host:5432/postgres |
MOAT_POSTGRES_HOST | Hostname | postgres |
MOAT_POSTGRES_PORT | Port | 5432 |
MOAT_POSTGRES_USER | Username | postgres |
MOAT_POSTGRES_PASSWORD | Auto-generated password | |
MOAT_POSTGRES_DB | Database name | postgres |
MySQL
| Variable | Description | Example |
|---|---|---|
MOAT_MYSQL_URL | Full connection URL | mysql://root:pass@host:3306/moat |
MOAT_MYSQL_HOST | Hostname | mysql |
MOAT_MYSQL_PORT | Port | 3306 |
MOAT_MYSQL_USER | Username | root |
MOAT_MYSQL_PASSWORD | Auto-generated password | |
MOAT_MYSQL_DB | Database name | moat |
Redis
| Variable | Description | Example |
|---|---|---|
MOAT_REDIS_URL | Full connection URL | redis://:pass@host:6379 |
MOAT_REDIS_HOST | Hostname | redis |
MOAT_REDIS_PORT | Port | 6379 |
MOAT_REDIS_PASSWORD | Auto-generated password |
Ollama
| Variable | Description | Example |
|---|---|---|
MOAT_OLLAMA_HOST | Service hostname | ollama |
MOAT_OLLAMA_PORT | Service port | 11434 |
MOAT_OLLAMA_URL | Base URL for the Ollama API | http://ollama:11434 |
Claude Code
claude.base_url
Redirect Claude Code API traffic through a host-side LLM proxy. Sets ANTHROPIC_BASE_URL inside the container and registers credential injection for the proxy host.
claude:
base_url: http://localhost:8787
- Type:
string(URL) - Default: none (Claude Code connects to
api.anthropic.comdirectly) - Scheme must be
httporhttps
Moat routes traffic through a relay endpoint on the Moat proxy, which forwards requests to the configured URL with credentials injected. This works transparently with localhost URLs because the relay runs on the host where localhost resolves correctly. Credentials from the anthropic or claude grant are injected for the base URL host in addition to the standard api.anthropic.com injection.
claude.llm-gateway
Evaluates Keep policy rules on Anthropic API responses. The proxy buffers each response, checks tool_use blocks against the rules, and denies responses that violate the policy before they reach the container.
Mutually exclusive with claude.base_url.
| Field | Type | Default | Description |
|---|---|---|---|
policy | string or object | — | Policy rules (same format as mcp[].policy) |
claude:
llm-gateway:
policy: .keep/llm-rules.yaml
- Type:
object - Default: none (no LLM policy)
See also: Running Claude Code: LLM response policy
claude.sync_logs
Mount Claude Code’s log directory for observability.
claude:
sync_logs: true
- Type:
boolean - Default:
true(whenanthropicgrant is used)
claude.subscription_type
Sets the subscriptionType written to Claude Code’s .credentials.json inside the
container (e.g. pro, max). Claude Code needs a non-empty subscription type to
treat the session as a subscription rather than “API Usage Billing”.
claude:
subscription_type: max
- Type:
string - Default:
max
setup-token and pasted-token grants carry no plan information, so they always
default to max. If you are not on a Max plan and use one of those grant types,
set claude.subscription_type to your actual plan (e.g. pro) — otherwise Claude
Code will show “Max” and may surface Max-only options locally that then fail
server-side. Grants created with import existing credentials read the real plan
from your host login, so they don’t need this override.
The real plan limits are always enforced server-side via the token the proxy injects; this value only affects what Claude Code displays and gates locally.
claude.rate_limit_tier
Sets the rateLimitTier written to Claude Code’s .credentials.json (e.g.
default_claude_max_20x). Optional; mainly affects Claude Code’s local rate-limit
hints.
claude:
rate_limit_tier: default_claude_max_20x
- Type:
string - Default: unset (omitted), unless an imported grant supplied it.
claude.plugins
Enable or disable plugins. Plugins are installed during image build and cached in Docker layers, eliminating startup latency.
claude:
plugins:
"plugin-name@marketplace": true
"other-plugin@marketplace": false
- Type:
map[string]boolean - Default:
{}
Host plugin inheritance
Moat automatically discovers plugins you’ve installed on your host machine via Claude Code:
- Host marketplaces: Marketplaces registered via
claude plugin marketplace addare read from~/.claude/plugins/known_marketplaces.json - Host plugins: Plugin settings from
~/.claude/settings.jsonare included - Moat defaults: Settings from
~/.moat/claude/settings.json(if present) - Project settings: Settings from your workspace’s
.claude/settings.json - moat.yaml: Explicit overrides in
claude.plugins(highest priority)
This means plugins you’ve enabled on your host are automatically available in Moat containers without additional configuration.
Moat detects plugin changes and rebuilds the image automatically on the next run. Use --rebuild only to force a fresh build when the configuration has not changed (e.g., to pick up updated base images or unpinned package versions).
claude.marketplaces
Additional plugin marketplaces.
claude:
marketplaces:
custom:
source: github
repo: owner/repo
- Type:
map[string]object - Default:
{}
Marketplace fields
| Field | Description |
|---|---|
source | Source type (github) |
repo | Repository path (owner/repo) |
claude.mcp
Sandbox-local MCP servers that run as child processes inside the container. Configuration is written to .claude.json with type: stdio.
claude:
mcp:
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
env:
API_KEY: my-key
cwd: /workspace
- Type:
map[string]object - Default:
{}
MCP server fields
| Field | Type | Description |
|---|---|---|
command | string | Server executable path (required) |
args | array[string] | Command arguments |
env | map[string]string | Environment variables |
cwd | string | Working directory for the server process |
Note: The grant field is not supported for claude.mcp servers. Use codex.mcp / gemini.mcp which support grant.
Note: For remote HTTP-based MCP servers, use the top-level mcp: field instead. See MCP servers guide.
Language servers
language_servers
Prepackaged language servers that provide code intelligence inside the container via Claude Code plugins. Each entry installs the server binary and its runtime dependencies during image build, then enables the corresponding Claude Code plugin.
language_servers:
- go
- typescript
- python
- Type:
array[string] - Default:
[]
Adding a language server automatically:
- Installs the server binary and its runtime dependencies during image build
- Enables the corresponding Claude Code plugin (from the
claude-plugins-officialmarketplace)
Available language servers:
| Name | Description | Dependencies installed |
|---|---|---|
go | Go language server (code intelligence, refactoring, diagnostics) | go, gopls |
typescript | TypeScript/JavaScript language server (code intelligence, diagnostics) | node, typescript, typescript-language-server |
python | Python language server (code intelligence, type checking, diagnostics) | python, pyright |
Example:
agent: claude
language_servers:
- go
grants:
- anthropic
Runtime dependencies are added automatically — listing them in dependencies: is not required.
Note: Prepackaged language servers are currently supported with Claude Code only.
mcp
Configures MCP (Model Context Protocol) servers accessed through Moat’s proxy relay. Supports both remote HTTPS servers and host-local HTTP servers.
mcp:
- name: context7
url: https://mcp.context7.com/mcp
auth:
grant: mcp:context7
header: CONTEXT7_API_KEY
- Type:
array[object] - Default:
[]
Fields:
name(required): Identifier for the MCP server (must be unique)url(required): Endpoint for the MCP server. HTTPS is required for remote servers. HTTP is allowed for host-local servers (localhost,127.0.0.1, or[::1])auth(optional): Authentication configurationgrant(required if auth present): Name of grant to use (format:mcp:<name>; the deprecatedmcp-<name>form is still accepted)header(required if auth present): HTTP header name for credential injection
Credential injection:
Credentials are stored with moat grant mcp <name> and injected by the proxy at runtime. The agent never sees real credentials.
Example with remote and host-local servers:
mcp:
- name: context7
url: https://mcp.context7.com/mcp
auth:
grant: mcp:context7
header: CONTEXT7_API_KEY
- name: local-tools
url: http://localhost:3000/mcp
# Host-local server: auth optional, proxy bridges container to host
- name: public-mcp
url: https://public.example.com/mcp
# No auth block = no credential injection
Host-local MCP servers:
MCP servers running on the host machine (e.g., http://localhost:3000) are not accessible from inside the container. Moat’s proxy relay bridges this gap — the relay runs on the host and forwards container requests to the host-local server.
Note: For sandbox-local MCP servers running inside the container, use claude.mcp, codex.mcp, or gemini.mcp instead.
See also: MCP servers guide
Each mcp: entry may be a bare service name (a string) when the server is in
Moat’s built-in catalog. The bare name resolves to its url and auth
automatically; switch to the map form to add a policy or override a field.
Unknown names require an explicit url.
Recognized shorthand names:
| Name | Auth type | Grant |
|---|---|---|
asana | OAuth | oauth:asana |
betterstack | OAuth | oauth:betterstack |
cloudflare | OAuth | oauth:cloudflare |
context7 | API key | mcp-context7 |
hubspot | OAuth | oauth:hubspot |
langfuse-eu | Basic auth | mcp:langfuse |
langfuse-hipaa | Basic auth | mcp:langfuse |
langfuse-jp | Basic auth | mcp:langfuse |
langfuse-us | Basic auth | mcp:langfuse |
linear | OAuth | oauth:linear |
notion | OAuth | oauth:notion |
posthog | OAuth | oauth:posthog |
sentry | OAuth | oauth:sentry |
stripe | OAuth | oauth:stripe |
For Langfuse, pick the entry matching your project’s region. All four share the
mcp:langfuse grant. See the MCP guide for the
Basic auth credential format.
mcp[].policy
Keep policy rules for this MCP server. Controls which tool calls are allowed, denied, or redacted.
Accepts three formats:
- Starter pack name: A built-in policy (e.g.,
linear-readonly) - File path: Path to a Keep rules YAML file (e.g.,
.keep/linear.yaml) - Inline rules: An object with
denyand optionalmodefields
# Starter pack
mcp:
- name: linear
url: https://mcp.linear.app/mcp
policy: linear-readonly
# File reference
mcp:
- name: linear
url: https://mcp.linear.app/mcp
policy: .keep/linear.yaml
# Inline rules
mcp:
- name: linear
url: https://mcp.linear.app/mcp
policy:
deny: [delete_issue, update_issue]
mode: enforce
- Type:
stringorobject - Default: none (no policy enforcement)
Available starter packs: linear-readonly.
Listed operations are denied; unlisted operations are implicitly allowed.
Set mode: audit to log policy decisions without enforcing them.
See also: MCP servers: Policy enforcement
Codex
codex.sync_logs
Mount Codex’s log directory for observability.
codex:
sync_logs: true
- Type:
boolean - Default:
true(whenopenaigrant is used)
When enabled, Codex session logs are synced to the host at ~/.moat/runs/<run-id>/codex/.
codex.mcp
Sandbox-local MCP servers that run as child processes inside the container. Configuration is written to .mcp.json in the workspace directory.
codex:
mcp:
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
env:
VAR: value
grant: openai
cwd: /workspace
- Type:
map[string]object - Default:
{}
MCP server fields
| Field | Type | Description |
|---|---|---|
command | string | Server executable path (required) |
args | array[string] | Command arguments |
env | map[string]string | Environment variables |
grant | string | Credential to inject as an environment variable |
cwd | string | Working directory for the server process |
When grant is specified, the corresponding environment variable is set automatically:
| Grant | Environment variable |
|---|---|
github | GITHUB_TOKEN |
openai | OPENAI_API_KEY |
anthropic | ANTHROPIC_API_KEY |
gemini | GEMINI_API_KEY |
Note: For remote HTTP-based MCP servers, use the top-level mcp: field instead. See MCP servers guide.
GitHub Copilot CLI
copilot.model
Model to pass to copilot --model.
copilot:
model: gpt-5.4
- Type:
string - Default: Copilot CLI default
- CLI override:
moat copilot --model
copilot.context
Context window tier to pass to copilot --context.
copilot:
context: long_context
- Type:
string - Values:
default,long_context - Default: Copilot CLI default
- CLI override:
moat copilot --context
copilot.reasoning_effort
Reasoning effort level to pass to copilot --reasoning-effort.
copilot:
reasoning_effort: high
- Type:
string - Values:
none,minimal,low,medium,high,xhigh,max - Default: Copilot CLI default
- CLI override:
moat copilot --reasoning-effort
copilot.experimental
Start Copilot CLI with experimental features enabled.
copilot:
experimental: true
- Type:
boolean - Default:
false - CLI override:
moat copilot --experimental
copilot.autopilot
Start Copilot CLI in autopilot mode.
copilot:
autopilot: true
- Type:
boolean - Default:
false - CLI override:
moat copilot --autopilot
moat copilot uses the github grant. Run moat grant github before starting a session. See Running GitHub Copilot CLI.
Gemini
gemini.sync_logs
Mount Gemini’s session logs directory for observability.
gemini:
sync_logs: true
- Type:
boolean - Default:
true(whengeminigrant is used)
When enabled, Gemini session logs are synced to the host at ~/.moat/runs/<run-id>/gemini/.
gemini.mcp
Sandbox-local MCP servers that run as child processes inside the container. Configuration is written to .mcp.json in the workspace directory.
gemini:
mcp:
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
env:
API_KEY: my-key
grant: gemini
cwd: /workspace
- Type:
map[string]object - Default:
{}
MCP server fields
| Field | Type | Description |
|---|---|---|
command | string | Server executable path (required) |
args | array[string] | Command arguments |
env | map[string]string | Environment variables |
grant | string | Credential to inject as an environment variable |
cwd | string | Working directory for the server process |
When grant is specified, the corresponding environment variable is set automatically:
| Grant | Environment variable |
|---|---|
github | GITHUB_TOKEN |
openai | OPENAI_API_KEY |
anthropic | ANTHROPIC_API_KEY |
gemini | GEMINI_API_KEY |
Note: For remote HTTP-based MCP servers, use the top-level mcp: field instead. See MCP servers guide.
Pi
The Pi coding agent has no credential of its own — it runs against your anthropic or openai grant. Only those two backends are supported today.
pi.provider
Select the model backend Pi uses.
pi:
provider: anthropic
- Type:
string(anthropicoropenai) - Default: inferred from the single configured grant
When unset, the backend is inferred: if exactly one of the anthropic / openai grants is configured it is used; if both are configured, pi.provider (or the --provider flag) is required and the run fails hard otherwise. Any value other than anthropic or openai fails hard — other Pi backends are planned but not yet wired up.
pi.model
Pin the model Pi uses.
pi:
provider: anthropic
model: claude-opus-4-8
- Type:
string - Default:
""(Pi’s per-provider default is used)
pi.packages
Pi packages (extensions, skills, prompt templates, themes) to install into the container image at build time via pi install, so they are baked into a reproducible, cached layer.
pi:
packages:
- "npm:@acme/pi-reviewer@1.2.0"
- "git:github.com/acme/pi-skills@v3"
- Type:
array[string] - Default:
[]
Only remote sources are accepted — npm:, git:, https://, or ssh://. Local paths are rejected: pi install <path> records a relative path that does not resolve at runtime, so publish the package to npm or git instead. Pin an exact @version/@ref — Pi has no lockfile, so an unpinned spec is not reproducible. If a package needs a runtime tool, add it to top-level dependencies: (Pi does not install a package’s runtime deps for you). A bad source fails the image build with the offending entry named.
Baked safe defaults
For every moat pi image, Moat bakes a ~/.pi/agent/settings.json with safe defaults a workspace cannot override:
defaultProjectTrust: never— a checked-out repo’s own.pi/config and extensions (arbitrary code) do not auto-load. (AGENTS.md/CLAUDE.mdstill load — that is inherent to Pi.)enableInstallTelemetry: false,enableAnalytics: false,quietStartup: true.
httpProxy is not set here — Moat already injects HTTP_PROXY into the container. Note that Pi config (baseUrl, custom providers, extensions) can redirect model traffic to any host, so only the network policy actually constrains egress; moat pi prints a warning under a permissive policy. Use network.policy: strict for untrusted work.
Not yet supported: pi.mcp (Pi core has no MCP configuration format) and pi.sync_logs are planned follow-ups.
Snapshots
snapshots.disabled
Disable snapshots entirely.
snapshots:
disabled: true
- Type:
boolean - Default:
false - CLI override: none (config-only)
snapshots.triggers
Configure automatic snapshot triggers.
snapshots:
triggers:
disable_pre_run: false
disable_git_commits: false
disable_builds: false
disable_idle: false
idle_threshold_seconds: 30
| Field | Type | Default | Description |
|---|---|---|---|
disable_pre_run | boolean | false | Disable pre-run snapshot |
disable_git_commits | boolean | false | Disable git commit snapshots |
disable_builds | boolean | false | Disable build snapshots |
disable_idle | boolean | false | Disable idle snapshots |
idle_threshold_seconds | integer | 30 | Seconds before idle snapshot |
snapshots.exclude
Files to exclude from snapshots.
snapshots:
exclude:
ignore_gitignore: false
additional:
- node_modules/
- .git/
- "*.log"
| Field | Type | Default | Description |
|---|---|---|---|
ignore_gitignore | boolean | false | Respect .gitignore |
additional | array[string] | [] | Additional patterns |
snapshots.retention
Snapshot retention policy.
snapshots:
retention:
max_count: 10
delete_initial: false
| Field | Type | Default | Description |
|---|---|---|---|
max_count | integer | 10 | Maximum snapshots to keep |
delete_initial | boolean | false | Allow deleting pre-run snapshot |
Tracing
tracing.disable_exec
Disable execution tracing.
tracing:
disable_exec: true
- Type:
boolean - Default:
false
Network request logging is separate and always enabled.
Precedence
When the same option is specified in multiple places:
- CLI flags (highest priority)
moat.yamlvalues- Default values (lowest priority)
For additive options (--grant, -e, --mount), CLI values are merged with moat.yaml values.