Give agents a machine they can wreck.Keep yours.

agent-vm runs AI coding agents with every permission, in a Linux VM per project. Your SSH keys, your browser sessions and the rest of your disk are invisible to them.

Get started

curl -fsSL https://www.agent-vm.org/install.sh | sh

MIT licensed · macOS, Linux, Windows (experimental) · built on Lima

cd ~/work/acme-api
agent-vm run cat ~/.ssh/id_ed25519
Starting VM 'agent-vm-acme-api-8f3c1d20'...
Resources: CPUs: 1, Memory: 3 GiB, Disk: 10 GiB
cat: /Users/you/.ssh/id_ed25519: No such file or directory
agent-vm opencode
Resources: CPUs: 1, Memory: 3 GiB, Disk: 10 GiB
# OpenCode starts here, with --auto

Only the project directory is mounted.

  • Lima is the only host dependency

    No Node, no npm, no Docker Desktop on your machine: the toolchain lives in the VM. On Linux, Lima also needs QEMU and access to KVM.

  • A separate kernel, not a namespace

    On Linux, escaping a container puts you on the host. Escaping a VM means getting through the hypervisor first.

  • Ports are forwarded for you

    A dev server started in the VM answers on localhost at the same port. Lima does it; there is no flag and no config.

  • Batteries included, root for the rest

    The base VM ships dev tools, Docker, headless Chromium and the agents, picked in a setup wizard. The agent is root in its VM: it installs whatever else it needs.

Why this exists

Why not just run the agent on your machine?

Every agent has a flag that turns approvals off, and many people run them that way. When that happens in your home directory, three things can go wrong.

  1. Dependency installs run arbitrary code

    npm ran every package’s install scripts by default until npm 12 (July 2026), pip still runs a source package’s build, Cargo its build.rs, and your tests then run whatever got installed. In 2025 the Shai-Hulud worm spread through thousands of npm packages that way: it collected npm tokens, GitHub PATs, SSH keys and cloud credentials, then republished itself using them.

  2. Approval prompts stop working

    The alternative is approving each command by hand. After the first few dozen prompts the answer is reflexive, and the one that mattered gets the same yes as the rest. That is why the bypass flag exists, and why it gets used.

  3. The agent is not reliable either

    No compromised package needed. Agents read issues, web pages and READMEs, and anything they read can try to give them instructions. They also make plain mistakes: the wrong path, a git checkout . over uncommitted work.

A VM limits what a mistake or a compromised agent can reach to the project and what you hand it, such as its env file. What stays open is the network: see Security.

Install

Install, build a template, run an agent.

agent-vm is a few shell scripts, with no daemon.

  1. Install the command

    Pick one. Each puts agent-vm on your PATH without root.

    curl -fsSL https://www.agent-vm.org/install.sh | sh

    Downloads the latest release from GitHub, checks it against the release’s SHA256SUMS, unpacks it in ~/.local/share/agent-vm and links agent-vm into ~/.local/bin. It then offers to run step 2 right away. Run it again to update. sh -s -- --version X.Y.Z installs a given release, sh -s -- --git a clone of main.

  2. Build the base template

    Run once. Without Lima, or without the build that keeps .git read-only, it first offers to install it. A wizard then picks what goes in the template: Enter takes the default set (see What is in the VM). It builds a Debian 13 VM with it and keeps it, stopped, as the template.

    agent-vm setup
  3. Run an agent in your project

    Clones the template into a VM for this directory, mounts the directory and launches the agent with permissions already granted. A start may first stop on a security question: see Protecting .git.

    cd your-project
    agent-vm opencode     # or: agent-vm claude

Usage

Everyday commands.

Run them from your project directory. The Reference lists every command, option and file.

Run an agent

Extra arguments are forwarded to the agent as-is, so anything its own CLI accepts works here.

agent-vm opencode                        # OpenCode
agent-vm claude                          # Claude Code
agent-vm codex                           # Codex CLI
agent-vm vibe                            # Mistral Vibe

agent-vm claude -p "fix all lint errors"
agent-vm opencode run "update the changelog"

Reach the dev server

Lima forwards every port opened in the VM to the same port on your localhost, so a dev server opens in your browser as usual. Treat those ports as the agent’s.

agent-vm run npm run dev         # then open localhost:5173
agent-vm run docker compose up

Get inside

A shell to poke around, or a single command when you already know what you want.

agent-vm shell                    # zsh in the VM
agent-vm run npm install          # one-off command
agent-vm run --tty opencode       # allocate a PTY for TUIs
agent-vm sh -c "ls -la | grep config"
agent-vm code                     # VS Code in the browser, if set up

Manage the fleet

The current directory’s VM is marked with > in list. If a directory is renamed, list is the only way to find its VM again.

agent-vm list          # every VM, the current one marked
agent-vm stop          # stop, keep the disk
agent-vm rm            # stop and delete
agent-vm destroy-all   # every VM, base template included
agent-vm doctor        # what is wrong, and what to run

