Sandbox Backends
Moltis runs LLM-generated commands inside containers to protect your host system. The sandbox backend controls which container technology is used.
Backend Selection
Configure in moltis.toml:
[tools.exec.sandbox]
backend = "auto" # default — picks the best available
# backend = "podman" # force Podman (daemonless, rootless)
# backend = "docker" # force Docker
# backend = "apple-container" # force Apple Container (macOS only)
# backend = "wasm" # force WASM sandbox (Wasmtime + WASI)
# backend = "restricted-host" # env clearing + rlimits only
With "auto" (the default), Moltis picks the strongest available backend:
| Priority | Backend | Platform | Isolation |
|---|---|---|---|
| 1 | Apple Container | macOS | VM (Virtualization.framework) |
| 2 | Podman | any | Linux namespaces / cgroups (daemonless) |
| 3 | Docker | any | Linux namespaces / cgroups |
| 4 | Restricted Host | any | env clearing, rlimits (no filesystem isolation) |
| 5 | none (host) | any | no isolation |
The WASM backend (backend = "wasm") is not in the auto-detect chain because
it cannot execute arbitrary shell commands — use it explicitly when you want
WASI-isolated execution.
Apple Container (recommended on macOS)
Apple Container runs each sandbox in a lightweight virtual machine using Apple’s Virtualization.framework. Every container gets its own kernel, so a kernel exploit inside the sandbox cannot reach the host — unlike Docker, which shares the host kernel.
Install
Download the signed installer from GitHub:
# Download the installer package
gh release download --repo apple/container --pattern "container-installer-signed.pkg" --dir /tmp
# Install (requires admin)
sudo installer -pkg /tmp/container-installer-signed.pkg -target /
# First-time setup — downloads a default Linux kernel
container system start
Alternatively, build from source with brew install container (requires
Xcode 26+).
Verify
container --version
# Run a quick test
container run --rm ubuntu echo "hello from VM"
Once installed, restart moltis gateway — the startup banner will show
sandbox: apple-container backend.
Podman
Podman is a daemonless, rootless container engine that is CLI-compatible with Docker. It is preferred over Docker in auto-detection because it doesn’t require a background daemon process and runs rootless by default for better security.
Install
# macOS
brew install podman
podman machine init && podman machine start
# Debian/Ubuntu
sudo apt-get install -y podman
# Fedora/RHEL
sudo dnf install -y podman
Verify
podman --version
podman run --rm docker.io/library/ubuntu echo "hello from podman"
Once installed, restart moltis gateway — the startup banner will show
sandbox: podman backend. All Docker hardening flags (see below) apply
identically to Podman containers.
If Moltis runs under systemd, rootless Podman must be allowed to create user
namespaces and access the Moltis user’s container storage. For the bundled
system unit, install deploy/moltis-podman.conf as
/etc/systemd/system/moltis.service.d/podman.conf. It disables
NoNewPrivileges and places Podman’s home and runtime state outside the paths
hidden by ProtectHome. Without those accommodations, Podman can fail with
cannot clone: Operation not permitted and Error: cannot re-exec process even
when the same podman run ... command works from an interactive shell.
Moltis supports using Podman as the sandbox backend. Running podman run ...
from inside an already sandboxed Moltis container is different and is blocked by
the default hardened sandbox, which drops capabilities and sets
no-new-privileges inside the container.
Two mutually exclusive escape hatches are available when you intentionally want
container commands inside a Podman sandbox. Both require backend = "podman";
host socket passthrough is supported only on Linux:
[tools.exec.sandbox]
backend = "podman"
# Use the host Podman service from inside the sandbox.
# This mounts the host Podman socket and sets CONTAINER_HOST/DOCKER_HOST.
allow_host_podman = true
# The sandbox image must include a Podman client. This replaces default packages.
packages = ["ca-certificates", "curl", "podman"]
# Or run Podman nested inside the sandbox.
# This starts the sandbox with privileged Podman launch flags and disables
# the normal no-new-privileges/cap-drop/read-only hardening for that sandbox.
# allow_nested_podman = true
# packages = ["ca-certificates", "curl", "podman", "uidmap", "slirp4netns", "fuse-overlayfs"]
Before using allow_host_podman, start the Podman API socket. For a CLI-installed
user service, run systemctl --user enable --now podman.socket. For the bundled
system service, install deploy/moltis-podman-api.service as documented in
VPS Deployment. Moltis verifies that the configured path is a
live Unix socket and fails sandbox startup if it is unavailable. The mount also
disables SELinux label separation so the container can access the socket.
allow_host_podman removes the sandbox boundary: sandboxed commands can use the
API to create containers with host bind mounts and execute code with the Podman
service user’s file access. allow_nested_podman keeps container state inside
the sandbox, but it is runtime-dependent and weakens the sandbox substantially
by using privileged container settings.
Docker
Docker is supported on macOS, Linux, and Windows. On macOS it runs inside a Linux VM managed by Docker Desktop, so it is reasonably isolated but adds more overhead than Apple Container.
Install from https://docs.docker.com/get-docker/
Docker/Podman Hardening
Docker and Podman containers launched by Moltis include the following security hardening flags by default:
| Flag | Effect |
|---|---|
--cap-drop ALL | Drops all Linux capabilities |
--security-opt no-new-privileges | Prevents privilege escalation via setuid/setgid binaries |
--tmpfs /tmp:rw,nosuid,size=256m | Writable tmpfs for temp files (noexec on real root) |
--tmpfs /run:rw,nosuid,size=64m | Writable tmpfs for runtime files |
--read-only | Read-only root filesystem (prebuilt images only) |
--hostname sandbox | Prevents host hostname leakage |
--tmpfs /sys/firmware:ro,nosuid | Masks BIOS/UEFI firmware data (Docker only) |
--tmpfs /sys/class/dmi:ro,nosuid | Masks system serial numbers and identifiers (Docker only) |
--tmpfs /sys/devices/virtual/dmi:ro,nosuid | Masks DMI attributes (Docker only) |
--tmpfs /sys/class/block:ro,nosuid | Masks block device info (Docker only) |
The --read-only flag is applied only to prebuilt sandbox images (where
packages are already baked in). Non-prebuilt images need a writable root
filesystem for apt-get provisioning on first start.
When allow_nested_podman = true and the backend is Podman, Moltis starts the
sandbox with --privileged and does not apply --cap-drop ALL,
--security-opt no-new-privileges, or --read-only. This is required for
nested container runtimes but materially reduces the security boundary.
The /sys tmpfs overlays prevent host hardware metadata (serial numbers, disk
models, LUKS UUIDs) from being visible inside the container. Note that
tools.fs.deny_paths only restricts Moltis file-access tools — these kernel
filesystem masks prevent leakage via shell commands as well.
Podman note: The sysfs tmpfs overlays are applied on Docker only. Podman’s OCI runtime performs “tmpcopyup” when mounting tmpfs over sysfs paths, which fails under
--cap-drop ALLbecause some sysfs files are permission-denied even for root. Podman masks/sys/firmwarevia its built-in OCIMaskedPaths;/sys/class/dmi,/sys/devices/virtual/dmi, and/sys/class/blockremain readable inside the container on Podman.
WASM Sandbox (Wasmtime + WASI)
The WASM sandbox provides real sandboxed execution using Wasmtime with WASI. Commands execute in an isolated filesystem tree with fuel metering and epoch-based timeout enforcement.
How It Works
The WASM sandbox has two execution tiers:
Tier 1 — Built-in commands (~20 common coreutils implemented in Rust):
echo, cat, ls, mkdir, rm, cp, mv, pwd, env, head, tail,
wc, sort, touch, which, true, false, test/[, basename,
dirname.
These operate on a sandboxed directory tree, translating guest paths (e.g.
/home/sandbox/file.txt) to host paths under ~/.moltis/sandbox/wasm/<id>/.
Paths outside the sandbox root are rejected.
Basic shell features are supported: &&, ||, ; sequences, $VAR
expansion, quoting via shell-words, and > / >> output redirects.
Tier 2 — Real WASM module execution: When the command references a .wasm
file, it is loaded and run via Wasmtime + WASI preview1 with full isolation:
preopened directories, fuel metering, epoch interruption, and captured I/O.
Unknown commands return exit code 127: “command not found in WASM sandbox”.
Filesystem Isolation
~/.moltis/sandbox/wasm/<session-key>/
home/ preopened as /home/sandbox (rw)
tmp/ preopened as /tmp (rw)
Home persistence is respected:
shared: usesdata_dir()/sandbox/home/shared/wasm/session: usesdata_dir()/sandbox/wasm/<session-id>/off: per-session, cleaned up oncleanup()
Resource Limits
- Fuel metering:
store.set_fuel(fuel_limit)— limits WASM instruction count (Tier 2 only) - Epoch interruption: background thread ticks epochs, store traps on deadline (Tier 2 only)
- Memory:
wasm_config.memory_reservation(bytes)— Wasmtime memory limits (Tier 2 only)
Configuration
[tools.exec.sandbox]
backend = "wasm"
# WASM-specific settings
wasm_fuel_limit = 1000000000 # instruction fuel (default: 1 billion)
wasm_epoch_interval_ms = 100 # epoch interruption interval (default: 100ms)
[tools.exec.sandbox.resource_limits]
memory_limit = "512M" # Wasmtime memory reservation
Limitations
- Built-in commands cover common coreutils but not a full shell
- No pipe support yet (planned via busybox.wasm in future)
- No network access from WASM modules
.wasmmodules must target WASI preview1
When to Use
The WASM sandbox is a good fit when:
- You want filesystem-isolated execution without container overhead
- You need a sandboxed environment on platforms without Docker or Apple Container
- You are running
.wasmmodules and want fuel-metered, time-bounded execution
Compile-Time Feature
The WASM sandbox is gated behind the wasm cargo feature, which is enabled by
default. To build without Wasmtime (saves ~30 MB binary size):
cargo build --release --no-default-features --features lightweight
When the feature is disabled and the config requests backend = "wasm", Moltis
falls back to restricted-host with a warning.
Restricted Host Sandbox
The restricted-host sandbox provides lightweight isolation by running commands
on the host via sh -c with environment clearing and ulimit resource
wrappers. This is the fallback when no container runtime is available.
How It Works
When the restricted-host sandbox runs a command, it:
- Clears the environment — all inherited environment variables are removed
- Sets a restricted PATH — only
/usr/local/bin:/usr/bin:/bin - Sets HOME to
/tmp— prevents access to the user’s home directory - Applies resource limits via shell
ulimit:ulimit -u(max processes) frompids_maxconfig (default: 256)ulimit -n 1024(max open files)ulimit -t(CPU seconds) fromcpu_quotaconfig (default: 300s)ulimit -v(virtual memory) frommemory_limitconfig (default: 512M)
- Enforces a timeout via
tokio::time::timeout
User-specified environment variables from opts.env are re-applied after the
environment is cleared, so the LLM tool can still pass required variables.
Limitations
- No filesystem isolation — commands run on the host filesystem
- No network isolation — commands can make network requests
ulimitenforcement is best-effort- No image building —
moltis sandbox buildreturns immediately
For production use with untrusted workloads, prefer Apple Container or Docker.
No sandbox
If no runtime is found (and the wasm feature is disabled), commands execute
directly on the host. The startup banner will show a warning. This is not
recommended for untrusted workloads.
Failover Chain
Moltis wraps the primary sandbox backend with automatic failover:
- Apple Container → Docker → Restricted Host: if Apple Container enters a corrupted state (stale metadata, missing config, VM boot failure), Moltis fails over to Docker. If Docker is unavailable, it uses restricted-host.
- Docker → Restricted Host: if Docker loses its daemon connection during a session, Moltis fails over to the restricted-host sandbox.
Failover is sticky for the lifetime of the gateway process — once triggered, all subsequent commands use the fallback backend. Restart the gateway to retry the primary backend.
Failover triggers:
| Primary | Triggers |
|---|---|
| Apple Container | config.json missing, VM never booted, NSPOSIXErrorDomain Code=22, service errors |
| Docker | cannot connect to the docker daemon, connection refused, is the docker daemon running |
Per-session overrides
The web UI allows toggling sandboxing per session and selecting a custom container image. These overrides persist across gateway restarts.
Home persistence
By default, /home/sandbox is persisted in a shared host folder so that CLI
auth/config files survive container recreation. You can change this with
home_persistence:
[tools.exec.sandbox]
home_persistence = "session" # "off", "session", or "shared" (default)
# shared_home_dir = "/path/to/shared-home" # optional, used when mode is "shared"
off: no home mount, container home is ephemeralsession: mount a per-session host folder to/home/sandbox; folder names are opaque, collision-resistant identifiers rather than raw session keysshared: mount one shared host folder to/home/sandboxfor all sessions (defaults todata_dir()/sandbox/home/shared, orshared_home_dirif set)
Moltis stores persisted homes under data_dir()/sandbox/home/. Per-session
folder names are opaque, collision-resistant identifiers; do not derive or
depend on them directly. The shared mode is intentionally visible to every
sandbox session and is not a session-isolation boundary. Use session or off
for credentials or other private state when sessions have different trust
levels.
After upgrading from a version that used readable session folder names, gateway startup discards legacy instance containers. Legacy home folders remain on disk but are not automatically reused because multiple session keys may have mapped to the same folder. Administrators can inspect and migrate trusted content manually.
Managed Files mount
Managed Files use a separate mount from the Moltis workspace and sandbox home:
[tools.exec.sandbox]
managed_files_mount = "ro" # "none", "ro" (default), or "rw"
Docker, Podman, and Apple Container mount the host’s <data_dir>/files at
/home/sandbox/files. none makes that tree unavailable, ro permits reads,
and rw permits mutations. WASM and remote sandbox backends do not yet support
this mount.
Although /home/sandbox/files is below the home path, it is an independent
mount. home_persistence = "shared" does not enable Managed Files, and changing
managed_files_mount does not change how sandbox caches, credentials, or other
home files persist. Moltis advertises the effective path and mount mode to the
agent and sets MOLTIS_FILES_DIR for exec.
The default ro mode prevents a sandboxed command from changing Managed Files,
but every agent session whose tool and channel policy permits reads sees the
same tree. See the Managed Files privacy warning
before storing sensitive data.
Docker-in-Docker workspace mounts
When Moltis runs inside a container and launches Docker-backed sandboxes via a
mounted container socket, the sandbox bind mount source must be a host-visible
path. Moltis auto-detects this by inspecting the parent container’s mounts. If
that lookup fails or you want to pin the value explicitly, set
host_data_dir:
[tools.exec.sandbox]
host_data_dir = "/srv/moltis/data"
This remaps sandbox workspace mounts and default sandbox persistence paths from
the guest data_dir() to the host path you provide. It is mainly an override
for Docker-in-Docker deployments where mount auto-detection is unavailable or
ambiguous.
Network policy
By default, sandbox containers have no network access (no_network = true).
For tasks that need filtered internet access, use
trusted network mode — a proxy-based allowlist that
lets containers reach approved domains while blocking everything else.
[tools.exec.sandbox]
network = "trusted"
trusted_domains = ["registry.npmjs.org", "github.com"]
See Trusted Network for full configuration and the network audit log.
Note: Home persistence applies to Docker, Apple Container, and WASM backends. The restricted-host backend uses
HOME=/tmpand does not mount persistent storage.
Resource limits
[tools.exec.sandbox.resource_limits]
memory_limit = "512M"
cpu_quota = 1.0
pids_max = 256
How resource limits are applied depends on the backend:
| Limit | Docker | Apple Container | WASM | Restricted Host | cgroup (Linux) |
|---|---|---|---|---|---|
memory_limit | --memory | --memory | Wasmtime reservation | ulimit -v | MemoryMax= |
cpu_quota | --cpus | --cpus | epoch timeout | ulimit -t (seconds) | CPUQuota= |
pids_max | --pids-limit | --ulimit nproc= | n/a | ulimit -u | TasksMax= |
Apple Container requires version 0.9 or newer to apply pids_max.
Its --cpus option only accepts positive whole CPU counts, so fractional
cpu_quota values are not supported by that backend.
Comparison
| Feature | Apple Container | Docker | WASM | Restricted Host | none |
|---|---|---|---|---|---|
| Filesystem isolation | ✅ VM boundary | ✅ namespaces | ✅ sandboxed tree | ❌ host FS | ❌ |
| Network isolation | ✅ | ✅ | ✅ (no network) | ❌ | ❌ |
| Kernel isolation | ✅ separate kernel | ❌ shared kernel | ✅ WASM VM | ❌ | ❌ |
| Environment isolation | ✅ | ✅ | ✅ | ✅ cleared + restricted | ❌ |
| Resource limits | ✅ | ✅ | ✅ fuel + epoch | ✅ ulimit | ❌ |
| Image building | ✅ (via Docker) | ✅ | ❌ | ❌ | ❌ |
| Shell commands | ✅ full shell | ✅ full shell | ~20 built-ins | ✅ full shell | ✅ full shell |
| Managed Files mount | yes: none/ro/rw | yes: none/ro/rw | not yet | host path, no mount | host path, no mount |
| Platform | macOS 26+ | any | any | any | any |
| Overhead | low | medium | minimal | minimal | none |