Volume-mode workspaces
By default, Moat bind-mounts your host directory at /workspace inside the container. In bind mode, the agent reads and writes your actual files — useful for most workflows, but it means the agent can overwrite or delete anything in the directory.
Volume mode copies the working tree into an ephemeral Docker named volume before the run starts. The container operates on the copy; your host directory is never written to during the run.
When to use volume mode
Volume mode suits two scenarios:
- Host protection — you want to inspect or approve changes before they land on your host (e.g., exploratory refactors, auto-generated migrations).
- macOS I/O performance — Docker Desktop’s VirtioFS bind-mount layer adds overhead on read-heavy workloads. A named volume uses Docker’s native storage and avoids that overhead.
If neither applies, bind mode is the right default.
Enabling volume mode
In moat.yaml
workspace:
mode: volume
Per-run with a flag
moat claude --workspace-mode volume
moat run --workspace-mode volume -- npm test
The flag overrides moat.yaml. Pass --workspace-mode bind to override a volume setting in moat.yaml for a single run.
What gets copied in
The full working tree is copied into the volume, except paths listed in the /workspace mount’s exclude: field:
workspace:
mode: volume
mounts:
- source: .
target: /workspace
exclude:
- node_modules
- .venv
- dist
Paths in exclude: are not copied into the volume. Use this to skip large directories that don’t need to be there (build artifacts, dependency caches).
Exclude patterns may contain letters, digits, /, ., -, _, and *. Other characters are rejected.
.git cannot be excluded. The copy-in always transfers the full .git directory because a partial .git is a broken repository.
Constraints
- Docker only. Volume mode requires the Docker runtime. The Apple container runtime does not support it; runs fail with a clear error. Pass
--runtime dockerif you need to force Docker on macOS. - Git worktrees and submodules are rejected. When
.gitis a file rather than a directory (the case ingit worktreecheckouts and submodules), volume mode fails. Run from the main checkout or useworkspace.mode: bind.
If any of these apply, use workspace.mode: bind (the default) instead.
A mounts: entry targeting /workspace is still allowed in volume mode — it is consulted only for its exclude: list (the named volume always provides /workspace, so no second mount is created).
Extracting changes
The Docker named volume is the only copy of the agent’s work. To get changes back to your host, capture a snapshot and extract it before destroying the run.
Step 1: capture a snapshot
moat snapshot <run>
For volume-mode runs, moat snapshot reads /workspace from the volume rather than the host directory. The snapshot includes .git, so commits the agent made inside the container are preserved.
Caution: snapshot archives include
.git, which may contain credentials in git history or config (e.g., tokens embedded in remote URLs). Snapshot archives are written with mode0600(owner-read only). Store and transmit them accordingly.
Step 2: extract to a directory
moat snapshot restore <run> --to ~/output
In-place restore is blocked for volume-mode runs (it would write back to the host, which is the opposite of what volume mode provides). You must use --to.
Step 3: bring commits into your repository
git -C ~/myrepo fetch ~/output
git -C ~/myrepo merge FETCH_HEAD
Or inspect the diff first:
git -C ~/myrepo diff HEAD ~/output
Destroying a volume-mode run
moat destroy refuses to destroy a volume-mode run that has no extraction snapshot, because doing so would permanently delete the agent’s work:
this volume-mode run has no extraction snapshot; destroying it deletes the workspace volume and loses all agent changes.
Capture your work first: `moat snapshot <run>` then `moat snapshot restore <run> --to <dir>`, or pass --force to destroy anyway
Capture a snapshot first, or pass --force to discard the volume:
moat destroy --force run_a1b2c3d4e5f6
moat clean applies the same guard: it skips un-extracted volume-mode runs (even with -f/--force, which only suppresses the confirmation prompt) and exits non-zero when it does, so scripts can detect leftover work. Pass --force-volumes to remove them too.
Volume lifecycle
The volume is named moat-ws-<run-id> and is removed when the run is destroyed. Volumes left behind by crashed runs are reclaimed automatically when the proxy daemon’s idle timer fires (after 5 minutes with no active runs).
Example: exploratory refactor
The following moat.yaml runs Claude Code in volume mode, excluding node_modules from the copy-in:
name: refactor
grants:
- anthropic
- github
workspace:
mode: volume
mounts:
- source: .
target: /workspace
exclude:
- node_modules
Start the session:
moat claude
When Claude finishes, capture the result:
moat snapshot <run>
moat snapshot restore <run> --to ~/refactor-output
git -C ~/myrepo fetch ~/refactor-output
git -C ~/myrepo log FETCH_HEAD --oneline
Review the commits and merge if satisfied.
Related
- moat.yaml reference: workspace —
workspace.modefield - moat.yaml reference: mounts —
exclude:on mounts - Workspace snapshots — snapshot triggers, retention, and pruning
- Git worktrees — parallel branches in bind mode