Tighten the session

--readonly makes every host share read-only, rw volumes included, and the host enforces it: root in the VM cannot lift it. Packages still install on the VM’s own disk, but nothing lands in the project. --scratch mounts nothing of yours: the agent clones the code with a token from agent-vm env, pushes its work, and the VM is deleted at exit.

agent-vm --readonly shell   # nothing on the host writable
agent-vm --scratch claude   # nothing mounted, gone at exit
agent-vm --rm run npm test  # destroy the VM on exit

Resize on the fly

A new VM gets the template’s 10 GB of disk, 3 GB of memory (4 with code-server picked in the wizard) and 1 CPU. Pass another value and the VM is reconfigured, after asking if it is running. Disk only grows. CPU and memory are capped at half the host, per VM.

agent-vm --disk 50 opencode
agent-vm --memory 16 --cpus 8 shell
agent-vm --reset claude   # re-clone from the base template

Share secrets across VMs

KEY=value lines in ~/.agent-vm/env, pushed into every VM on each command. Everything in the VM can read them: use narrow tokens you can revoke. Edit with env set, since one bad quote in the file breaks all of it.

agent-vm env set GH_TOKEN   # pasted unseen, not in history
agent-vm env list           # names only, never values
agent-vm env has ANTHROPIC_API_KEY

# scoped to this project only
agent-vm project-env set SOME_PATH ./config

Edit with the VM’s tools

For language servers, linters, the debugger and the VM’s packages, run the editor in the VM: agent-vm code serves VS Code (code-server) to a browser tab, the only part on your machine. Set it up with code-server or a code-* name (What is in the VM). The tab can still open links, and every VM reaches the editor’s port, hence a password and a host name per VM (Network and ports). Do not connect a desktop editor over SSH instead (SSH from your machine).

agent-vm setup --preinstall=default,code-claude   # once
agent-vm code   # prints the address and the password
                # Ctrl-C stops the editor

Letting the agent commit

git reads its identity from the environment, so the shared env file covers it with no git config inside the VM. All four variables are needed: git refuses to commit without a committer, not just an author.

# ~/.agent-vm/env
GIT_AUTHOR_NAME=Your Name
GIT_AUTHOR_EMAIL=12345+you@users.noreply.github.com
GIT_COMMITTER_NAME=Your Name
GIT_COMMITTER_EMAIL=12345+you@users.noreply.github.com

These variables win over git config, in every repository of the VM: for per-repository identities, set user.name and user.email from a runtime script instead. gh picks up GH_TOKEN on its own, so gh pr create works with nothing else. Plain git push over HTTPS needs a credential helper: one gh auth setup-git line in your runtime script.

That said, keep the commits human

Being able to commit is not a reason to let it. A commit says you read the diff, so let the agent write the code, read it, and commit it yourself. With .git protected, that is the only way.

Architecture

A template, then a clone per directory.

Setup pays the provisioning cost once. Every project after that is a clone of a VM that already has the toolchain in it.

  1. setup builds the template

    Lima creates a Debian 13 VM. agent-vm.setup.sh installs the dev tools, Docker, Chromium and the agents inside it, then the VM is stopped and kept as a base template.

  2. The first run clones it

    Running an agent in a directory clones the template into a persistent VM keyed to that path, mounts the working directory, and runs the runtime scripts: the per-user one first, the per-project one second.

  3. The agent works unattended

    It is launched with its own auto-approve flag. Lima forwards the ports it opens inside the VM, so a dev server is reachable in your browser as usual.

  4. The VM outlives the session

    It persists after the agent exits. Any later command in the same directory reuses it, with its containers and its logins intact, until rm or --rm.

Your machine

Only Lima installed
  • SSH keys
  • API tokens
  • Browser sessions
  • git config
  • Everything else
Hypervisor boundary

The VM

Separate kernel
  • The agent
  • node, python, docker, ...
  • Headless Chromium, ...
  • Ports, forwarded out

~/work/your-project

Shared

Your project directory, mounted into the VM at the same path. Your editor and the agent work on the same files, so there is nothing to copy back. Read-write by default, read-only with --readonly. Anything else that crosses is something you chose: the env file, and extra mounts listed in ~/.agent-vm/volumes.

Each VM authenticates on its own: claude login happens inside it. Credentials persist across restarts of that VM and are shared with neither the host nor any other VM. The network is shared, though: see Network and ports.

What is in the VM

The setup wizard offers the default set: everything below except the rows marked no. Answer n to pick each item. --preinstall takes the names instead, comma-separated, plus default, all or none, and skips the wizard, as does a setup with no terminal.

