Model or dataset
lynaghk/vibe avatar
lynaghk/vibe

vibe: a ~2000-line Rust tool that boots a Debian VM per project directory

Easy Linux virtual machine on MacOS to sandbox LLM agents.

952 stars53 forksRustMIT

At a glance

What is it?
vibe copies a raw disk image into .vibe/instance.raw, boots it under Apple's virtualization framework, and attaches your terminal to it. It is a sandbox for LLM agents that run with --yolo, and its mount table is the whole security model.
Who is it for?
Adopt vibe if you run coding agents with --yolo on an ARM Mac and want the agent's blast radius limited to a disk image you can delete. Do not adopt it if you need x86 guests, a formal release process, or a sandbox that hides your home directory by default, because the default mount table shares ~/.claude, ~/.codex, ~/.gemini, ~/.pi, ~/.m2 and ~/.cargo/registry into the guest read-write.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 15 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem: agents that read outside the directory you started them in

The README states the motivation directly: the author was running codex without --yolo and saw it reading files outside the directory it was launched from. The two obvious fixes both have costs. Approving every command interrupts the agent constantly, which is why people reach for --yolo in the first place. Running the agent unsandboxed on the host means an agent that decides to install a toolchain, delete a cache, or follow a path it found in a file can touch anything your user account can touch.

vibe targets the narrow case of a developer on an ARM Mac who wants the agent to have a real Linux userspace with root, package installation, and no approval prompts, while the files the agent can actually see are enumerated in advance. The README frames the control as a mount table: you decide what is shared into the guest, and therefore what the agent and the upstream model provider can reach. This is a per-project tool. You cd into a repository, run vibe, and get a root shell in a Debian VM whose view of your machine is a short list of bind mounts.

The audience is correspondingly narrow. MacOS 13 or later on Apple silicon, a network connection on first run, and nothing else. No Linux host support, no Windows, no Intel Macs.

Two verbs: run a raw image, or provision one

The README says vibe only does two things. It runs VMs from a raw disk image file, and it provisions VMs by booting a base raw disk image, running scripts inside it, then saving the result as a template. Everything else in the CLI is options on those two operations.

Running is the default mode. In a project directory, vibe copies the default template at ~/.cache/vibe/default.raw to .vibe/instance.raw, boots it, and attaches your terminal. When you exit that shell the VM shuts down, but the disk state persists until you delete it. That persistence is the point: your apt installs, your mise toolchains, and your shell history survive across sessions, so the second boot is not a fresh setup.

The template is built lazily. On the first run, a Debian base image is downloaded and every provisioning script is run against it to produce default.raw. Subsequent runs skip that work. The README reports roughly 10 seconds to boot on an M1 MacBook Air, which is a claim about the author's machine, not a benchmark you can assume transfers.

Provisioning is the second verb and it is where the tool becomes interesting for anyone with opinions about their shell. `vibe provision --image my-template @rust @codex my-custom-script.sh` builds a named template from a list of scripts. Scripts run in order, and names prefixed with @ resolve against the built-in scripts shipped with Vibe. The base provisioning script is always run, installing gcc, mise-en-place, ripgrep and similar tools, unless you bypass provisioning entirely by making your own .raw disk image and dropping it into ~/.cache/vibe/.

The mount table is the sandbox, and it is wider than you think

The README's example output shows what a default run shares. Your project directory appears at /root/my-project read-write. So does a guest mise cache, ~/.m2, ~/.cargo/registry, and the config directories for codex, claude, gemini and pi. All read-write. That list is not incidental: it is how the agent gets your existing credentials and tool caches without you re-authenticating inside the VM.

It also means the default configuration is not a hermetic sandbox. Anything an agent writes into ~/.claude or ~/.codex on the host persists after the VM is gone, and those directories are exactly where agent tooling stores configuration that later sessions read. If your threat model is a malicious model output, the default mounts are a wider channel than the project directory alone. The README is upfront that you control what is shared, and the flag to do so is `--no-default-mounts`, which disables all default mounts including the .git and .vibe project subfolder masking. That masking detail matters and is easy to miss: by default, some paths inside your project are hidden from the guest, and turning off defaults turns that off too. The README does not spell out the masking rules in the material available here, so treat the exact set of hidden subfolders as something to verify in the source rather than assume.

