在 MacOS 上搭建易于使用的 Linux 虚拟机,用于隔离 LLM 代理程序。
Vibe is a quick, zero-configuration way to spin up a Linux virtual machine on Mac to sandbox LLM agents:
…
On my M1 MacBook Air it takes ~10 seconds to boot.
Dependencies:
--yolo, since they're not always interrupting you to approve their commands.codex without --yolo and seeing it reading files outside of the directory I started it in --- not cool, bro.)I'm using virtual machines rather than containers because:
Finally, as a matter of taste and style:
Vibe is a single binary built with Rust, with a bundled networking helper written in Go.
Download the latest binary built by GitHub actions and put it somewhere on your $PATH:
curl -LO https://github.com/lynaghk/vibe/releases/download/latest/vibe-macos-arm64.zip
unzip vibe-macos-arm64.zip
mkdir -p ~/.local/bin
mv vibe ~/.local/bin
export PATH="$HOME/.local/bin:$PATH"
If you use mise-en-place:
mise use github:lynaghk/vibe@latest
I'm not making formal releases or keeping a change log. I recommend reading the commit history and pinning to a specific version.
If you're building from a checkout, use mise to get the Rust and Go compilers:
mise install --locked
cargo build --locked
Vibe only does two things:
When you run vibe in a project directory, it copies the default template (~/.cache/vibe/default.raw) to .vibe/instance.raw, boots it up, and attaches your terminal to this VM.
When you exit this shell, the VM is shutdown.
The disk state persists until you delete it.
Where does this default.raw raw disk image come from?
When you first run vibe, a Debian Linux base image is downloaded and all of the provisioning scripts are run against it.
As a thoughtful person who reads the README, you'll probably appreciate the ability to create custom template images by running:
vibe provision --image my-template @rust @codex my-custom-script.sh
(Omitting --image provisions the default template.)
Scripts are run in order, and those prefixed with @ are resolved against the built-in scripts shipped with Vibe.
In my-custom-script.sh I set up my tmux keybindings, favorite shell customizations, etc.
In a project directory, you can then run vibe --image my-template to use this template image (this flag is ignored if .vibe/instance.raw already exists).
The base provisioning script is always run when provisioning to install basic tools like gcc, mise-en-place, ripgrep, etc.
If you don't want this, you can make your own .raw disk images and copy them into ~/.cache/vibe/ to use them as templates.
…
Vibe VMs can reach the host at 192.168.5.2.
The host cannot reach the Vibe VMs, nor can VMs reach each other.
DNS is handled by the host resolver, so VMs get VPN and split-DNS compatibility.
This networking is based on a bundled gVisor/Lima-style user-mode network helper process, vibe-usernet, which is spawned automatically when you run vibe.
I ended up with this solution because Apple's VZNATNetworkDeviceAttachment lost packets and VMs got wrecked whenever host networking changed (e.g., switching between wifi/ethernet/VPN). VZBridgedNetworkDeviceAttachment requires kowtowing to acquire the restricted com.apple.vm.networking entitlement, which I'm not interested in doing.
If you have suggestions for how to improve networking capabilities, please open an issue or PR!
The default VM disk is 100 GiB, but since Apple Filesystem is copy-on-write and doesn't count zeros, disk space is only used when you actually write new blocks.
You can use du -h to see how much space is actually consumed:
$ ls -lah .vibe/instance.raw
-rw-r--r-- 1 dev staff 100G Feb 11 21:57 .vibe/instance.raw
$ du -h .vibe/instance.raw
2.5G .vibe/instance.raw
If you need even more space within the VM, e.g., 500 GiB, run truncate -s 500G .vibe/instance.raw on your Mac and then within the VM run growpart /dev/vda 1 && resize2fs /dev/vda1.
MacOS only lets binaries signed with the com.apple.security.virtualization entitlement run virtual machines, so vibe checks itself on startup and, if necessary, signs itself using codesign. SeCuRiTy!
Debian "nocloud" is used as a base image because it boots directly to a root prompt. The other images use cloudinit, which I found much more complex:
systemd-networkd-wait-online.service).Claude Code requires both your ~/.claude folder (shared in the VM by default) and also the ~/.claude.json file for auth credentials and session history.
VirtioFS only works with folders, so there's not a nice way to "mount" the latter inside the VM.
Here's what I recommend:
Run claude and login. (You can do this in a VM or on your actual machine if you trust claude.)
mv ~/.claude.json ~/.claude/dot_claude_dot_json_should_have_been_here.json
make a shell alias/script to launch Vibe as:
vibe --send "ln -fs ~/.claude/dot_claude_dot_json_should_have_been_here.json ~/.claude.json" \
--send "IS_SANDBOX=1 claude --allow-dangerously-skip-permissions --dangerously-skip-permissions"
Here's what I tried before writing this solution:
Sandboxtron - My own little wrapper around Mac's sandbox-exec.
Turns out both Claude Code and Codex rely on this as well, and MacOS doesn't allow creating a sandbox from within a sandbox.
I considered writing my own sandboxing rules and running the agents --yolo, but didn't like the risk of configuration typos and/or Mac sandbox escapes (there are a lot --- I'm not an expert, but from this HN discussion I figured virtualization would be safer).
Lima, quick Linux VMs on Mac. I wanted to like this, ran into too many issues in first 30 minutes to trust it:
limactl start --mount foo and then later limactl start --mount bar, both foo and bar will be mounted.--mount CLI flag always mounts at the same path in the guest. If you want to mount at a different path, you have to do that via YAML.Vagrant - I fondly remember using this back in the early 2010's, but based on this 2025 Reddit discussion it seemed like running it on an ARM-based Mac was A Project and so I figured it'd be easier to roll my own thing.
Tart - I found this via some positive HN comments, but unfortunately wasn't able to run the release binary from their GitHub because it's not signed.
They apparently hack around that when installing with homebrew, but I don't use homebrew either.
I tried cloning the repo and compiling myself, but the build failed with lots of language syntax errors despite the repo SHA is the same as one of their releases.
I assume this is a Swift problem and not Tart's, since this sort of mess happens most times when I try to build Swift. ¯\_(ツ)_/¯
OrbStack - This looked nice, but seems mostly geared towards container stuff. It runs a single VM, and I couldn't figure out how to have this VM run without my entire disk mounted inside of it. I didn't want to run agents via containers, since containers aren't security boundaries.
Apple Container Framework - This looks technically promising, as it runs every container within a lightweight VM. Unfortunately it requires MacOS 26 Tahoe, which wrecks window resizing, adds useless icons everywhere, and otherwise seems to be a mess. Sorry excellent Apple programmers and hardware designers, I hope your management can reign in the haute couture folks before we all have to switch to Linux for professional computing.
QEMU - The first prototype of this app was a single bash script wrapping qemu. This worked swimmingly, except for host/guest directory sharing, which ended up being a show-stopper. This is because QEMU doesn't support virtiofs on Mac hosts, it only supports "9p", which is way slower --- e.g., mise use node@latest takes > 10 minutes on 9p and 5 seconds on virtiofs.
I wrote this software for myself, and I'm open to pull requests and otherwise collaborating on features that I'd personally use:
vibe against a disk image that's already running should connect to the already-running VMvibe command--expect and --send actions have completed--plan flag which pretty-prints a CLI invocation with all of the default暂无开放 Issues,或尚未同步最近议题。