CategoryPackagesNameDefault
Coregit, curl, wget, jq, zsh, ca-certificates, sshfs, build-essential, pkgconf, patch, unzip, zip, ripgrep, fd-find, htopalwaysyes
Build libslibssl-dev, libreadline-dev, zlib1g-dev, libyaml-dev, libffi-devalwaysyes
Version managermisealwaysyes
Pythonpython3, pip, venvpythonyes
Node.jsNode.js 24 LTS via NodeSourcenodeyes
Rubyruby-fullrubyno
Rustrustup, stable toolchainrustno
Gogolang-gogolangno
GitHub CLIghghyes
BrowserChromium headless, xvfbchromiumyes
ContainersDocker Engine, Docker Composedockeryes
AI agentsClaude Code, OpenCode, Codex CLI, Mistral Vibeclaude, opencode, codex, vibeyes
AI agentsPipino
MCPChrome DevTools MCP, wired into every installed agent but Pi (no MCP support)mcp-chromeyes
MCPPlaywright MCP, reusing the same Chromiummcp-playwrightno
Editorcode-server (VS Code in the browser, for `agent-vm code`), dark theme, GitHub Copilot off, no telemetrycode-serverno
EditorThe Claude Code, Codex or Mistral Vibe extension, each starting with no permission prompts, with code-servercode-claude, code-codex, code-vibeno
agent-vm setup --preinstall=default                  # no prompts
agent-vm setup --preinstall=default,rust             # plus Rust
agent-vm setup --preinstall=python,docker,claude     # minimal Claude setup
agent-vm setup --preinstall=default,code-claude      # plus the editor
agent-vm setup --disk 50 --memory 16 --cpus 8        # a bigger template

codex and pi pull in node, and so does mcp-chrome with chromium and an agent. Both MCP servers are skipped without node and chromium. Each editor extension ships its own copy of its agent (200 to 600 MB): the wizard asks whether to keep the command-line one too. Without it, agent-vm claude says it is not installed. This is only what the template ships: the agent is root in the VM and has the network, so it installs whatever else it needs.

Why not Docker

On Linux, containers share the host kernel, so a compromised dependency that finds a kernel bug is on your host. On macOS, Docker Desktop already runs a VM, which narrows the gap, but it is one VM shared by every container rather than one per project. A VM brings its own kernel, and root inside it still has the hypervisor between it and your machine. The practical differences matter as much as the security one.

No sandboxDocker / devcontaineragent-vm
Agent can run any commandYesYesYes
Host files it can reachAll of themWhat you mountThe project directory
git credentials, SSH agentYoursNone with plain Docker, forwarded by VS Code devcontainersNothing, beyond what you put in ~/.agent-vm/env
Outbound networkOpenOpen by defaultOpen (see the roadmap)
Shares the host kernelYesOn Linux; on macOS, the Docker Desktop VM’sNo
Reaching the host needsNothingA kernel bug, plus a hypervisor bug on macOSA hypervisor bug
Docker insideYesNeeds DinD or a socket mountYes, natively
Headless browserOn the hostUp to your imageChromium, included
Environment definitionNone, your machineA devcontainer.json versioned with the projectOne base template, the same for every project

Docker runs natively inside the VM with no Docker-in-Docker, and headless Chromium works out of the box. On a Mac, this replaces Docker Desktop.

Security

What crosses the boundary.

The VM cannot leave its shares. What remains is what crosses them: the project folder, which the agent writes and your machine reads, the terminal, and the network.

Protecting .git

Why .git

Git on your machine runs what a repository’s .git/config and hooks name: core.fsmonitor on every git status, hooks on commit. Your editor and prompt run git status on their own, so a VM able to write .git could run commands on your host within seconds, invisible in git diff.