Adding a mount is explicit: `--mount HOST_PATH:GUEST_PATH[:read-only | :read-write]`, defaulting to read-write, and erroring if the host path does not exist. Environment variables cross only when named: `--env NAME` exports a host variable into the VM and errors if it is unset or empty. That erroring behaviour is a small but real design choice. A missing variable fails the run instead of silently exporting an empty string into the guest.

Networking, sizing, and the bundled Go helper

The guest gets a network stack chosen by `--network`, with nat as the default and vznat as the alternative. The README describes nat as Vibe's bundled user-mode network stack and vznat as Apple's VZNATNetworkDeviceAttachment. The bundled stack is a Go program, /helpers/vibe-usernet, shipped alongside the Rust binary. That is worth noting for anyone auditing or packaging the tool: despite the README's framing of a single Rust binary with only Objc2 and lexopt as dependencies, the distributed artifact includes a Go component, and the install instructions ship both.

Sizing is fixed at launch. `--cpus` defaults to 2 and `--ram` takes megabytes with a default of 2048. There is no mention of resizing a running instance or of ballooning. If a build needs more memory than 2 GB, you pass the flag at boot time, and the choice is baked into that session.

Login actions are the scripting surface for the run mode rather than the provisioning mode. `--script PATH_TO_SCRIPT` runs a script in the VM and stops if it exits non-zero. `--send SOME_COMMAND` types a command followed by a newline into the VM. `--expect STRING [timeout-...]` waits for output, with the timeout argument truncated in the README text available here. These are repeatable and execute in order after root login, which makes vibe usable as a driver for automated agent runs, not just an interactive shell. The ordering guarantee is stated; the exact timeout syntax is not fully visible in the material, so check `vibe --help` on the binary you download.

Getting it onto your PATH, and what you are actually pinning

Install is a download and a move. The README gives the curl line for vibe-macos-arm64.zip from the latest release tag, then unzip, `mkdir -p ~/.local/bin`, `mv vibe ~/.local/bin`, and a PATH export. If you use mise-en-place, there is a one-liner: `mise use github:lynaghk/vibe@latest`. Building from a checkout uses `mise install --locked` followed by `cargo build --locked`.

The versioning story is the part to read carefully. The README states plainly that the author is not making formal releases or keeping a change log, and recommends reading the commit history and pinning to a specific version. The release tags in the repository follow a date-and-SHA pattern, for example 2026-08-31-0858d69, with a moving latest tag that the install command points at. `--version` prints the commit SHA and build date, which is the only reliable way to know what you are running after the fact.

This is a deliberate trade. You get a small tool that tracks its author's own workflow, and you give up the release discipline that would let you reason about upgrades from a changelog. For a personal sandbox on one laptop that is a reasonable exchange. For a team standardising on it, the pinning burden lands on you, and the `@latest` in both install paths is the opposite of what the README recommends.

Where vibe is the wrong tool

The dependency list is the first boundary. An ARM-based Mac running MacOS 13 or higher. There is no x86 guest, no Linux host, no remote execution. If your CI runs on Linux, vibe does not follow you there, and the per-project .vibe/instance.raw model does not map onto an ephemeral build runner.

The second boundary is the provisioning model itself. Templates are raw disk images built by booting a base image and running scripts. There is no image registry, no content-addressed cache beyond the local ~/.cache/vibe directory, and no signature or provenance check described. Sharing a template with a colleague means sending them a .raw file or sending them the script list and having them rebuild it. Rebuilding means a network download and a full provisioning pass, which the README notes is required on first run.

The third is the security boundary, and it cuts against the tool's own pitch. Virtualization is described as more secure against malicious escapes than containers or the MacOS sandbox framework, and that reasoning is sound as far as it goes. But the default mount table hands the guest read-write access to agent config directories and package caches on the host. A VM that shares ~/.claude read-write is not isolating the agent from your credentials; it is giving the agent a persistent, host-visible place to write. If your concern is a model provider exfiltrating data, the mounts you leave on are the exfiltration surface, and the default set is generous.

