Isolated VM-based development environments powered by Multipass. Spin up full Linux VMs with Docker, language runtimes, and dev tools pre-configured — stronger isolation than containers alone.
Quick Start | Requirements | Installation | Commands | Auto-Naming | Mounting | File Transfer | Configuration | Profiles | Cloud-init Templates | Image Flavors | SSH & Port Forwarding | Pre-built Images | Development
# Install
./install.sh
# Create and start a sandbox (auto-names from CWD, mounts current directory)
mps up
# Or seed it with your Claude config
mps up --transfer ~/.claude:/home/ubuntu/.claude
# Open a shell (auto-resolves sandbox from CWD)
mps shell
# Run a command
mps exec -- docker ps
# List sandboxes
mps list
# Stop (prevents auto-restart on host reboot)
mps down
# Destroy
mps destroy --forceNote: Multipass automatically restarts running VMs on host reboot. Use mps down to stop sandboxes you don't want restarting.
# Install (symlinks bin/mps to ~/.local/bin/)
./install.sh
# Or via make
make installThe installer checks for multipass and jq, creates ~/mps/ directories, symlinks mps onto your PATH, and offers to update your shell profile if needed. Override the install directory with MPS_INSTALL_DIR.
Snap Confinement (Ubuntu): Multipass installed via snap cannot access hidden directories (dotdirs) directly under
$HOME— this is an AppArmor restriction of the snaphomeinterface. MPS uses~/mps/(not~/.mps/) to avoid this. If you provide paths under a hidden directory (e.g.,~/.secret/projectas a mount source, transfer path, or cloud-init file), MPS will detect active snap confinement and refuse the operation with a clear error. Workaround: move files to a non-hidden path or copy them to a staging directory.
# Remove symlink, VMs, caches, and configs
./uninstall.sh
# Or via make
make uninstall| Command | Description |
|---|---|
mps create [path] [flags] |
Create a new sandbox |
mps up [path] [flags] |
Create (if needed) and start a sandbox; restores mounts and port forwards on restart |
mps down [-f] [-n <name>] |
Stop a sandbox; cleans up port forwards and session-only mounts |
mps destroy [-f] [-n <name>] |
Remove a sandbox permanently (--force skips confirmation) |
mps shell [-n <name>] [-w <path>] |
Open an interactive shell |
mps exec [-n <name>] [-w <path>] -- <cmd> |
Execute a command in a sandbox |
mps transfer [-n <name>] <src...> <dst> |
Transfer files or directories between host and sandbox |
mps list [--json] |
List all sandboxes |
mps status [-n <name>] [--json] |
Show detailed status (resources, image staleness, mounts, Docker) |
mps ssh-config [-n <name>] |
Configure SSH access (inject key, generate config) |
mps image <list|pull|import|remove> |
Manage sandbox images |
mps mount <add|remove|list> |
Manage mounts at runtime (origin tracking: auto/config/adhoc) |
mps port <forward|list> |
Manage port forwarding |
Common flags across commands: -n (--name), -f (--force), -w (--workdir), --mem (--memory). create/up also accept --transfer <src:dst> (repeatable). Run mps <command> --help for detailed usage on any command.
Sandboxes are automatically named based on your project directory and cloud-init template:
<folder>-<template>
For example, running mps up from ~/projects/myapp produces myapp-default.
- Override with
--name <name>flag orMPS_NAMEin.mps.env - Long names are truncated with a short hash suffix for uniqueness
- Commands that operate on existing instances auto-resolve the name from CWD
mps up # Auto-named from CWD
mps up --name mydev # Explicit name
mps shell # Auto-resolves sandbox for CWD
mps shell --name mydev # Explicit nameBy default, mps mounts your current working directory into the VM at the same absolute path:
# On host: /home/user/projects/myapp
mps up
# Inside VM: cd /home/user/projects/myapp — same files!mps up ~/code/project # Mount specific directory instead of CWD
mps create --no-mount --name scratch # No automatic mount (requires --name)
mps create --mount ./data:/home/ubuntu/data # Extra mount (repeatable)Extra mounts from MPS_MOUNTS in .mps.env are additive (on top of the auto-mount). Set MPS_NO_AUTOMOUNT=true to disable the CWD auto-mount.
Add or remove mounts on a running sandbox with mps mount. Each mount is tracked by origin:
- auto — the CWD auto-mount (from
mps up) - config — persistent mounts from
MPS_MOUNTSin.mps.env - adhoc — session-only mounts added at runtime (removed automatically on
mps down)
mps mount add ./data:/home/ubuntu/data # Add a session-only mount
mps mount list # Show mounts with origin
mps mount remove /home/ubuntu/data # UnmountPersistent mounts (auto and config) are automatically restored when restarting a stopped sandbox with mps up.
Transfer files or directories between host and sandbox using the : prefix convention for guest paths:
# Host -> guest
mps transfer ./config.json :/home/ubuntu/config.json
mps transfer file1.txt file2.txt :/home/ubuntu/
# Guest -> host
mps transfer :/home/ubuntu/output.log ./output.log
# With explicit sandbox name
mps transfer --name mydev ./script.sh :/tmp/script.sh
# Seed files during creation
mps create --transfer ./setup.sh:/home/ubuntu/setup.shConfiguration is loaded in cascade (later values win):
config/defaults.env— shipped defaults~/mps/config— user global overrides.mps.env— per-project (in your repo)- Profile — resource fractions from
templates/profiles/<name>.env - Auto-scaling — vCPU/memory computed from host hardware fractions
- CLI flags — highest priority
MPS_NAME=myproject-dev
MPS_CPUS=8
MPS_MEMORY=16G
MPS_DISK=100G
MPS_PROFILE=heavy
MPS_PORTS="8899:8899 8900:8900 3000:3000"
MPS_MOUNTS="./data:~/extra-data"
MPS_NO_AUTOMOUNT=false| Variable | Default | Description |
|---|---|---|
MPS_NAME |
(auto) | Override auto-generated sandbox name |
MPS_IMAGE |
Per-project image override (takes precedence over MPS_DEFAULT_IMAGE) |
|
MPS_DEFAULT_IMAGE |
base |
Default image (base, base:1.0.0, or Ubuntu version like 24.04) |
MPS_PROFILE |
Per-project profile override (takes precedence over MPS_DEFAULT_PROFILE) |
|
MPS_DEFAULT_PROFILE |
lite |
Default resource profile (micro, lite, standard, heavy) |
MPS_CLOUD_INIT |
Per-project cloud-init override (takes precedence over MPS_DEFAULT_CLOUD_INIT) |
|
MPS_DEFAULT_CLOUD_INIT |
default |
Cloud-init template name or file path |
MPS_CPUS |
(from profile) | vCPUs (host threads) |
MPS_MEMORY |
(from profile) | Memory with unit (e.g., 4G) |
MPS_DISK |
(from profile) | Disk with unit (e.g., 40G) |
MPS_PORTS |
(empty) | Space-separated host:guest port pairs, auto-forwarded on create/up |
MPS_MOUNTS |
(empty) | Extra mounts (src:dst), additive on top of auto-mount |
MPS_NO_AUTOMOUNT |
false |
Disable automatic CWD mount |
MPS_SSH_KEY |
(auto-detect) | SSH key path (auto-detect: ed25519 > ecdsa > rsa) |
MPS_IMAGE_BASE_URL |
https://mpsandbox.horizenlabs.io |
Image registry URL |
MPS_IMAGE_CHECK_UPDATES |
true |
Check for image and instance staleness updates |
MPS_INSTANCE_PREFIX |
mps |
Prefix for auto-generated instance names |
MPS_CHECK_UPDATES |
true |
Check for CLI version updates (at most once per 24h) |
MPS_DEBUG |
false |
Enable debug logging (--debug flag) |
Profiles define resource allocation as fractions of host hardware, with minimum floors and maximum caps. A lite profile on a 16-thread machine allocates more resources than on a 4-thread machine.
| Profile | vCPU Fraction | vCPU Min | Mem Fraction | Mem Min | Mem Cap | Disk |
|---|---|---|---|---|---|---|
micro |
1/8 | 1 | 1/16 | 1G | 2G | 10G |
lite (default) |
1/4 | 2 | 1/6 | 2G | 8G | 20G |
standard |
1/3 | 4 | 1/4 | 4G | 16G | 40G |
heavy |
1/2 | 6 | 1/3 | 6G | 64G | 75G |
Example resolved values on a 16-thread / 64GB host:
| Profile | vCPUs | Memory | Disk |
|---|---|---|---|
micro |
2 | 4G | 10G |
lite |
4 | 8G | 20G |
standard |
5 | 16G | 40G |
heavy |
8 | 21G | 75G |
Disk sizes are upper limits — Multipass creates thin-provisioned (sparse) virtual disks that start at the size of the source image and grow on demand up to the configured maximum.
mps create --profile heavy
mps create --profile lite --cpus 4 --memory 4G # Profile + overridesCloud-init templates customize VMs at launch time, on top of pre-built images. This is how you install extra packages, enable plugins, write config files, or run setup scripts without rebuilding an image.
Sandboxes are designed to be disposable — image staleness checks will regularly flag them for rebuild to pick up security patches and tool updates. Rather than manually installing dependencies inside a running sandbox (e.g., mps shell then apt-get install ...), define your project's setup in a cloud-init template or .mps.env. This way, mps destroy && mps up always gives you a fresh, correctly configured environment — and teammates get the same setup automatically.
# Use the default template (enabled plugins, commented-out examples)
mps create
# Use a named template from templates/cloud-init/ or ~/mps/cloud-init/
mps create --cloud-init mytemplate
# Use any file path directly
mps create --cloud-init ./my-cloud-init.yaml
# Set a per-project default in .mps.env (name flows into auto-naming)
MPS_CLOUD_INIT=.mps/dev.yaml
# Set a personal default in ~/mps/config
MPS_DEFAULT_CLOUD_INIT=personalNamed templates are resolved in order: templates/cloud-init/ (project), then ~/mps/cloud-init/ (personal).
The shipped default template (templates/cloud-init/default.yaml) enables HorizenLabs Claude Code marketplace plugins (hl-product-ideation, zkverify-product-development, context-utils) and includes commented-out examples for:
- Packages: Install additional apt packages (
packages:block) - Run commands: Execute scripts on first boot (
runcmd:block) - Write files: Drop config files into the VM (
write_files:block) - Hostname / timezone: Set VM hostname and timezone
- Claude Code plugins: Commented-out examples for Trail of Bits, GSD, SuperClaude, Superpowers, BMAD, GitHub Spec Kit (uncomment to enable)
Create a #cloud-config YAML file and place it in one of these locations:
- Project-shared (
<project>/.mps/<name>.yaml): Checked into git, shared by the team. SetMPS_CLOUD_INIT=.mps/<name>.yamlin.mps.env. Use a descriptive name — it flows into auto-naming (e.g.,dev.yaml→myproject-dev). The.mps/directory keeps MPS config out of the project root. - Personal (
~/mps/cloud-init/<name>.yaml): Personal defaults, not in any repo. Reference by name (e.g.,--cloud-init personal) or setMPS_DEFAULT_CLOUD_INIT=<name>in~/mps/config. - MPS built-in (
templates/cloud-init/<name>.yaml): For templates shipped with MPS itself. - Any file path (e.g.,
--cloud-init ~/configs/dev-setup.yaml)
#cloud-config
packages:
- postgresql-client
- redis-tools
runcmd:
- echo "Hello from cloud-init" > /tmp/hello.txt
- sudo -u ubuntu bash -c 'pip install my-tool'
write_files:
- path: /home/ubuntu/.env
content: |
DATABASE_URL=postgres://localhost/mydb
owner: ubuntu:ubuntu
permissions: '0600'
timezone: America/New_YorkThese templates run on top of whatever image you're using. For the image build-time layers (what packages come pre-installed), see images/layers/*.yaml.
The /init-template skill guides you through creating a cloud-init template interactively. It reads the default template, knows what's pre-installed in each image flavor, and generates valid YAML with the right config wiring.
# In Claude Code (from the mps repo), run:
/init-templateThe skill walks you through: scope (personal vs project), target image flavor, which plugins/frameworks to enable, extra packages, custom commands, and sandbox settings (.mps.env). It writes the template and config files for you.
Pre-built images come in four flavors. Each builds on the previous, adding specialized tooling:
| Flavor | Builds On | Description | Min Profile |
|---|---|---|---|
base |
— | Ubuntu 24.04 + Docker + Node.js + Python + dev tools + AI assistants | micro |
protocol-dev |
base | + C/C++ toolchain + Go + Rust | lite |
smart-contract-dev |
protocol-dev | + Solana/Anchor (amd64) + Foundry + Hardhat | lite |
smart-contract-audit |
smart-contract-dev | + Slither + Mythril (amd64) + Echidna + Medusa + Halmos (amd64) | standard |
What's in each flavor
base: Docker (CE + Compose + Buildx), Node.js (LTS via nvm), pnpm, yarn, Bun, Python 3 + uv, git, curl, jq, yq, tmux, ripgrep, fd, Neovim, shellcheck, hadolint, zsh. AI assistants: Claude Code, Crush, OpenCode, Gemini CLI, Codex CLI.
protocol-dev adds: build-essential, clang, LLVM, lld, cmake, Go (latest stable), Rust (stable via rustup + cargo-audit), protobuf, SSL/crypto dev libraries.
smart-contract-dev adds: Solana CLI + Anchor (amd64-only), Foundry (forge, cast, anvil, chisel), Hardhat, Solhint.
smart-contract-audit adds: Slither, solc-select, Mythril + Halmos (amd64-only, via uv), Aderyn, Echidna (sigstore-verified), Medusa, cosign.
Images warn (but do not block) when your profile is below the minimum. For example, launching smart-contract-audit with micro triggers a warning since it requires standard.
mps create --image protocol-dev --profile standard
mps create --image smart-contract-audit --profile heavySSH and port forwarding require explicit setup — by design, mps does not automatically inject SSH keys or open tunnels.
Configure SSH access with mps ssh-config. This resolves your SSH key, injects it into the VM, and generates an SSH config entry — no sudo required.
# Auto-detect key, inject, print config to stdout
mps ssh-config
# Use a specific key
mps ssh-config --ssh-key ~/.ssh/id_ed25519
# Write config to ~/.ssh/config.d/ (for VS Code)
mps ssh-config --appendSSH key resolution order: --ssh-key flag > MPS_SSH_KEY config > auto-detect from ~/.ssh/ (ed25519 > ecdsa > rsa).
The primary use case for mps ssh-config is connecting to sandboxes from VS Code via the Remote-SSH extension.
# 1. Write SSH config for your sandbox
mps ssh-config --append
# 2. Ensure ~/.ssh/config includes the config.d directory
# Add this line at the TOP of the file (before any Host blocks):
# Include config.d/*
# 3. In VS Code: Cmd/Ctrl+Shift+P -> "Remote-SSH: Connect to Host" -> select <name>The --append flag writes to ~/.ssh/config.d/<instance-name>, which VS Code picks up automatically when your ~/.ssh/config includes config.d/*. Use --print (the default) if you prefer to manage SSH config manually.
Ports are forwarded via SSH tunnels, so mps ssh-config must be run first. The --port flag on create stores forwarding rules in instance metadata but does not activate them until SSH is configured.
# 1. Create with port rules (stored, not yet active)
mps create --port 3000:3000 --port 8080:8080
# 2. Configure SSH (required before any forwarding)
mps ssh-config
# 3. Forward ports (uses stored rules, or specify manually)
mps port forward mydev 3000:3000
# List active forwards
mps port list
# Auto-forward via .mps.env (activated on next mps up after ssh-config)
# MPS_PORTS="8899:8899 3000:3000"Forwards bind to localhost only — they are not exposed on external network interfaces. Privileged ports (< 1024) require the --privileged flag, which elevates the SSH tunnel via sudo:
mps port forward --privileged mydev 80:80Note: Auto-forwarding (via
MPS_PORTSor--portmetadata) skips privileged ports for safety — it never triggers asudoprompt automatically. If you have privileged ports configured,mps upwill warn and print the exactmps port forward --privilegedcommand to run manually.
Ports are automatically cleaned up on mps down and mps destroy.
Pre-built QCOW2 images come with tools pre-installed, so cloud-init has far less to do at startup. Images are distributed via Backblaze B2 with Cloudflare proxy, versioned with SemVer, and verified with SHA256 checksums.
# Browse cached images (shows update status)
mps image list
# Browse remote registry
mps image list --remote
# Pull an image (auto-detects host architecture)
mps image pull base
mps image pull base:1.0.0
mps image pull base --force # Re-download even if up to date
# Import a locally built image
mps image import images/artifacts/mps-base-amd64.qcow2.img
# Remove cached images
mps image remove base:1.0.0
mps image remove --allImages are checked for updates automatically on mps create and mps up. Running sandboxes are also checked for staleness (rebuilt image pulled, or newer version available locally) on mps up, mps shell, mps exec, mps status, mps transfer, and mps ssh-config. Disable all image/instance update checks with MPS_IMAGE_CHECK_UPDATES=false.
Image builds run inside Docker via Packer + QEMU. Flavors chain from their parent — building smart-contract-audit automatically builds the full chain.
make image-base # Both architectures (parallel)
make image-base-amd64 # Single architecture
make image-protocol-dev # Chains from base
make image-smart-contract-dev # Chains from protocol-dev
make image-smart-contract-audit # Full chain: base → protocol-dev → sc-dev → sc-audit
make import-base # Import host-arch image into local mps cacheAll build, lint, and test commands run inside Docker containers for reproducibility. The Makefile auto-builds the container images when their Dockerfiles change.
# Docker images (auto-built on first use)
make build-docker-linter # Linter/test image
make build-docker-builder # Builder image (Packer, QEMU)
make build-docker-publisher # Publisher image (b2, jq, yq)
# Lint and test
make lint # Run all linters
make lint-actions # Lint GitHub Actions workflows
make test # Run BATS tests
# Publish (requires B2_APPLICATION_KEY_ID and B2_APPLICATION_KEY)
make publish-base VERSION=1.0.0 # Upload + manifest (both archs)
make upload-base-amd64 VERSION=1.0.0 # CI: upload only (no manifest)
make update-manifest VERSION=1.0.0 # CI fan-in: single manifest write
make help # Show all targets| File type | Linter |
|---|---|
| Bash | shellcheck |
| Bash 3.2 compat | lint-bash32-compat.sh |
| PowerShell | py-psscriptanalyzer |
| Dockerfile | hadolint |
| Makefile | checkmake |
| YAML | yamllint |
| HCL/Packer | packer fmt |
| GitHub Actions | actionlint |
Licensed under the Business Source License 1.1. You may use, copy, modify, and redistribute the Licensed Work for non-production purposes. Production use is permitted provided you do not offer it as a competitive hosted or embedded product. After four years from each release, the license converts to Apache 2.0.
See LICENSE for full terms. Third-party copyleft notices are in NOTICES. For alternative licensing, contact devops@horizenlabs.io.
