A practical guide to running AI coding agents (Claude Code, Codex, Gemini CLI, etc.) in isolated Linux VMs on macOS, with live bidirectional file sharing and per-sandbox Docker daemons.
- Architecture Overview
- Prerequisites
- Installation
- Core Concepts
- Creating Your First Sandbox
- The Custom Template
- Running Agents in Sandboxes
- Running Multiple Sandboxes Concurrently
- Lifecycle Management
- Snapshots
- Cloning Sandboxes
- Shell Wrapper Script
- Editing Sandbox Files with Zed
- Resource Tuning
- Networking Details
- Troubleshooting
- Reference
macOS Host
┌──────────────────────────────────────────────────────────┐
│ ~/projects/myapp/ ~/projects/sideproject/ │
│ │ │ │
│ │ VirtioFS mount │ VirtioFS mount │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌─────────────────┐ │
│ │ Lima VM "myapp" │ │ Lima VM "side" │ │
│ │ │ │ │ │
│ │ /Users/john/ │ │ /Users/john/ │ │
│ │ projects/myapp │ │ projects/ │ │
│ │ │ │ sideproject │ │
│ │ Docker daemon │ │ Docker daemon │ │
│ │ Agent (claude) │ │ Agent (codex) │ │
│ └──────────────────┘ └─────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ Apple Virtualization.framework (VZ) │
└──────────────────────────────────────────────────────────┘
Each sandbox is a lightweight Linux VM running on Apple's native Virtualization framework. It has:
- Its own filesystem -- the VM sees only the project directory you mount, not your entire home directory.
- Its own Docker daemon -- agents can build images and run containers without touching your host Docker or any other sandbox.
- Live bidirectional file sharing -- VirtioFS mounts your host directory into the guest at the same absolute path. Edits on either side are instantly visible to the other. No sync process, no delay.
- Full isolation -- a rogue
rm -rf /inside the sandbox cannot touch your host (beyond the mounted directory).
- macOS 13 (Ventura) or later
- Apple Silicon or Intel Mac
- Homebrew installed
- ~2 GB RAM per sandbox (configurable)
- The
dockerCLI on your host (only if you want to talk to the in-VM Docker from your host -- optional)
brew install limaVerify:
limactl --version
# lima version 2.1.1 (or similar)Optionally install the Docker CLI (not Docker Desktop) if you want to run docker
commands from your host against the sandbox's daemon:
brew install dockerThat's it. Lima handles everything else -- downloading the Ubuntu image, configuring the VM, setting up SSH.
A Lima instance is a named VM. Each instance has its own disk, CPU/memory allocation, mount configuration, and Docker daemon. Instances persist across reboots until you explicitly delete them.
limactl list # list all instances
limactl list --format json # machine-readable outputA template is a YAML file that describes how to create an instance: what OS
image to use, what to provision, what to mount. Lima ships with built-in templates
(e.g. template:docker), and you can write your own.
Mounts connect host directories to the guest. The key flags:
| Flag | Behavior |
|---|---|
| (default) | Mounts ~ read-only, /tmp/lima writable |
--mount-only <path>:w |
Mount ONLY <path>, writable. Nothing else. |
--mount-only <path> |
Mount ONLY <path>, read-only. |
--mount-none |
No host mounts at all. Fully isolated. |
For agent sandboxing, you almost always want --mount-only <project>:w.
Mounts are not permanent. Unlike Docker sandboxes, a Lima VM is not tied
to a single working directory. You can change, add, or remove mounts at any
time using limactl edit (see Editing a sandbox).
Path preservation: Lima mounts host directories at the same absolute path
inside the VM. If your project is at /Users/john/projects/foo on the host,
it appears at /Users/john/projects/foo inside the VM -- not at /mnt/foo
or some other translated path. This is the same behavior as Docker Sandboxes.
It means error messages, stack traces, config files, and .git paths all
reference paths that are valid on both sides. Agents don't get confused, and
you can copy-paste paths between host and guest without translation.
On macOS 13+, Lima defaults to vmType: vz (Apple Virtualization.framework).
This gives you:
- Native hypervisor performance (no QEMU overhead)
- VirtioFS for fast file mounts
- Rosetta 2 support for running x86_64 binaries in arm64 guests
You don't need to configure this -- it's the default.
You work inside a sandbox by getting a terminal session into the VM. Lima provides several ways to do this -- all of them are SSH under the hood, but Lima manages the keys, ports, and connection details so you never have to.
Interactive shell:
limactl shell myapp bashThis SSHs into the myapp VM and starts bash. You get a full Linux terminal.
Your prompt changes (you'll see john@lima-myapp or similar), and everything
you run is inside the VM. Ctrl-D or exit to leave.
Run a command directly (non-interactive):
limactl shell myapp uname -a
# Linux lima-myapp 6.x.x ...Run an interactive agent directly:
limactl shell --workdir ~/projects/myapp myapp claudeThis is the most convenient form for coding agents. It SSHs in, cds to your
project directory, and launches the agent. The agent's TUI (terminal UI) works
normally -- colors, key bindings, interactive prompts all pass through.
Set the working directory:
The --workdir flag tells Lima to cd into that path inside the VM before
running the command. Since VirtioFS mounts your project at the same absolute
path, --workdir ~/projects/myapp works identically to cd ~/projects/myapp
on your host.
Raw SSH (if you ever need it):
Lima writes a per-instance SSH config file. Use it directly:
# SSH using Lima's config file
ssh -F ~/.lima/myapp/ssh.config lima-myappNote:
limactl show-sshexists but is deprecated. Usessh -F ~/.lima/<name>/ssh.config lima-<name>instead.
You rarely need raw SSH. limactl shell <name> handles everything including
terminal allocation, forwarding your env vars, and proper TTY setup so that
interactive TUI programs (like Claude Code) work correctly.
The simplest way to get a sandboxed Docker environment for a project:
cd ~/projects/myapp
limactl start \
--name myapp \
--mount-only "$(pwd):w" \
template:dockerThis command:
- Creates a VM named
myapp - Downloads an Ubuntu LTS image (cached after first run)
- Installs Docker inside the VM
- Mounts
~/projects/myappinto the VM at the same absolute path, writable - Does NOT mount your home directory or anything else
Wait for it to finish (first run downloads the image; subsequent starts are fast).
Shell into the VM:
limactl shell myapp bashInside the VM:
# Your project files are here, at the same path as on your host
ls ~/projects/myapp
# Docker is running inside the VM
docker run --rm hello-world
# Changes are bidirectional -- create a file
echo "hello from the sandbox" > ~/projects/myapp/sandbox-test.txtBack on your host:
cat ~/projects/myapp/sandbox-test.txt
# hello from the sandboxClean up:
rm ~/projects/myapp/sandbox-test.txtThe built-in template:docker works, but for agent sandboxing you'll want a
custom template that pre-installs the tools your agents need.
The template is in this repo: agent-sandbox.yaml
Copy it to ~/.lima/ (or reference it by path when creating sandboxes):
cp agent-sandbox.yaml ~/.lima/agent-sandbox.yamlWhat it provisions:
- VZ + VirtioFS -- Apple Virtualization.framework with fast mounts
- Docker (rootless) -- per-sandbox Docker daemon
- Build tools -- build-essential, git, curl, wget, jq, ripgrep, fd-find
- PostgreSQL 18 client -- psql, pg_dump, etc.
- yq -- YAML/JSON/XML processor
- Node.js 22 -- needed by most agents
- No default mounts -- caller specifies
--mount-only - Port forwarding disabled -- prevents collisions between sandboxes (Docker socket forwarding is kept)
cd ~/projects/myapp
limactl start \
--name myapp \
--mount-only "$(pwd):w" \
~/.lima/agent-sandbox.yamlFirst boot takes 2-3 minutes (provisioning). Subsequent limactl start myapp
takes ~5 seconds.
All agent interaction happens through a terminal session inside the VM. You
run limactl shell <name> ... from your macOS terminal; Lima SSHs into the VM
transparently and gives you a Linux shell (or launches the agent directly).
Colors, key bindings, and interactive TUIs all work normally -- the experience
is identical to running the agent on your host, except everything is sandboxed.
After creating a sandbox, install the agent(s) you want. These are installed inside the VM and persist across stop/start cycles (they only go away if you delete or factory-reset the sandbox).
# Shell into the sandbox
limactl shell myapp bash
# Inside the VM -- install whichever agents you use:
sudo npm install -g @anthropic-ai/claude-code # Claude Code
sudo npm install -g @openai/codex # Codex
sudo npm install -g @google/gemini-cli # Gemini CLI
sudo npm install -g opencode-ai # OpenCode
exitYou can also add these to the template's provision section so that every new
sandbox comes with agents pre-installed (see The Custom Template).
The most direct approach -- one command from your macOS terminal:
# Launch Claude Code, already cd'd to your project
limactl shell --workdir ~/projects/myapp myapp claudeThis SSHs into the VM, changes to the project directory, and starts the agent.
You're now interacting with Claude Code running inside the sandbox. When you
quit the agent (/exit, Ctrl-C, etc.), you're back in your macOS terminal.
The same pattern works for any agent:
limactl shell --workdir ~/projects/myapp myapp codex
limactl shell --workdir ~/projects/myapp myapp gemini
limactl shell --workdir ~/projects/myapp myapp opencodeIf you prefer to poke around or do setup before launching the agent:
# Get a bash shell inside the sandbox
limactl shell myapp bash
# You're now inside the VM (prompt shows lima-myapp)
cd ~/projects/myapp # same absolute path as host
docker ps # sandbox's own Docker daemon
claude # launch agent
# When done with the agent, you're back in the VM's bash
# Ctrl-D or 'exit' to return to your macOS terminalOAuth-based agents (Claude Code, Gemini): Follow the interactive browser flow on first launch. The auth token is stored inside the VM and persists across stop/start.
API-key-based agents: Pass keys from your host without storing them in the VM:
# Forward a single key
limactl shell --workdir ~/projects/myapp myapp \
env ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" claudeOr set keys permanently inside the sandbox:
limactl shell myapp bash
echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.bash_profile
echo 'export OPENAI_API_KEY="sk-..."' >> ~/.bash_profile
source ~/.bash_profileYou can open as many terminal sessions into the same sandbox as you want. Each
limactl shell myapp bash opens a new SSH connection to the same VM. This is useful
when an agent is running in one terminal and you want to inspect something in
another:
# Terminal 1: agent is running
limactl shell --workdir ~/projects/myapp myapp claude
# Terminal 2: check what containers the agent started
limactl shell myapp docker ps
# Terminal 3: watch resource usage
limactl shell myapp htopThis is where Lima really shines for your workflow. Each instance is fully independent -- its own VM, own Docker daemon, own disk.
# Terminal 1: main project
limactl start --name main \
--mount-only "$HOME/projects/mainapp:w" \
~/.lima/agent-sandbox.yaml
limactl shell --workdir ~/projects/mainapp main claude
# Terminal 2: side project A (different checkout of same repo)
limactl start --name side-a \
--mount-only "$HOME/projects/mainapp-feature-x:w" \
~/.lima/agent-sandbox.yaml
limactl shell --workdir ~/projects/mainapp-feature-x side-a claude
# Terminal 3: side project B
limactl start --name side-b \
--mount-only "$HOME/projects/sideproject:w" \
~/.lima/agent-sandbox.yaml
limactl shell --workdir ~/projects/sideproject side-b codex| Resource | Isolated per sandbox? | Notes |
|---|---|---|
| Filesystem (non-mount) | Yes | Each VM has its own root disk |
| Mounted project dir | Shared with host | That's the point -- bidirectional |
| Docker daemon | Yes | Each VM runs its own Docker |
| Docker images/containers | Yes | Built in one sandbox, invisible to others |
| Network | Yes | Each VM has its own network stack |
| Installed packages | Yes | apt-get install in one doesn't affect others |
| CPU/memory | Yes | Each VM has its own allocation |
limactl list
# NAME STATUS SSH VMTYPE CPUS MEMORY DISK DIR
# main Running 127.0.0.1:... vz 4 4GiB 50GiB ~/.lima/main
# side-a Running 127.0.0.1:... vz 4 4GiB 50GiB ~/.lima/side-a
# side-b Running 127.0.0.1:... vz 4 4GiB 50GiB ~/.lima/side-bEach sandbox exposes its Docker socket at a unique path:
# Talk to main's Docker
export DOCKER_HOST=$(limactl list main --format 'unix://{{.Dir}}/sock/docker.sock')
docker ps
# Talk to side-a's Docker
export DOCKER_HOST=$(limactl list side-a --format 'unix://{{.Dir}}/sock/docker.sock')
docker psOr use Docker contexts:
docker context create main --docker "host=unix://$(limactl list main --format '{{.Dir}}')/sock/docker.sock"
docker context create side-a --docker "host=unix://$(limactl list side-a --format '{{.Dir}}')/sock/docker.sock"
docker --context main ps
docker --context side-a ps# Stop a sandbox (preserves all state)
limactl stop myapp
# Start it back up (fast -- no reprovisioning)
limactl start myapp
# Stop all running sandboxes
limactl list -q --filter '.status == "Running"' | xargs limactl stop# Delete a sandbox and its VM disk
limactl delete myapp
# Delete a stopped sandbox (fails if running)
limactl delete myapp
# Force delete a running sandbox
limactl delete --force myappDeleting a sandbox removes the VM disk, Docker images/containers inside it, and installed packages. Your host project directory is untouched.
If a sandbox gets into a bad state but you want to keep the name/config:
limactl factory-reset myapp
limactl start myappThis re-provisions from scratch while keeping the same mount configuration.
A sandbox is not permanently tied to one project directory. You can change
mounts, CPU, memory, and other settings on a stopped instance using
limactl edit:
limactl stop myapp
# Re-point at a different project directory
limactl edit myapp --mount-only ~/projects/other-project:w
# Or add a second mount alongside the existing one
limactl edit myapp --mount ~/projects/shared-libs:w
# Change resources
limactl edit myapp --cpus 8 --memory 8
# Remove all mounts (fully isolated VM)
limactl edit myapp --mount-none
limactl start myappYou can also open the instance YAML directly in your editor for full control:
limactl stop myapp
limactl edit myapp # opens the instance YAML in $EDITOR
limactl start myappThis makes it easy to reuse a sandbox that already has your tools installed -- just swap the mounted directory instead of creating a new VM from scratch.
# SSH into a sandbox
limactl shell myapp bash
# Run a single command
limactl shell myapp docker ps
limactl shell myapp df -h
limactl shell myapp free -mSnapshots let you checkpoint a sandbox's state and roll back. Useful before letting an agent make sweeping changes to installed packages or Docker state inside the VM.
Note: Snapshots capture the VM disk state, not your mounted host files (those are on your host filesystem -- use git for that).
# Create a snapshot before a big refactor
limactl snapshot create myapp --tag before-refactor
# List snapshots
limactl snapshot list myapp
# Agent made a mess of the VM's installed packages? Roll back.
limactl stop myapp
limactl snapshot apply myapp --tag before-refactor
limactl start myapp
# Delete a snapshot you no longer need
limactl snapshot delete myapp --tag before-refactor# On host: create a git branch for the agent's work
cd ~/projects/myapp
git checkout -b agent/feature-x
# Snapshot the VM state
limactl snapshot create myapp --tag pre-agent
# Let the agent loose
limactl shell --workdir ~/projects/myapp myapp claude
# If you like the results: commit on host, delete snapshot
git add -A && git commit -m "agent: implement feature X"
limactl snapshot delete myapp --tag pre-agent
# If you don't: revert both
git checkout main && git branch -D agent/feature-x
limactl stop myapp
limactl snapshot apply myapp --tag pre-agent
limactl start myapplimactl clone creates a new VM from the current state of an existing one.
The clone gets a full copy of the source's disk -- all installed packages,
Docker images, agent configurations, and tool setups carry over. This is much
faster than provisioning from scratch when you want multiple sandboxes with the
same base environment.
# Clone an existing sandbox
limactl clone myapp myapp-copy
# Clone and re-point at a different project
limactl clone myapp feature-x --mount-only ~/projects/feature-x:w
# Clone, change resources, and start immediately
limactl clone myapp lightweight --cpus 2 --memory 2 --startSet up one sandbox with all your tools, then clone it for each new project instead of provisioning from scratch every time:
# One-time: create and configure a "golden" sandbox
limactl start --name golden \
--mount-none \
~/.lima/agent-sandbox.yaml
# Install extra tools, configure shell, etc.
limactl shell golden bash
# ... install things, then exit
# For each new project: clone the golden image
limactl clone golden project-a \
--mount-only ~/projects/project-a:w \
--start
limactl clone golden project-b \
--mount-only ~/projects/project-b:w \
--startThere's no flag to clone from a snapshot tag directly. Apply the snapshot first, then clone:
# Restore the source to a known-good state
limactl stop myapp
limactl snapshot apply myapp --tag clean-slate
# Clone from that state
limactl clone myapp new-sandbox --mount-only ~/projects/new:w
# Restore the source to its latest state
limactl snapshot apply myapp --tag latest
limactl start myappThe script is in this repo: sandbox.sh
Copy it somewhere on your $PATH:
cp sandbox.sh ~/bin/sandbox
chmod +x ~/bin/sandboxIt creates a sandbox if it doesn't exist, starts it if stopped, and forwards common API key env vars automatically. What it does:
- Creates an instance using the template (if it doesn't exist)
- Starts the instance (if stopped)
- Forwards
ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY, etc. - Shells in with
--workdirset to the project directory
Usage:
# Create sandbox and drop into a shell
cd ~/projects/myapp
sandbox myapp
# Create sandbox and run claude directly
sandbox myapp claude
# Create sandbox for a specific directory
sandbox side-a ~/projects/other-checkout claude
# Override resources
SANDBOX_CPUS=2 SANDBOX_MEMORY=2 sandbox small-projectZed's remote development feature lets you edit files inside a Lima sandbox from your macOS host over SSH. Zed runs its UI locally on your Mac while language servers, terminals, and tasks run inside the VM. This means you get full LSP support -- completions, diagnostics, go-to-definition -- powered by the toolchains installed in the sandbox, not on your host.
With the default VirtioFS workflow you edit files on your host and agents run in the VM. That works, but your local editor uses its own LSP servers. Those may not match the VM's toolchain versions, may not see VM-only dependencies, or may not even be installed on your Mac at all.
With Zed remote development, the language servers, terminal, and task runner all run inside the sandbox. Zed's UI stays on your Mac -- fast and native -- while the heavy lifting happens in the VM.
Lima manages SSH keys and ports for each VM and writes a per-instance SSH
config file at ~/.lima/<name>/ssh.config. You can use this to connect Zed
without manually looking up ports or keys.
Option A: Command line (quickest)
# Look up the SSH port from Lima's config
grep Port ~/.lima/myapp/ssh.config
# e.g. Port 52163
# Open the project in Zed (adjust user, port, and path)
zed ssh://john@127.0.0.1:52163/Users/john/projects/myappOption B: Include Lima's SSH config in your own (recommended)
Add an Include directive at the top of ~/.ssh/config (before any
Host blocks) so that your SSH client -- and Zed -- can resolve Lima's host
aliases automatically:
# ~/.ssh/config -- add this at the very top
Include ~/.lima/myapp/ssh.config
Now you can connect from the command line:
zed ssh://lima-myapp/Users/john/projects/myappOr from the Zed UI:
- Press
Ctrl+Cmd+Shift+O(Open Remote Project) - Click "Connect New Server"
- Enter
lima-myappas the host - Choose your project path (e.g.
/Users/john/projects/myapp)
Note: The SSH port can change when a sandbox is stopped and restarted. The
Includeapproach handles this automatically because Lima updates its config file on each start. If you hard-code azed ssh://...:<port>/...command, you'll need to look up the new port after a restart.
Option C: Zed settings file
Add the connection to your Zed settings (Cmd+,) so it persists across
sessions. The -F flag tells SSH to read Lima's config file directly:
{
"ssh_connections": [
{
"host": "lima-myapp",
"args": ["-F", "~/.lima/myapp/ssh.config"],
"projects": [{ "paths": ["/Users/john/projects/myapp"] }],
"nickname": "myapp sandbox"
}
]
}The first time Zed connects, it downloads and installs a headless server
binary inside the VM (stored in ~/.zed_server). This happens once per Zed
version update. If the VM has restricted internet access, add
"upload_binary_over_ssh": true to the connection settings and Zed will
upload the binary from your Mac instead.
| Component | Where it runs |
|---|---|
| Zed UI, rendering, keybindings | Your Mac (local) |
| Language servers (LSP) | Inside the Lima VM |
| Terminals opened in Zed | Inside the Lima VM |
| Tasks (build, test, lint) | Inside the Lima VM |
| File watching / indexing | Inside the Lima VM |
| AI features (Agent, Inline Assist) | Your Mac (talks to LLM providers) |
If an agent or build script starts a dev server inside the sandbox and you want to access it from your Mac's browser, add port forwards in your Zed connection settings:
{
"ssh_connections": [
{
"host": "lima-myapp",
"args": ["-F", "~/.lima/myapp/ssh.config"],
"projects": [{ "paths": ["/Users/john/projects/myapp"] }],
"port_forwards": [{ "local_port": 3000, "remote_port": 3000 }]
}
]
}- Extensions are synced. Zed extensions installed on your Mac are propagated to the remote server automatically. Language support, formatters, and linters work without extra setup.
- Multiple sandboxes. Add one
ssh_connectionsentry per sandbox. Give each anicknameto tell them apart in the Zed UI. - VirtioFS still works in parallel. The project directory is still mounted via VirtioFS, so you can open the same files directly on your Mac with any other editor at the same time. Edits from either side are visible instantly.
- Agent + Zed side by side. A practical workflow: run an agent in one
terminal (
limactl shell --workdir ~/projects/myapp myapp claude) while editing the same project in Zed via remote development. The agent's file changes appear immediately in Zed, and Zed's LSP diagnostics update in real time.
Override CPU and memory at creation time:
limactl start --name myapp \
--cpus 2 --memory 2 \
--mount-only "$(pwd):w" \
~/.lima/agent-sandbox.yaml| Workload | CPUs | Memory | Disk |
|---|---|---|---|
| Light agent (editing, no builds) | 2 | 2 GiB | 20 GiB |
| Agent with Docker builds | 4 | 4 GiB | 50 GiB |
| Agent with large test suites | 4-8 | 8 GiB | 50 GiB |
| Multiple concurrent sandboxes | 2 each | 2-4 GiB each | 30 GiB each |
macOS manages the total allocation across VMs. On a 16 GB Mac, you can comfortably run 2-3 sandboxes at 4 GiB each. On a 32 GB+ Mac, 4-6 is fine.
# From host: see all VMs
limactl list
# Inside a sandbox
limactl shell myapp free -m
limactl shell myapp df -h
limactl shell myapp docker system df# Inside the sandbox: prune Docker
limactl shell myapp docker system prune -af
# From host: trim the VM disk
limactl shell myapp sudo fstrim -aEach VM gets outbound internet access through the host. The guest can curl,
apt-get install, docker pull, etc. No configuration needed.
By default, Lima auto-forwards every TCP/UDP port that a guest process listens on to the same port on the host. This is convenient for single-VM setups but causes collisions when running multiple sandboxes -- if three sandboxes each run postgres on port 5432, only one can bind the host's port 5432.
The custom template in this guide disables automatic port forwarding with this rule:
portForwards:
# ... Docker socket forward (kept) ...
# Block all automatic TCP/UDP port forwarding
- guestIP: "127.0.0.1"
guestPortRange: [1, 65535]
hostIP: "127.0.0.1"
proto: "any"
ignore: truePort forwarding rules are evaluated sequentially. Lima internally appends a
catch-all "forward everything" rule at the end. By placing an ignore: true
rule before it, we suppress all automatic forwarding. The Docker socket forward
(which uses unix sockets, not TCP) is unaffected and still works.
What this means in practice:
- Containers inside a sandbox talk to each other normally via
localhostwithin the VM (e.g. your app container connects tolocalhost:5432for postgres -- this all happens inside the VM's network namespace). - Nothing leaks onto the host's ports. No collisions between sandboxes.
- The Docker socket is still forwarded, so
dockerCLI works from the host.
If an agent runs a dev server and you need to access it from your host browser, add specific forwards before the ignore rule. Rules are matched top-down, so explicit forwards take priority:
portForwards:
# Docker socket
- guestSocket: "/run/user/{{.UID}}/docker.sock"
hostSocket: "{{.Dir}}/sock/docker.sock"
# Explicitly forward port 3000 for this sandbox
- guestPort: 3000
hostPort: 3000
# Block everything else
- guestIP: "127.0.0.1"
guestPortRange: [1, 65535]
hostIP: "127.0.0.1"
proto: "any"
ignore: trueTo avoid editing the template for each sandbox, you can also use limactl edit
to add port forwards to a specific instance after creation:
limactl stop myapp
limactl edit myapp # opens the instance YAML in your editor
limactl start myappThe guest can resolve host.docker.internal to reach services on your Mac (e.g.
a local API server). This is configured in the template's hostResolver section.
The provisioning might still be running. Check:
limactl shell myapp tail -f /var/log/cloud-init-output.logOr wait and retry. The probe in the template waits up to 30s for Docker.
Make sure you're using VZ + VirtioFS (the default on macOS 13+). Verify:
limactl list myapp --format '{{.VMType}}'
# Should print: vzIf it shows qemu, delete and recreate with --vm-type vz.
You tried to create a sandbox with a name that's already taken:
limactl list # see existing instances
limactl start myapp # start existing one
limactl delete myapp # or delete and recreatelimactl factory-reset myapp
limactl start myappThis re-provisions from scratch. Your mounted host files are unaffected.
Check DNS inside the VM:
limactl shell myapp ping -c2 8.8.8.8
limactl shell myapp curl -I https://github.comIf DNS fails but ping works, check /etc/resolv.conf inside the VM.
# Check what's using space
limactl shell myapp df -h
limactl shell myapp docker system df
# Clean up
limactl shell myapp docker system prune -af
limactl shell myapp sudo apt-get clean
limactl shell myapp sudo fstrim -aIf you use a terminal emulator like Ghostty, your host sets TERM=xterm-ghostty
(or similar). SSH forwards this to the guest, but the Ubuntu VM doesn't have
the matching terminfo entry, causing garbled output or warnings like
tput: unknown terminal "xterm-ghostty".
Fix (one-time, per sandbox):
# Base64-encode the terminfo on the host, decode and install in the VM
infocmp -x xterm-ghostty 2>/dev/null | base64 | \
xargs -I{} limactl shell myapp bash -c 'echo {} | base64 -d | tic -x -'Note: piping directly into limactl shell doesn't work (limactl shell
doesn't forward stdin to the remote command). The base64 approach embeds
the data in the command string instead.
This persists across stop/start. The sandbox.sh wrapper
script handles this automatically -- it detects missing terminfo and
installs it before shelling in.
Quick workaround (no install needed): override TERM when shelling in:
TERM=xterm-256color limactl shell myapp bashThis isn't Ghostty-specific -- any terminal whose terminfo is missing from the guest has the same issue.
Verify the mount path matches exactly. Lima mounts at the same absolute path:
# If your host directory is /Users/john/projects/myapp
# It appears in the VM at /Users/john/projects/myapp
limactl shell myapp ls /Users/john/projects/myapp# Instance management
limactl start --name <n> --mount-only <path>:w <template> # create
limactl start <name> # start existing
limactl stop <name> # stop
limactl delete <name> # delete
limactl delete --force <name> # force delete
limactl list # list all
limactl factory-reset <name> # reprovision
# Shell access
limactl shell <name> bash # interactive shell
limactl shell --workdir <dir> <name> <cmd> # run command
limactl shell <name> env KEY=VAL <cmd> # with env vars
# Editing and cloning
limactl edit <name> --mount-only <path>:w # change mounts
limactl edit <name> --cpus <n> --memory <n> # change resources
limactl clone <old> <new> # clone a sandbox
limactl clone <old> <new> --mount-only <path>:w # clone + new mount
# Snapshots
limactl snapshot create <name> --tag <tag>
limactl snapshot list <name>
limactl snapshot apply <name> --tag <tag> # (must stop first)
limactl snapshot delete <name> --tag <tag>
# Docker (from host, talking to sandbox's Docker)
export DOCKER_HOST=$(limactl list <name> --format 'unix://{{.Dir}}/sock/docker.sock')
docker ps- Official docs: https://lima-vm.io/docs/
- AI agents guide: https://lima-vm.io/docs/examples/ai/
- Mount types: https://lima-vm.io/docs/config/mount/
- VM types (VZ): https://lima-vm.io/docs/config/vmtype/vz/
- Networking: https://lima-vm.io/docs/config/network/
- Templates: https://lima-vm.io/docs/templates/
- Snapshots: https://lima-vm.io/docs/reference/limactl_snapshot/
- Apple Virtualization.framework: the hypervisor
- VirtioFS: the filesystem sharing mechanism (bidirectional, zero-copy)
- vfkit / Code-Hex/vz: the Go bindings Lima uses under the hood
- Ubuntu LTS: the default guest OS