Finally, the README's own note that `--image` is ignored if .vibe/instance.raw already exists is a real footgun. You provision a new template, run vibe with it, and nothing changes because an instance from a previous session is still on disk. The fix is deleting the instance file, which the README does not spell out in the material here.

The container comparison, taken seriously

The README gives two reasons for choosing virtual machines over containers. The first is that virtualization is more secure against malicious escapes than containers or the MacOS sandbox framework. The second is that containers on MacOS require spinning up a virtual machine anyway, so the container layer buys you less isolation on this platform than it would on Linux.

The second argument is the stronger one and it is specific to the host. On Linux, a container runtime shares the host kernel and you get a smaller footprint with a thinner isolation boundary. On MacOS, Docker Desktop and its relatives are already running a Linux VM underneath. Adding a container layer on top of that VM means the agent's escape path goes through the container runtime and then through the hypervisor, while vibe's path goes through the hypervisor alone. Whether that difference matters depends on how much you trust the container runtime, but the architectural point stands: on a Mac you are paying for a VM either way.

What containers give you that vibe does not is an ecosystem. Image registries, layered caching, Dockerfiles as a shared format, and tooling that runs the same on a laptop and in CI. vibe's answer is a list of shell scripts and a raw disk image, which is simpler to read and much harder to distribute. If you already have a container workflow that works, vibe is not obviously an upgrade; it is a different trade, buying a smaller isolation stack at the cost of portability.

The implementation size is part of the same argument. The README states the whole thing is about 2000 lines of Rust, with Objc2 interop crates and lexopt as the only Rust dependencies. That is small enough to read in an afternoon, which is a genuine property for a tool that sits between your machine and an agent holding root.

Maintenance, licensing, and what to check before you rely on it

The licence is MIT. That permits commercial use, modification, and redistribution provided the copyright notice and permission notice are included. It comes with no warranty, and nothing here is legal advice; if you redistribute a binary that bundles the Go networking helper, confirm the notice requirements for the combined artifact with someone qualified to answer.

Maintenance cost is dominated by the missing release process. There is no changelog, so upgrading means reading commits between the SHA you pinned and the one you are moving to. The README states the author is not making formal releases and recommends pinning. Practically, that means recording the output of `vibe --version` somewhere your future self will find it, because the commit SHA and build date are the only identifiers the tool exposes.

The second maintenance cost is disk. Each project directory gets its own .vibe/instance.raw, and templates accumulate in ~/.cache/vibe/. Raw disk images are not sparse in the way a container layer is, and the README gives no guidance on reclaiming space beyond deleting the instance file. On a laptop with a small SSD, a handful of projects with installed toolchains is worth watching.

The third is the first-run dependency on a network connection to download the Debian base image. There is no described offline path for initial setup, and no mirror configuration mentioned. In a restricted network, that is a blocker rather than an inconvenience.

What to verify first, concretely: run vibe once with `--no-default-mounts` and read the printed mount table, because that table is the actual specification of what the agent can see, and it is the one thing the README shows in full. Then provision a template with your own script via `vibe provision --image` and confirm the ordering and the @built-in resolution behave as documented. Then pin.

Editorial conclusion

Adopt vibe if you run coding agents with --yolo on an ARM Mac and want the agent's blast radius limited to a disk image you can delete. Do not adopt it if you need x86 guests, a formal release process, or a sandbox that hides your home directory by default, because the default mount table shares ~/.claude, ~/.codex, ~/.gemini, ~/.pi, ~/.m2 and ~/.cargo/registry into the guest read-write. Before trusting it, run `vibe --no-default-mounts` once and diff the printed mount table against what you expect, then read the commit history at the SHA you pinned.

Official sources

  1. Issues
  2. License: MIT
  3. lynaghk/vibe on GitHub
  4. README
  5. Releases
Community notes

Community notes