With a Lima that has sshfs.readonlyNames, every .git and .hg in the shares is read-only for the VM, at any depth, enforced on the host by Lima’s SFTP server. The agent reads the history but cannot commit. It is not merged upstream yet (lima-vm/lima#5529), so agent-vm setup offers a build that has it. The shares then use reverse-sshfs, slower on many files (see Node.js), and without sshfs’s cache on the writable ones, so the VM never writes back a file as it was before you changed it: AGENT_VM_SSHFS_CACHE=1 trades that for speed.

brew unlink lima; brew install sylvinus/tap/lima-sylvinus
agent-vm doctor    # where you stand

# let the agent commit anyway
agent-vm --unsafe-writable-git claude

Beyond the name .git

The folder a core.hooksPath points to in a share (.husky for husky) is read-only too, as is a link on the way to it, and the same goes for the repositories in your writable volumes. A folder holding git’s internals (HEAD, objects/, refs/, a config) is a repository under any name: setting safe.bareRepository to explicit in your global git config makes git ignore it, and one in a writable volume is read-only by its name. A config included from a share, a command or a hook git runs from one, or hooks at the top of a share, are the same kind of door.

Before a VM boots with writable shares, agent-vm stops on each of these it finds, and on a Lima without readonlyNames, and asks: Enter, or no terminal, aborts. doctor lists them. --unsafe-writable-git (or AGENT_VM_UNSAFE_WRITABLE_GIT=1 in your shell, never read from the project) lets the agent commit and reopens that path to your host, with a warning on every run.

The Lima build

On Windows, setup downloads it and checks it against checksums pinned in agent-vm. Elsewhere, Homebrew installs it, or build it from source.

A Lima whose answer agent-vm cannot read stops the start with an error, rather than passing for one without readonlyNames.

# back to Homebrew’s Lima
brew uninstall lima-sylvinus && brew link lima

# the Lima build, without Homebrew (needs Go and make)
git clone --depth 1 -b v2.3.0-sylvinus.2 https://github.com/sylvinus/lima
cd lima && make native && sudo make install

What agent-vm does

Refused directories and files

agent-vm will not share your home directory, /, its own directory, ~/.agent-vm, Lima’s directory, or a directory containing one of them: cd ~ && agent-vm shell would hand the VM your dotfiles and SSH keys.

It never reads a file in the project by its path, since the VM can make it a symlink to any file of yours. Its own git calls in a project refuse a bare repository and run no core.fsmonitor or pager.

--readonly, in detail

--readonly covers every share, rw volumes included: a writable volume containing the project would be a second way in. Whether the host enforces it is read from what Lima reports, never from the guest. Where it would not (reverse-sshfs without readonlyNames, virtiofs under QEMU), the flag is refused.

A running VM in the other mode is restarted, asked first. Declined, or with no terminal, the command fails, so another session’s read-only VM is never made writable under it.

What else on your machine reads the project

The rule

Anything on your machine that reads the project and acts on what it finds can run what the agent wrote. agent-vm closes git’s doors (see Protecting .git), but cannot lock the files your other tools read without stopping the agent from working.

So on your machine, open the project in your editor and use git in it; run everything else in the VM, with agent-vm run. The lists below are examples, not an inventory.

  • Nothing at all: what runs when you cd into the folder, your prompt redraws, your editor opens it. The most dangerous kind.
  • Something you do anyway: git commit runs hooks kept in the working tree, docker compose up mounts what the compose file names.
  • Running project code: npm test, make, a build. That is the agent’s code: run it in the VM.

Keeping your checkout out of reach

Do not give agent-vm the checkout you work in. Clone the project a second time, run agent-vm there, and git fetch its work into your checkout once you have read the diff. Fetching does not check files out, so the agent’s files reach your working tree only when you merge. This needs a Lima that keeps .git read-only: otherwise the VM can write the clone’s .git, which git then reads.

# once; --no-local copies objects, no hardlinks
git clone --no-local ~/work/app ~/agent/app
# the agent works there
cd ~/agent/app && agent-vm claude
# back in your checkout, when it is done
cd ~/work/app
git fetch ~/agent/app HEAD:agent/review
git diff ...agent/review   # read all of it
git merge agent/review

Editors and agents

A changed tracked file shows in git diff, a new one as untracked; one in an ignored path (node_modules, .venv, target/) shows nowhere.

  • VS Code: open agent-vm projects in Restricted Mode, and do not trust a parent folder. A trusted workspace runs project code through tasks and extensions: eslint.config.js, vite.config.ts, build.rs.
  • JetBrains IDEs: “Preview in Safe Mode”. A trusted project runs its Gradle or Maven scripts on import.
  • Vim: leave exrc off. Neovim and Emacs ask before running project config.
  • Agents on your machine run commands from .claude/settings.json hooks, .mcp.json or .cursor/: review those, and treat CLAUDE.md and AGENTS.md as the VM’s writing.

SSH from your machine

Do not connect VS Code Remote-SSH or open-remote-ssh to an agent VM. They run a server in the VM, which root in the VM controls, and the editor on your machine trusts it. Microsoft’s Remote-SSH page says so: “a compromised remote could use the VS Code Remote connection to execute code on your local machine”, and it is by design. Public write-ups show it opening a terminal on the host and running commands there. That removes the boundary agent-vm sets up, which is worse than opening the project in Restricted Mode. JetBrains Gateway does not say it is safer: its security model page says what the backend loads goes to your machine without asking, the backend opens links there (after a prompt), and decides which client version your machine downloads. For language servers and a debugger with the VM’s packages, use agent-vm code.

For scripts, scp or rsync, agent-vm info prints the SSH alias as ssh_host. Put these lines at the top of ~/.ssh/config: a ForwardAgent yes found before them would hand the VM your SSH keys. --ssh-port pins the port for tools that save it.

# top of ~/.ssh/config
Include ~/.lima/*/ssh.config
Host lima-agent-vm-*
  ForwardAgent no
  ForwardX11 no

agent-vm info | grep ^ssh_host   # the alias to use
agent-vm --ssh-port 2222 shell   # a fixed port

Shell, commands, hooks

  • direnv only loads an .envrc you allowed, and a change revokes it.
  • mise trusts a config by its path, so the agent can change a trusted mise.toml: your shell runs its hooks and sets its env on the next cd, agent-vm’s AGENT_VM_UNSAFE_* variables included. mise settings set paranoid true ties trust to the content.
  • npm run, make, ./gradlew, pytest, node_modules/.bin, an activated .venv: each runs files the agent can write. Run them in the VM.
  • docker compose up on your machine can mount any folder of yours into a root container. Docker runs in the VM: use it there.
  • lefthook and pre-commit keep their commands in the working tree, and husky’s hooks call project scripts: read them in the diff, or commit with --no-verify.

File managers

  • macOS: files written through the share carry no quarantine flag, so Gatekeeper does not check an .app, .command or .pkg left there. Do not open them from Finder.
  • Windows: Explorer contacts the server named in a .library-ms, .url, .lnk or desktop.ini it displays, sending your NTLM hash (CVE-2025-24054). Keep Windows updated and outbound SMB blocked.
  • Linux: KDE’s Dolphin ran commands from a .desktop file in a folder it only displayed (CVE-2019-14744, fixed in KDE Frameworks 5.61).

Terminal and network

Terminal escape sequences

What the VM prints reaches your terminal unfiltered, and terminals act on escape sequences. agent-vm cannot filter them without breaking the agents’ full-screen interfaces.

  • Clipboard: OSC 52 lets a program set your clipboard, and in some terminals read it. Never allow reads, and check what you paste into a host shell.
  • Replies typed for you: a sequence sent just before exit can make the terminal type into your host shell. Some were real bugs (iTerm2 CVE-2024-38396): keep your terminal updated.
  • Features that act on your machine: iTerm2 file transfer and triggers, kitty remote control (keep it off), links whose text differs from their target.
  • Spoofing: the VM can print a fake host prompt after a fake exit. Check where you are before typing a secret.

Network and ports

The VM reaches the internet and every service on your machine’s loopback, at 192.168.5.2. Lima forwards every port a VM listens on to your 127.0.0.1 when it is free: a VM that listens first on 5432 receives the connections, and passwords, meant for your local Postgres. Blocking this is on the roadmap.

So every VM can reach the editor of every other one. agent-vm code gives each its own password, made in that VM, and its own host name, <vm-name>.localhost: browsers keep cookies per host name and not per port, so a page a VM serves at 127.0.0.1 does not receive the editor sessions of the others. Chrome and Firefox resolve *.localhost themselves; Safari may not, and then 127.0.0.1 in a private window kept for the editor does the same.

That stops a page, not a VM bent on it. A VM can listen on a port Lima forwards to your machine and send your browser to another VM’s host name on that port: the browser hands it that editor’s session, and the VM can use it on the editor’s own port, with its terminals. Keeping VMs out of each other needs network isolation, which agent-vm does not have yet (roadmap). Until then, stop agent-vm code (Ctrl-C) when you are not using it, and treat an editor as reachable from every VM that runs.

Reference

Every command, option and file.

What the sections above leave out: the full command set, the configuration files and variables, and what a script can rely on.

Run an agent

opencode [args]Run OpenCode with --auto.
claude [args]Run Claude Code with --dangerously-skip-permissions.
codex [args]Run Codex CLI with --dangerously-bypass-approvals-and-sandbox.
vibe [args]Run Mistral Vibe with --agent auto-approve.
pi [args]Run Pi, which has no permission prompts.

Get inside

shell, shOpen a zsh shell in the VM. -c "…" runs a one-shot command through a login shell.
run <cmd> [args]Run a command with no shell. --tty allocates a PTY for TUIs.
codeServe VS Code (code-server) from the VM at http://<vm-name>.localhost:<port>/, until Ctrl-C, for you to open in your browser. Its password is made in the VM on first use and printed with the address. Needs code-server or a code-* name at setup (What is in the VM).

Manage the fleet

list, statusList all agent-vm VMs, the current directory’s marked with >, with the agent-vm version that built the base each was cloned from, and when.
stop [vm-name]Stop this directory’s VM, or the named one. The disk survives.
rm [vm-name]Stop and delete. A name from list reaches a VM whose directory is gone.
destroy-allStop and delete every agent-vm VM, the base template included. setup rebuilds it.
doctorCheck the host, Lima, the base template and this directory, and say what to run. Changes nothing.

Set up and configure

installPut agent-vm on your PATH, from the clone: ./agent-vm.sh install. The curl installer runs it for you.
uninstallRemove that link. VMs and ~/.agent-vm stay.
setupBuild the base template. --preinstall=LIST skips the wizard (What is in the VM); --disk, --memory and --cpus size it.
env <sub>set, get, has, unset, list on the secrets shared by every VM.
project-env <sub>Same subcommands, scoped to this project. Wins over the shared file.
helpShow the built-in help.

Ask, from a script

versionPrint the version. --min X.Y.Z checks it instead of printing it.
info [dir]Machine-readable state, one key=value per line.
name [dir]Print the VM name for a directory. Defaults to the working directory.

VM options

For claude, opencode, codex, vibe, pi, shell, run and code, placed before the command or right after its name. Anything later belongs to the command: in agent-vm run docker run --rm x, --rm is docker’s.

FlagWhat it doesDefault
--disk GBVM disk size. Can grow, never shrink.the template’s (10)
--memory GBVM memory. Clamped to half the host, per VM.the template’s (3, or 4 with code-server picked in the wizard)
--cpus NCPU count. Clamped to half the host, per VM.the template’s (1)
--ssh-port NFixed host port for the VM’s SSH, for tools that save it. 0 goes back to a new one on each start. Restarts a running VM, asked first.a new one per start
--resetDestroy and re-clone the VM from the base template.off
--readonlyEvery host share read-only, project and volumes. Restarts a running VM, asked first.off
--unsafe-writable-gitLeave every .git writable so the agent can commit, with a warning. See Protecting .git.off
--unsafe-disable-security-promptsGo on where a start would stop on a security question (see Protecting .git). The warnings are still printed. Same as AGENT_VM_UNSAFE_DISABLE_SECURITY_PROMPTS=1.off
--rmDestroy the VM once the command exits.off
--scratchA new VM with nothing of yours mounted, deleted when the command ends (asked first on a terminal). ~/.agent-vm/env and runtime.sh go in, the project’s files do not. Several can run at once.off

Environment variables

Read from your shell, never from a file of the project.

VariableWhat it doesDefault
AGENT_VM_STATE_DIRMoves ~/.agent-vm, for tests, CI or a second install, without moving HOME (and Lima’s VMs with it).~/.agent-vm
AGENT_VM_PROJECT_ENVThe project’s env file, relative to the project or absolute..agent-vm.env
AGENT_VM_PROJECT_RUNTIMEThe project’s runtime script, relative to the project or absolute. Outside the project, the host reads it..agent-vm.runtime.sh
AGENT_VM_HOST_SHARECPU and memory per VM are capped at the host’s divided by this. 1: the whole host.2
AGENT_VM_BIN_DIRWhere install links agent-vm.~/.local/bin
AGENT_VM_LIMA_DIRWindows: where setup puts its Lima build.~/.local/share/lima-sylvinus
AGENT_VM_QEMU_DIRWindows: where QEMU is, when not on PATH./c/Program Files/qemu
AGENT_VM_SSHFS_CACHE1: sshfs caches the writable shares too, faster on many files (git status, find); the VM may then see a file as it was up to 20 seconds before, and write that back. Applies when a VM is next started.unset
AGENT_VM_UNSAFE_WRITABLE_GIT1: same as --unsafe-writable-git.unset
AGENT_VM_UNSAFE_DISABLE_SECURITY_PROMPTS1: same as --unsafe-disable-security-prompts.unset

Customisation files

Six optional files, none of them needed to get started. The per-user ones sit in ~/.agent-vm/, the per-project ones in the project itself.

FileScopeRuns when
~/.agent-vm/envAll VMsPushed in on every invocation
~/.agent-vm/volumesAll VMs, or the projects an entry namesMounted at VM creation
~/.agent-vm/setup.shBase templateOnce, during agent-vm setup
~/.agent-vm/runtime.shAll VMsEvery command that enters a VM, first
.agent-vm.runtime.shSingle projectEvery command that enters its VM, after the global one
.agent-vm.envSingle projectPushed in after the shared env, so it wins

Extra mounts: ~/.agent-vm/volumes

One source[:destination][:mode][:project] per line, ~ expanded on the left, # for comments. The mode is ro (default) or rw, and rw only works for directories. Without a destination, the path is mounted at the same place in the VM; a relative one is inside the project. The fourth field limits the entry to the projects it matches, * matching anything.

# ~/.agent-vm/volumes
~/.gitconfig    # same path, read-only
~/.cache/shared:/home/you.guest/.cache/shared:rw

# only in ~/work/webapp, as its .claude, read-only
~/.claude-vm/webapp:.claude:ro:~/work/webapp

# every project under ~/work
~/.cache/pip:/home/you.guest/.cache/pip:rw:~/work/*

# same path, one project only
~/datasets::ro:~/work/ml

The list lives on your side, not in the project: the agent writes the project, and could otherwise mount any host folder into its VM. A relative destination that leaves the project, through .. or a symlink, is skipped. Changes apply to new VMs: --reset re-applies them.

Node.js: node_modules in the VM

Hundreds of thousands of files are slow across the share, and native packages differ between macOS and Linux anyway. Mount a folder of the VM’s own disk over node_modules from the project’s runtime script, which runs on every command:

#!/bin/bash
# .agent-vm.runtime.sh
set -e
mkdir -p "$HOME/node_modules" node_modules
mountpoint -q node_modules ||
  sudo mount --bind "$HOME/node_modules" node_modules

The host sees an empty node_modules, or keeps its own: install there too if your editor needs the packages. In a workspace, repeat the mount for a package with a large node_modules of its own. A dev server in the VM may need polling to see edits made on the host (Vite: server.watch.usePolling).

From a script

Use these commands rather than parsing human-facing output or reading ~/.agent-vm: VM names and state files are implementation details. All of them work without Lima.

version --min exits 0 when recent enough, 1 when older, 2 on a malformed call; an engine predating --min ignores it and exits 0. In info, booleans are 1 or 0, unknown when undeterminable, and base_exists=1 means the template is usable.

A start asks its questions only when stderr is a terminal: otherwise it stops there. security_questions in info names them beforehand (lima, lima-unknown, hooks, git-config, bare-repo or none), and --unsafe-disable-security-prompts accepts them once the user agreed.

agent-vm version --min 0.2.0 || exit 1  # silent when OK
agent-vm name [dir]    # VM name for a directory
agent-vm info [dir]    # one key=value per line

# info keys: version, template, state_dir,
# project_env, dir, vm_name, base_exists,
# vm_exists, vm_running, vm_stale,
# ssh_host, ssh_config, git_protected,
# security_questions

Install and update

Installer options

Options go after sh -s --: --version X.Y.Z, --git for a clone of main, --dir DIR instead of ~/.local/share/agent-vm. To read the installer first, download it, then run it with sh.

curl -fsSL https://www.agent-vm.org/install.sh |
  sh -s -- --dir ~/tools/agent-vm

# read it first
curl -fsSLO https://www.agent-vm.org/install.sh
sh install.sh

Updating

Run the curl installer again, brew upgrade agent-vm, or git pull in a clone. agent-vm uninstall removes the link; VMs and ~/.agent-vm stay.

Running setup again rebuilds the template, not the VMs cloned from it: agent-vm warns about those, and --reset re-clones one. Its full log is in ~/.agent-vm/setup.log.

From 0.1.0, run agent-vm setup: until then, each VM from a base 0.1.0 built boots once more to install sshfs.

Windows and WSL

VMs need the Windows Hypervisor Platform feature. It is off by default and only an administrator can turn it on, once, in Windows Features or with the command below, then a reboot. On a managed laptop, that is a request to IT.

In WSL2, KVM needs nested virtualization from the Windows host; WSL1 cannot run VMs.

# in PowerShell as administrator, then reboot
DISM /Online /Enable-Feature `
  /FeatureName:HypervisorPlatform /All

Paths

Paths with whitespace, quotes, backslashes or control characters are refused. iCloud Drive paths have spaces: go through a symlink.

ln -s ~/Library/Mobile\ Documents/com~apple~CloudDocs/Dev \
  ~/Dev
cd ~/Dev/your-project && agent-vm claude

Ports and doctor

A port set with --ssh-port stays until --reset or rm. doctor prints no secret, so its output can go into an issue as is.

Agents and configuration

How each agent runs

Claude Code also gets bypass mode from managed settings, since it drops the flag when it relaunches itself (#72479). For Pi, setup sets defaultProjectTrust: "always".

Log in inside the VM (claude login, gh auth login): the login stays with that VM. Mounting your host credentials would hand them to everything in it.

A 24-bit colour terminal that does not set COLORTERM (Terminal.app on macOS 26) needs export COLORTERM=truecolor.

MCP servers

Playwright MCP uses the VM’s Chromium and downloads no browser. For another engine, edit its entry (drop --executable-path, add --browser firefox) and run npx playwright install firefox. Add your own servers to mcpServers in ~/.claude.json, from ~/.agent-vm/setup.sh or inside a VM.

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-postgres",
        "postgresql://localhost:5432/mydb"
      ]
    }
  }
}

The env files

agent-vm knows none of the names: gh reads GH_TOKEN, Claude Code ANTHROPIC_API_KEY, Codex OPENAI_API_KEY, Vibe MISTRAL_API_KEY. Edits need no --reset.

The project’s .agent-vm.env is pushed after the shared one and wins. It sits in a repository, so keep secrets out: project-env set prints the line that gitignores it.

get and has read the file without running it, and exit 2 on a value that needs a shell. set without a value reads it unseen, out of your shell history.

Extra mounts, in detail

A destination is used as written, with no ~: the VM home is /home/<you>.guest (.linux before Lima 2.1). A missing source is skipped with a warning.

A single file is hardlinked into ~/.agent-vm/file-mounts/<vm>/ and bind-mounted. Across filesystems it is copied, and host edits wait for the next start.

# ~/.agent-vm/volumes: your Claude instructions and skills,
# not the whole ~/.claude, which holds your login on Linux
~/.claude/CLAUDE.md:/home/you.guest/.claude/CLAUDE.md
~/.claude/skills:/home/you.guest/.claude/skills

Setup and runtime scripts

~/.agent-vm/setup.sh runs once in the template, at the end of setup, under zsh with sudo. ~/.agent-vm/runtime.sh, then the project’s .agent-vm.runtime.sh, run on every command that enters the VM, so both must be safe to re-run. runtime.example.sh covers git identity, gh auth setup-git, skills and MCP servers.

They are fed on standard input: give a command in them that reads it </dev/null. Keep private keys out, the agent can read what they set up.

# .agent-vm.runtime.sh
mise install
npm install
docker compose up -d

Contribute

Patches welcome.

agent-vm is a small, readable Bash codebase with a test suite that creates no VMs and needs no network. Bug reports, engine fixes and support for new agents are all welcome.

Known gaps.

  • Needs Lima

    Block outbound network

    --offline existed and was removed: it set iptables rules inside the VM, where the agent has root and could drop them. Blocking on the host needs a Lima setting that does not exist. QEMU has the flag already (-netdev user,restrict=on); on vz the user-mode network is Lima’s own gvisor stack, so it would go there. The same block would also close the host loopback, which the guest reaches at 192.168.5.2 today.

  • Needs an M3

    Run a VM inside the VM

    A --allow-nested-vm flag, for testing VM tooling from in there. Lima supports nested virtualization, but it is off by default and cannot be turned on unconditionally: below an Apple M3, limactl start fails outright instead of degrading, so every VM would stop starting. The flag would have to detect host support first. Nothing here has an M3 to test it on. It would also hand the agent a hypervisor interface.

  • Designed, not built

    Run the VMs on another machine

    One install on your laptop driving VMs on a box you rent. Close the laptop, the agent keeps working. Open questions: addressing a VM by name rather than by working directory, and who owns deleting a workspace.

Run the tests

Two suites. Neither touches your VMs, your ~/.agent-vm or your git config.

  • ./test.sh runs against a stub limactl in a throwaway HOME: no VM, no network. It covers naming, resources, the info and version surface, the --preinstall parser and the mount modes.
  • ./test-e2e.sh builds a real VM with --preinstall=none in its own LIMA_HOME, and checks what a stub cannot: that root in the guest cannot lift --readonly or write .git, and that a --scratch VM sees nothing of the host.
  • Unit tests are tests/NN-*.sh, run in order in one shell after tests/helpers.sh. A new area gets its own file.
./test.sh        # fast, no VM
./test-e2e.sh    # real VM, needs Lima

Test the shells that matter

macOS still ships bash 3.2, which is stricter about empty array expansion under set -u than modern bash. A change that passes on bash 5 can still break on a stock Mac. The bash:3.2 image has no git, so the tests that need it skip there: add it with apk.

docker run --rm -v "$PWD:/w" -w /w bash:3.2 ./test.sh

# with git, for the tests that need it
docker run --rm -v "$PWD:/w" -w /w bash:3.2 sh -c '
  set -e
  apk add -q git
  git config --global safe.directory "*"
  ./test.sh'

Where things live

FileWhat it is
agent-vm.shThe command: settings, the lib/ loader, starting a VM, the commands. What goes on your PATH.
lib/The rest of the command, one file per concern: mounts, .git protection, env, doctor, setup…
agent-vm.setup.shPackage installation, runs inside the base VM during setup.
install.shFormer installer, now a wrapper for ./agent-vm.sh install.
test.shTest suite. Stub limactl, no VMs, no network. Runs tests/, in order.
test-e2e.shEnd-to-end suite. Builds a real VM in a throwaway LIMA_HOME.
runtime.example.shCommented template for ~/.agent-vm/runtime.sh.
CHANGELOG.mdWhat changed in each release.
release.shChecks, tags and publishes a release. --dry-run first.
www/This website. Astro, static, deployed to GitHub Pages.
www/public/install.shThe curl installer, served at /install.sh.

Before you open a PR

  • Keep it bash 3.2 compatible, and run ./test.sh under bash:3.2 as well as your own shell.
  • Every code path should be safe to re-run: check state before acting rather than assuming a clean machine.
  • New behaviour gets a test in tests/ (a new area gets its own NN-*.sh). The stub limactl makes that cheap.
  • Integrator-facing surfaces (info, env, version) are contracts. Adding keys is fine, changing meanings is not.
  • No secret, token or personal path in a commit, a test fixture or an issue.

Credits

Built on other people’s work.

agent-vm is a thin layer over software other people wrote. Most of what makes it work is on this list.

The machine

  • LimaLinux VMs on macOS, Linux and Windows. The only host dependency, with QEMU on Windows.
  • DebianThe guest distribution. Debian 13.
  • miseRuntime version manager inside the VM.

The agents

Browser access

Maintainer

Built and maintained by Sylvain Zimmer.

@sylvinus

License

MIT: use, copy, modify and redistribute it, as long as the copyright and license notice stay with it. No warranty.

LICENSE