Skip to content

Repository files navigation

aivm

Launch AI agents in a secure, disposable Linux runtime (Lima or Docker) — with one command.

CI Go macOS only

Demo

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)


Table of Contents


Requirements

  • macOS (Intel or Apple Silicon) — Linux/Windows is not supported
  • Lima for the default lima backend (brew install lima), or Docker Engine/Desktop for the docker backend
  • Authentication for your chosen agent is handled inside the VM after first launch

Installation

One-liner (recommended)

curl -fsSL https://raw.githubusercontent.com/sisimomo/aivm/main/install.sh | sh

This 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.

From source

git clone https://github.com/sisimomo/aivm.git
cd aivm
make install

Verify:

aivm version

Quick Start

  1. Edit your config:

    nano ~/.aivm/aivm.yaml

    Set agents.enabled and optionally agents.default. Keep vm.backend: lima for the default setup, or set vm.backend: docker and vm.docker_image to run on Docker directly.

  2. 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).


Configuration

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.log

Override 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.

VM Resources

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)

VM Backend

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

Mounts

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: rw

Remapped mount — host path differs from VM path:

vm:
  mounts:
    - source: "{{ .host_home }}/company-secrets"
      target: "/secrets"
      mode: ro

When 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.

Session host environment

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.

Persistent VM environment

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.

VM type and recreation prompt

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).

Idle Management

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 activity

Set 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

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 claude

Omit from to run whenever the to agent is active, regardless of plugins (name is required in that case).

Compose File

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_file runs on host Docker (Docker Desktop, OrbStack, etc.) — not inside the Lima VM. To use Docker inside the VM instead, enable the opt-in docker plugin (see Plugins).

compose_file: ./docker-compose.yml

The 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-stopped

Security 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.

T3 Code GUI

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:3773

aivm 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.enabled is required; agents.default is inferred automatically when only one agent is enabled. Idle monitoring is disabled in this mode; use aivm stop to shut down explicitly.


Commands

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 traceerror (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).

Tool integration

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.

aivm recreate

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

aivm cp

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, overwrite

Flags:

Flag Description
-r / --recursive Copy directories recursively
-f / --force Overwrite destination if it already exists

Plugins

Plugins install software in the VM during bootstrap. Enable them in aivm.yaml:

plugins:
  enabled:
    - system        # on by default
    - mise-node
    - awscli

Dependencies install automatically — if you enable cocoindex-code, aivm also installs mise-uv, mise, and system without you listing them.

Available plugins

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

system

Base VM packages via apt-get. Required by nearly every other plugin.

docker

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
    - docker

This is separate from compose_file, which still requires host Docker.

mise and mise-<tool>

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-go

Pin versions under plugins.config — see Customize built-in plugins.

awscli

Installs AWS CLI v2 with the official architecture-aware installer.

plugins:
  enabled:
    - awscli

cocoindex-code

Cocoindex Code — AST-based semantic code search (ccc CLI). Installs local embeddings by default (no API key).

plugins:
  enabled:
    - cocoindex-code

context7

Context7 — 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:
    - context7

skills

Installs agent skills from public GitHub repos via the skills CLI. Requires a sources list under plugins.config — see Customize built-in plugins.

plugins:
  enabled:
    - skills

t3code

Installs the T3 Code npm package. Auto-enabled when t3code.enable: true. Started by aivm start — see T3 Code GUI.

Customize built-in plugins

Tune shipped plugins via plugins.config (settings) or plugins.define (override install scripts). After changing setup, run aivm recreate.

Settings with plugins.config

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-key

skills — choose repositories

plugins:
  enabled:
    - skills
  config:
    skills:
      sources:
        - repo: mattpocock/skills          # all skills (--all)
        - repo: another-author/skills
          skills: [tdd, grill-me]          # named skills only

context7

No plugin config keys — use vm.env for CONTEXT7_API_KEY.

Override install scripts with plugins.define

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

Create a custom plugin

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 | bash

setup scripts must be safe to run on a fresh VM (use idempotent installers).

Schema

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

Config-driven installs

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

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.

Customizing agents

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"

Claude Code

agents:
  default: claude
  enabled:
    - claude

SSH 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 auth

GitHub Copilot

agents:
  default: copilot
  enabled:
    - copilot

SSH 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 login

Cursor Agent

agents:
  default: cursor
  enabled:
    - cursor

SSH 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 login

OpenCode

agents:
  default: opencode
  enabled:
    - opencode

SSH 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.

Custom Agents

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"

Building from Source

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 fmt

Release snapshot (requires goreleaser):

make release-snapshot

Contributing

Pull requests are welcome. For major changes, please open an issue first.

  1. Fork the repo and create a feature branch
  2. Make your changes with tests where applicable
  3. Run make fmt vet test to verify
  4. Open a pull request against main

License

MIT

About

Launch AI agents in a secure, disposable Linux runtime (Lima or Docker) — with one command.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages