Launch AI agents in a secure, disposable Linux runtime (Lima or Docker) — with one command.
aivm.mp4
aivm manages the full lifecycle of a disposable Linux runtime dedicated to running AI coding agents. It runs on Lima by default or Docker via the Docker backend, bootstraps the runtime with your choice of toolchain plugins, optionally starts Docker Compose services (e.g. MCP servers) tied to the VM lifecycle, keeps sessions alive while you work, and auto-cleans up when you're idle.
Supported agents: Claude Code (claude) · GitHub Copilot (copilot) ·
Cursor Agent (cursor) · OpenCode (opencode)
- Requirements
- Installation
- Quick Start
- Configuration
- Plugins
- Commands
- Tool integration
- Agents
- Building from Source
- Contributing
- License
- macOS (Intel or Apple Silicon) — Linux/Windows is not supported
- Lima for the default
limabackend (brew install lima), or Docker Engine/Desktop for thedockerbackend - Authentication for your chosen agent is handled inside the VM after first launch
curl -fsSL https://raw.githubusercontent.com/sisimomo/aivm/main/install.sh | shThis downloads the latest release binary for your architecture (darwin/amd64
or darwin/arm64) and installs it to ~/.local/bin/aivm (override with
AIVM_INSTALL_DIR). Copy aivm.example.yaml to
~/.aivm/aivm.yaml on first use, or run make install from source to create
it automatically.
git clone https://github.com/sisimomo/aivm.git
cd aivm
make installVerify:
aivm version-
Edit your config:
nano ~/.aivm/aivm.yamlSet
agents.enabledand optionallyagents.default. Keepvm.backend: limafor the default setup, or setvm.backend: dockerandvm.docker_imageto run on Docker directly. -
Launch an agent from any directory under your dev root:
cd ~/dev/my-project aivm
On the first run, aivm starts the VM, bootstraps all plugins, and drops you into an interactive agent session. Subsequent runs skip the bootstrap (idempotent).
Config file: ~/.aivm/aivm.yaml
Use aivm.example.yaml as a reference.
# Which built-in agent to launch (claude | copilot | cursor | opencode)
agents:
enabled:
- claude
vm:
cpus: 4
memory: "8GB"
disk: "60GB"
backend: lima
name: aivm
mounts:
- source: "~/dev"
target: "{{ .host_home }}/dev"
mode: rw
- source: "~/.ssh"
target: "~/.ssh"
mode: ro
# session_env:
# MY_TOOL_SESSION_ID: "${MY_TOOL_SESSION_ID}"
# CI_JOB_ID: "${CI_JOB_ID}"
idle:
stop_timeout: 5m
delete_timeout: 5m
# aivm CLI terminal verbosity (trace | debug | info | warn | error)
log_level: info # all levels are always written to logs/aivm.logOverride at runtime with --log-level or AIVM_LOG_LEVEL (see
Commands).
At the default info level the terminal shows milestones and warnings; use
debug for operational detail or trace for subprocess output. The log file at
~/.aivm/logs/aivm.log always records trace and above.
| Key | Default | Description |
|---|---|---|
vm.cpus |
4 |
Number of vCPUs |
vm.memory |
"8GB" |
RAM (supports MB, GB) |
vm.disk |
"60GB" |
Disk size (supports MB, GB, TB) |
| Key | Default | Description |
|---|---|---|
vm.backend |
lima |
VM runtime: lima or docker |
vm.name |
aivm |
VM identity (Lima instance / Docker container name) |
vm.docker_image |
for docker |
Base image for docker backend |
Directories listed under vm.mounts are bind-mounted into the VM. Each entry
is a structured MountSpec with three required fields:
| Field | Description |
|---|---|
source |
Host path to bind |
target |
VM path inside the VM |
mode |
rw (read-write) or ro (read-only) |
Paths support {{ .host_home }} and {{ .state_dir }} templates (same engine
as plugin setup scripts). After template rendering, a leading ~ expands to the
host home in source and the VM user home in target. Both paths must
be absolute before the mount is accepted.
Same-path mount — host and VM paths resolve to the same logical location.
On Lima/macOS, ~/… in source and target expands to different absolute paths
(host /Users/you/… vs VM /home/you.guest/…), so use {{ .host_home }} in
target to keep paths aligned (recommended on macOS):
vm:
mounts:
- source: "~/dev"
target: "{{ .host_home }}/dev"
mode: rwRemapped mount — host path differs from VM path:
vm:
mounts:
- source: "{{ .host_home }}/company-secrets"
target: "/secrets"
mode: roWhen source and target differ, only the VM path is visible inside
the VM. aivm ssh and aivm (agent launch) translate your host current working
directory to the matching VM path automatically. aivm cp vm:/path is
unchanged — VM paths stay explicit via the vm: prefix.
vm.session_env maps environment variable names to values, using the same
${HOST_VAR} expansion as vm.env. On each agent or shell session (aivm,
aivm agent -- …, or aivm ssh), values are resolved from the invoking host
process and exported inside the VM for that session only.
Use this for context that exists in the terminal where you run aivm — session IDs, job metadata, tool-specific identifiers — without hardcoding values in config or the VM image.
Unlike persistent vm.env (applied at bootstrap/sync and shared by every login
shell), vm.session_env is resolved fresh per session and is not written into
the VM.
vm:
session_env:
MY_TOOL_SESSION_ID: "${MY_TOOL_SESSION_ID}"
CI_JOB_ID: "${CI_JOB_ID}"Every configured key is exported each session. Missing or empty host references expand to empty strings.
vm.env injects environment variables into the VM on bootstrap and on config
sync. Values support ${HOST_VAR} expansion from the host shell. Unlike
vm.session_env, these are persisted in the VM and shared by every shell
session.
vm:
env:
CONTEXT7_API_KEY: "${CONTEXT7_API_KEY}"
MY_API_TOKEN: "${MY_API_TOKEN}"Changes to vm.env are applied in place on the next aivm run — no VM
recreation required.
| Key | Default | Description |
|---|---|---|
vm.type |
(auto) | Lima hypervisor: vz, qemu, or omit for auto |
vm.base_image_enable |
true |
Save and restore VM snapshots for fast recreate |
vm.recreate_prompt_after |
"7d" |
Prompt to recreate VM after this age |
vm.bootstrap_refresh_prompt_after |
"30d" |
Prompt to rerun full bootstrap after this age |
When base images are enabled (default), aivm saves a snapshot after bootstrap and
can recreate the VM from it quickly without rerunning plugin install.
recreate_prompt_after nudges you to recreate the VM instance; when the base is
still valid, that recreate is fast (aivm recreate --fast).
bootstrap_refresh_prompt_after nudges you to rerun a full bootstrap so
installed plugins and tools stay fresh.
By default, aivm destroy clears the base image, bootstrap state, and age
tracking files in ~/.aivm. Use aivm destroy --keep-base to preserve that
state for a fast start or recreate --fast (same semantics as idle auto-delete).
aivm runs an idle monitor daemon that watches session activity and automatically tears down the VM when unused:
idle:
stop_timeout: 5m # stop VM this long after last active session ends
delete_timeout: 5m # delete the stopped VM after this additional wait
poll_interval: 30s # how often the idle monitor checks session activitySet stop_timeout or delete_timeout to 0 to disable that stage. Idle
monitoring is automatically disabled when T3 Code is enabled.
Plugin installation, customization, and authoring are documented in Plugins.
Integrations run a configure shell script after bootstrap when their
conditions are met: a specific plugin is installed (from) and a specific
agent is active (to). Use them to wire tools into an agent's context (e.g.
register an MCP server).
integrations:
- name: my-tool-claude # required when from is empty; optional otherwise
from: my-tool # plugin that must be installed (omit for agent-only)
to: claude # agent that must be active
configure: |
my-tool configure --agent claudeOmit from to run whenever the to agent is active, regardless of plugins
(name is required in that case).
Point aivm at a standard docker-compose.yml to run services alongside the VM.
All services are brought up with docker compose up -d when the VM starts, and
torn down with docker compose down on stop. Named volumes are never
deleted by aivm — they persist across VM recreations and aivm destroy.
Note:
compose_fileruns on host Docker (Docker Desktop, OrbStack, etc.) — not inside the Lima VM. To use Docker inside the VM instead, enable the opt-indockerplugin (see Plugins).
compose_file: ./docker-compose.ymlThe path is resolved relative to aivm.yaml. The compose project name is set to
the VM profile name (vm.name).
Example docker-compose.yml:
services:
metamcp:
image: ghcr.io/metatool-ai/metamcp:latest
ports:
- "127.0.0.1:3000:3000"
environment:
AUTH_TOKEN: ${AUTH_TOKEN} # source from .env or export before launching aivm
restart: unless-stoppedSecurity note: Always use a strong, randomly generated secret for
AUTH_TOKEN. Set it in a .env file (add .env to .gitignore) or export it
before launching aivm. Never commit weak or placeholder credentials like
"changeme".
Use your compose file's native .env file or environment: keys for variable
substitution — no aivm-specific template variables are needed.
Status display: aivm status reports each service's running state.
Log access: aivm logs tails logs/aivm.log (orchestration and subprocess
output at trace). aivm logs monitor tails the idle monitor daemon.
When enabled, aivm start (or the default aivm command, which starts the VM
first) launches the T3 Code web UI inside the VM and
port-forwards it to your host. The normal agent terminal still opens when you run
aivm — T3 Code runs alongside it as a browser-accessible frontend.
t3code:
enable: true
port: 3773 # http://localhost:3773aivm status shows the access URL and pairing token. The t3code plugin is
auto-installed when this mode is enabled.
Note: At least one entry in
agents.enabledis required;agents.defaultis inferred automatically when only one agent is enabled. Idle monitoring is disabled in this mode; useaivm stopto shut down explicitly.
aivm [directory] Launch the configured AI agent (default command)
| Command | Description |
|---|---|
aivm |
Start VM + services, then launch agent in current directory |
aivm agent -- <args> |
Agent CLI with custom args (launch_args skipped) |
aivm start |
Start VM and services only |
aivm stop |
Stop VM and services (disk preserved) |
aivm restart |
Stop then start VM and services |
aivm destroy |
Delete VM and clear base image, bootstrap state, and age files (--keep-base preserves them for fast recreate) |
aivm recreate |
Destroy VM and re-run full bootstrap (see below) |
aivm status |
Show VM, compose service, and T3 Code status |
aivm ssh |
Open an interactive shell in the VM |
aivm cp <src> <dst> |
Copy host ↔ VM files (vm: prefix for VM paths) |
aivm logs [component] |
Tail logs (aivm · monitor; default aivm) |
aivm version |
Print version |
Global flags:
| Flag | Description |
|---|---|
--config <path> |
Path to aivm.yaml (default: ~/.aivm/aivm.yaml) |
--agent <name> |
Agent to use (overrides agents.default) |
--log-level |
trace–error (default info; overrides env/config) |
Environment variable AIVM_LOG_LEVEL and config key log_level: use the same
values. Precedence: flag → env → config.
--log-level may appear before the subcommand (aivm --log-level error agent -- …) or after agent but before -- (aivm agent --log-level error -- …).
Flags after the first -- are passed to the agent CLI, not aivm.
Interactive vs passthrough:
| Invocation | Uses launch_args |
Agent arguments |
|---|---|---|
aivm (default) |
Yes | From config (e.g. --dangerously-skip-permissions) |
aivm agent -- <args> |
No | Your args after -- |
Use --agent <name> with either path to override agents.default (agent must
be enabled in config).
External tools (Cursor custom agent command, OpenCode, etc.) can run agents
inside aivm by setting a command prefix—no PATH shims. The tool appends its
own CLI flags after the trailing --; do not run the prefix alone (aivm
requires at least one argument after --).
# Cursor — custom agent command prefix (appends flags after --)
export AIVM_LOG_LEVEL=error # optional: quiet bootstrap (failures still print)
aivm --agent cursor agent --# OpenCode — same pattern (OpenCode appends its subcommand/flags after --)
export AIVM_LOG_LEVEL=error
aivm --agent opencode agent --The -- separator is required between aivm flags and agent arguments.
Example (you can also run this directly):
aivm --agent cursor agent -- -p "explain this repo"With AIVM_LOG_LEVEL=error, progress steps are hidden so the tool sees mostly
agent output. Cold VM start can take one to two minutes with little output until
the agent runs or an error is reported.
Destroys the current VM and re-runs full bootstrap on a fresh one, unconditionally installing all plugins. Useful after adding new plugins or when the image has drifted.
aivm recreate # prompts if active sessions exist
aivm recreate --force # stop active sessions without prompting
aivm recreate --fast # restore from base image when valid (skips full bootstrap)Environment overrides:
| Variable | Description |
|---|---|
AIVM_LOG_LEVEL |
Same values as --log-level |
AIVM_STATE_DIR |
Override ~/.aivm state directory |
AIVM_INSTALL_DIR |
Install destination for install.sh / make install |
Copy files or directories between the host and the VM. Prefix VM paths with
vm:.
aivm cp vm:/home/user/file.txt ./local/ # copy file from VM to host
aivm cp ./local/file.txt vm:/home/user/ # copy file from host to VM
aivm cp -r vm:/home/user/dir/ ./local/dir/ # copy directory from VM to host
aivm cp -rf ./local/dir/ vm:/home/user/dir/ # copy directory to VM, overwriteFlags:
| Flag | Description |
|---|---|
-r / --recursive |
Copy directories recursively |
-f / --force |
Overwrite destination if it already exists |
Plugins install software in the VM during bootstrap. Enable them in aivm.yaml:
plugins:
enabled:
- system # on by default
- mise-node
- awscliDependencies install automatically — if you enable cocoindex-code, aivm also
installs mise-uv, mise, and system without you listing them.
Only system is enabled by default. Everything else is opt-in under
plugins.enabled, except t3code which is added when t3code.enable: true.
| Plugin | Installs | How to enable |
|---|---|---|
system |
apt: git, curl, jq, compilers, … |
Default |
docker |
Docker Engine inside the VM (opt-in) | plugins.enabled |
mise |
mise version manager | Auto (via mise-*) |
mise-<tool> |
Any mise registry tool | plugins.enabled |
awscli |
AWS CLI v2 | plugins.enabled |
cocoindex-code |
ccc CLI + semantic search skill |
plugins.enabled |
context7 |
ctx7 CLI + find-docs skill |
plugins.enabled |
skills |
Community skills from GitHub repos | plugins.enabled |
t3code |
T3 Code web GUI | t3code.enable: true |
Base VM packages via apt-get. Required by nearly every other plugin.
Installs rootful Docker Engine inside the Lima VM via the official convenience
script. Not enabled by default — add docker to plugins.enabled:
plugins:
enabled:
- system
- dockerThis is separate from compose_file, which still requires host Docker.
mise installs the mise runtime. mise-<tool> is a family of plugins — one per
tool in the mise registry. aivm runs mise use --global <tool>@<version>.
| Plugin | Tool |
|---|---|
mise-node |
Node.js |
mise-python |
Python |
mise-uv |
uv |
mise-go |
Go |
mise-java |
Java (Temurin) |
mise-rust |
Rust |
mise-maven |
Maven |
mise-gradle |
Gradle |
mise-terraform |
Terraform |
mise-helm |
Helm |
plugins:
enabled:
- mise-node
- mise-goPin versions under plugins.config — see
Customize built-in plugins.
Installs AWS CLI v2 with the official architecture-aware installer.
plugins:
enabled:
- awscliCocoindex Code — AST-based
semantic code search (ccc CLI). Installs local embeddings by default (no API
key).
plugins:
enabled:
- cocoindex-codeContext7 — up-to-date library docs (ctx7 CLI +
find-docs skill). No API key required; set CONTEXT7_API_KEY in vm.env for
higher rate limits.
plugins:
enabled:
- context7Installs agent skills from public GitHub repos via the
skills CLI. Requires a sources list
under plugins.config — see
Customize built-in plugins.
plugins:
enabled:
- skillsInstalls the T3 Code npm package. Auto-enabled when t3code.enable: true.
Started by aivm start — see T3 Code GUI.
Tune shipped plugins via plugins.config (settings) or plugins.define
(override install scripts). After changing setup, run
aivm recreate.
Values are passed to the plugin's setup script.
mise-<tool> — pin versions
| Key | Default | Description |
|---|---|---|
version |
"latest" |
Global version (mise use --global) |
extra_versions |
[] |
Extra versions via mise install |
plugins:
config:
mise-node:
version: "22"
extra_versions: ["20", "18"]cocoindex-code — install variant
plugins:
enabled:
- cocoindex-code
config:
cocoindex-code:
variant: slim # default: full (local embeddings)
config: # ~/.cocoindex_code/global_settings.yml
embedding:
model: voyage/voyage-code-3
envs:
VOYAGE_API_KEY: your-api-keyskills — choose repositories
plugins:
enabled:
- skills
config:
skills:
sources:
- repo: mattpocock/skills # all skills (--all)
- repo: another-author/skills
skills: [tdd, grill-me] # named skills onlycontext7
No plugin config keys — use vm.env for CONTEXT7_API_KEY.
Merge into a built-in plugin field-by-field. Non-empty fields replace the built-in value.
plugins:
define:
awscli:
setup: |
ARCH=$(uname -m)
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" \
-o /tmp/awscliv2.zip
unzip -q /tmp/awscliv2.zip -d /tmp/awscliv2
sudo /tmp/awscliv2/aws/install --update| Field | Purpose |
|---|---|
description |
Label shown in bootstrap logs |
dependencies |
Plugins installed first |
path_entries |
Directories added to VM PATH |
defaults |
Default values merged with plugins.config |
agents |
Limit plugin to specific agents |
setup |
Install script |
Define a new plugin in plugins.define and list it in plugins.enabled.
plugins:
enabled:
- my-tool
define:
my-tool:
description: "My custom tool"
dependencies: [system]
path_entries:
- "$HOME/.local/bin"
setup: |
curl -fsSL https://example.com/install.sh | bashsetup scripts must be safe to run on a fresh VM (use idempotent installers).
| Field | Required | Purpose |
|---|---|---|
description |
Recommended | Label in bootstrap logs |
setup |
Yes | Shell script run on bootstrap |
dependencies |
No | Plugins to install first |
path_entries |
No | Directories added to VM PATH |
defaults |
No | Default plugins.config values |
agents |
No | Restrict to specific agents |
setup supports Go templates.
Values from plugins.config.<name> are available as template fields:
plugins:
enabled:
- my-tool
config:
my-tool:
version: "3.2.1"
define:
my-tool:
dependencies: [system]
setup: |
curl -fsSL "https://example.com/my-tool-{{ .version }}.tar.gz" \
| tar -xz -C "$HOME/.local"| Template | Source |
|---|---|
.version, .repo, … |
plugins.config merged with defaults |
.state_dir |
aivm state directory (auto-injected) |
toYAML |
Serialize a value to YAML |
b64enc |
Base64-encode for safe shell embedding |
Run aivm recreate and verify with aivm ssh.
Agents are the AI coding CLIs that aivm launches inside the VM. Configure them
under agents in aivm.yaml.
All agents listed in agents.enabled are installed during bootstrap, so you can
switch between them with --agent without rebuilding the VM. If only one agent
is enabled, agents.default is inferred automatically. Enabled agent plugins are
registered automatically based on agents.enabled.
Built-in agent definitions live in
internal/agent/defaults.yaml.
Agent-specific mounts are defined under mounts in each agent definition.
These bind host state directories into the VM at the paths tools expect. Mount
data lives on the host under ~/.aivm/ (or AIVM_STATE_DIR) and is not
stored on the VM disk — it survives aivm recreate and VM deletion.
| Field | Purpose |
|---|---|
cli_command |
Binary invoked in the VM |
launch_args |
Flags appended by bare aivm (not aivm agent --) |
setup |
Override the agent's install script |
dependencies |
Plugins/toolchains required before install |
path_entries |
Directories added to VM PATH |
mounts |
Host→VM bind mounts for agent state (see per-agent sections) |
agents:
enabled:
- claude
define:
claude:
launch_args: "--dangerously-skip-permissions --model opus"agents:
default: claude
enabled:
- claudeSSH in the VM and runs claude --dangerously-skip-permissions. Claude project
history and image cache are persisted on the host at
~/.aivm/.claude/projects/ and ~/.aivm/.claude/image-cache/, mounted into
the VM at ~/.claude/projects and ~/.claude/image-cache.
Authenticate inside the VM once:
aivm ssh
claude authagents:
default: copilot
enabled:
- copilotSSH in the VM and runs copilot --yolo. Session state is persisted on the host
at ~/.aivm/.copilot/session-state/, mounted into the VM at
~/.copilot/session-state/.
Authenticate inside the VM once:
aivm ssh
gh auth loginagents:
default: cursor
enabled:
- cursorSSH in the VM and runs agent. aivm installs Cursor with the upstream curl https://cursor.com/install -fsS | bash flow, keeps ~/.local/bin on PATH for
login shells, and persists Cursor CLI state on the host at ~/.aivm/.cursor/,
mounted into the VM at ~/.cursor/.
Authenticate inside the VM once:
aivm ssh
agent loginagents:
default: opencode
enabled:
- opencodeSSH in the VM and runs opencode. The binary is installed with the upstream
curl -fsSL https://opencode.ai/install | bash flow, with PATH modification
disabled so aivm controls the runtime environment.
Authenticate inside the VM on first launch if prompted.
Each agent has a cli_command (binary in the VM) and optional launch_args
used only by the interactive shortcut (aivm). Override install steps or launch
behavior:
agents:
define:
copilot:
cli_command: copilot
launch_args: "suggest"Run a one-off prompt without launch_args (forwards only the args after --):
aivm agent -- -p "Refactor utils.js to use arrow functions"git clone https://github.com/sisimomo/aivm.git
cd aivm
# Build binary to bin/aivm
make build
# Install to ~/.local/bin/aivm (override with AIVM_INSTALL_DIR)
make install
# Run unit tests
make test
# Lint
make vet
# Format
make fmtRelease snapshot (requires goreleaser):
make release-snapshotPull requests are welcome. For major changes, please open an issue first.
- Fork the repo and create a feature branch
- Make your changes with tests where applicable
- Run
make fmt vet testto verify - Open a pull request against
main