Boot a physical disk, a partition or a disk image as a virtual machine, or enter an installed Linux system as a chroot / container — from one command.
ved is a single, modular Bash tool that wraps QEMU/KVM, chroot and systemd-nspawn
behind a friendly, interactive menu. It auto-detects your disks, EFI loaders, GPUs,
TPM and USB devices, installs missing packages for your distribution, and asks for
the administrator password only once.
- Features
- Requirements
- Supported distributions
- Installation
- Quick start
- Backends
- Interactive flow
- USB devices (webcam, Bluetooth/AirPods, phones)
- Display and OpenGL
- TPM and Secure Boot
- Languages
- Graphical desktop sessions (chroot / nspawn)
- nspawn status bar
- Options
- Examples
- Project layout
- Development and testing
- Troubleshooting
- License
- One entry point:
./ved. Run it with no arguments for an interactive menu. - Physical disks, partitions or images: pass a whole disk (
--disk), a Linux root partition (--partition) or a disk image (--image). - Three backends: QEMU/KVM,
chroot, andsystemd-nspawn. - Automatic detection of disks, EFI system partitions, EFI loaders, guest OS, DRM render nodes, hardware TPM and USB devices.
- OpenGL acceleration through
virtio-vga-gland a selected DRM render node, with a safe fallback chain (spice-gl→spice→ …). - TPM via hardware passthrough or a software TPM (
swtpm), and optional Secure Boot OVMF firmware. - USB auto-passthrough: webcam and Bluetooth are passed automatically; Android phones are detected and offered. Includes a host-side Bluetooth reset trick so wireless earbuds (e.g. AirPods) work inside the VM.
- Storage modes: temporary (discard changes on exit) or persistent.
- Automatic dependency installation for your distribution, or
--no-install. - Internationalization with English and Spanish out of the box.
- Dry run (
--dry-run) to print the exact command without running it. - Administrator password asked once, then re-executes through
sudocleanly. - Graphical desktop sessions in chroot/nspawn: launch a full desktop session (GNOME, KDE Plasma, Xfce, …) or a single app from an installed system, with GPU acceleration, Wayland/X11, audio and input devices.
- GNOME 49+ aware: ved inspects the installed system and, when GNOME no longer supports X11, starts the shell as a nested Wayland window instead of Xephyr.
- nspawn status bar (Ctrl+A prefix): when booting a full system with
--boot, a persistent status bar shows machine name/PID and lets you Stop/Reboot/Poweroff/Quit without leaving the terminal.
- Linux with Bash 4.3+.
- KVM (
/dev/kvm) for hardware acceleration — falls back to TCG (slow) if absent. - A graphical session if you want a windowed VM (SPICE/GTK viewer) or a graphical chroot/nspawn session.
ved checks for its dependencies and can install them automatically. The logical
dependencies are:
| Purpose | Packages (logical) |
|---|---|
| Virtualization | qemu-system-x86 / qemu-full / qemu |
| UEFI firmware | OVMF (edk2-ovmf, ovmf, …) |
| FAT tooling | dosfstools (mkfs.vfat) |
| Core tooling | util-linux, coreutils, findutils |
| SPICE viewer (optional) | virt-viewer (remote-viewer, virt-viewer, spicy) |
| Software TPM (optional) | swtpm |
| Containers (nspawn backend) | systemd-container |
| Nested X11 (optional) | Xephyr (only for guests that still support X11 sessions) |
Requirements inside the installed (guest) system for a full graphical desktop:
| Guest desktop | What it needs |
|---|---|
| GNOME 49+ (Ubuntu 25.10+, Fedora 43+, …) | mutter-devkit (Ubuntu: package mutter-dev-bin) |
| GNOME ≤ 48, KDE Plasma, Xfce, MATE, Cinnamon, … | the usual session binary (gnome-session, startplasma-x11, startxfce4, …) |
| Any | dbus-run-session (package dbus-daemon / dbus-x11) |
Package names are resolved automatically for these families:
Debian/Ubuntu/Mint/Pop/Kali · Fedora/RHEL/CentOS/Nobara/Rocky/AlmaLinux · Arch/Manjaro/EndeavourOS/CachyOS · openSUSE/SLES · Alpine · Void.
If the package manager is unknown, use --no-install and provide the tools yourself.
git clone https://github.com/bitstuffing/virtual-external-devices.git
cd virtual-external-devices
chmod +x ved
./vedThere is nothing to build. You can also symlink ved into your PATH:
sudo ln -s "$PWD/ved" /usr/local/bin/vedRun it with no options — the interactive menu guides you through everything:
./vedThe tool will:
- scan and list your physical disks, and ask which one to use;
- ask what to do with it (virtual machine, chroot or container);
- ask for the administrator password (once);
- mount the system, inspect it and detect the EFI loader, guest OS, desktop, memory, CPUs and storage mode;
- show a summary of exactly what it is about to launch, and start it.
| Backend | Flag | What it does |
|---|---|---|
| QEMU/KVM | -b qemu |
Runs a physical disk, a partition or an image as a full virtual machine (UEFI + SPICE). |
| chroot | -b chroot |
Mounts an installed Linux system and enters it with chroot. Can bind the host display/audio with --graphical. |
| systemd-nspawn | -b nspawn |
Mounts an installed Linux system and runs it as a container; --boot boots it with systemd. |
Security:
chrootis not a security boundary. It runs with host privileges and exposes host kernel interfaces to the installed system. Use it only with installations you trust; use QEMU for untrusted disks or images.
Available disks:
1) /dev/nvme0n1 ...
Which disk do you want to use? [1]:
What do you want to do?
1) Start it as a virtual machine (QEMU/KVM)
2) Enter an installed Linux system (chroot)
3) Run an installed Linux system as a container (systemd-nspawn)
Option [1]:
What do you want to launch inside the system?
1) An interactive shell
2) A single graphical application
3) A full desktop session
4) Boot the whole system (init/systemd)
Option [1]:
Where should the graphical session be shown?
1) On the current desktop (host screen)
2) In a separate nested window (Xephyr)
Option [1]:
The last question is skipped automatically when the installed desktop cannot run inside Xephyr (see GNOME 49+).
By default (--usb-auto auto) ved scans the USB bus and:
- Webcams (USB video class) are passed to the VM automatically.
- Bluetooth adapters are passed with
guest-reset=true,guest-resets-all=true, and shortly after boot the host toggles the adapter'sauthorizedflag0 → 1to recover it. This is what lets wireless earbuds such as AirPods work inside the guest. - Android phones (known vendors or MTP interface) are detected and you are asked whether to pass them.
Control the behavior with:
| Option | Effect |
|---|---|
--usb-auto auto |
Pass webcam + Bluetooth, offer phones (default). |
--usb-auto all |
Also pass detected phones without asking. |
--usb-auto none / --no-usb |
Disable USB auto-detection. |
--usb VID:PID |
Pass a specific device (repeatable). |
--reset-bluetooth VID:PID |
Reset a device shortly after the VM starts (repeatable). |
The network is provided by an emulated e1000e NIC (--network user), which avoids
flaky vendor Wi-Fi/Bluetooth drivers inside the guest.
Display modes: auto, spice-gl, spice, gtk-gl, gtk, sdl, vnc, none.
In auto mode, ved prefers a GPU-accelerated SPICE/GTK window and falls back
gracefully if the viewer or render node is unavailable. Force options with
--gl / --no-gl and --render-node /dev/dri/renderDXXX. When OpenGL is active,
ved reports the render node in use.
--tpm/--no-tpm: hardware TPM passthrough when/dev/tpm0or/dev/tpmrm0exists, otherwise a software TPM viaswtpm.--secure-boot: selects the Secure Boot OVMF firmware.
The interface ships in English and Spanish. Choose explicitly with
--lang en / --lang es (-L), otherwise it follows your locale. Adding a language
is a matter of copying locale/en.sh and translating the values.
With --graphical (or --session desktop in the menu) you can launch a full
desktop environment from an installed system:
# chroot backend (lightweight, no PID 1 isolation)
./ved --backend chroot --partition /dev/nvme0n1p2 --graphical --session desktop
# nspawn backend (container; no systemd user session unless --boot is used)
./ved --backend nspawn --partition /dev/nvme0n1p2 --graphical --session desktopWhat gets bound automatically:
| Resource | chroot | nspawn |
|---|---|---|
X11 socket (/tmp/.X11-unix, read-write) |
✓ | ✓ |
| Wayland socket (exported into the private runtime dir) | ✓ | ✓ |
GPU/DRM (/dev/dri, read-write) |
✓ | ✓ |
Audio (/dev/snd, PipeWire/Pulse) |
✓ | ✓ |
Input devices (/dev/input) |
✓ | ✓ |
System D-Bus (/run/dbus) |
✓ | ✓ (without --boot) |
| Session D-Bus | ✓ (dbus-run-session) |
✓ (dbus-run-session) |
Private user runtime dir (/run/user/$UID) |
✓ | ✓ |
| Xauthority | ✓ | ✓ |
The guest gets its own private XDG_RUNTIME_DIR, owned by the graphical user.
Only the host sockets it actually needs are exported into it (the Wayland
compositor socket for the host target, plus the PulseAudio/PipeWire audio
sockets). The host's /run/user/$UID is never bind-mounted as a whole: sharing it
would make the guest's systemd, keyring, gvfs, Remmina and D-Bus daemons collide
with the host's live sockets.
The session is wrapped with dbus-run-session so GNOME/KDE can start their
required services (gvfs, keyring, etc.). systemd-run --user is used only when a
systemd user manager is actually running inside the container; a container
started without --boot has no PID 1 and therefore no user manager, so the host's
systemd socket must not be used.
The system root is mounted before the graphical questions are asked, so ved
can inspect the installed desktop and offer only the options that will work.
GUI target (--gui / --gui-target):
host(default): render on your current Wayland/X11 session. GPU acceleration is available.nested: start a nested Xephyr X server (isolated window). Xephyr has no DRI3, so the session is forced to software rendering (LIBGL_ALWAYS_SOFTWARE=1); GPU acceleration is only available with thehosttarget. Only valid for desktops that still support X11 sessions.
GNOME Shell 49 (for example Ubuntu 25.10) no longer supports X11 sessions. Running
gnome-session inside Xephyr starts the background services (Remmina, Evolution,
gvfs, …) but never starts the shell, which shows up as a permanent black screen.
Leftover *-xorg.desktop entries under /usr/share/xsessions do not change that.
When ved detects GNOME ≥ 49 in a desktop session it:
-
forces the
hosttarget (and skips the Xephyr question); -
starts the shell directly as a nested Wayland window on your desktop:
dbus-run-session -- gnome-shell --devkit --wayland --wayland-display ved-$$ -
warns if the guest has no
mutter-devkitinstead of leaving you with a black screen.
Trade-offs: this path does not go through gnome-session, so autostart
applications are not launched. That is usually desirable for a rescue/inspection
desktop. Passing your own command (--command) disables the automatic selection.
Status: the chroot backend has been tested with Ubuntu 25.10 (GNOME 49) guests. The nspawn path uses the same command and the bindings listed above; it is considered experimental for GNOME 49+.
When using --backend nspawn --boot (or selecting "Boot the whole system" in the
menu), a persistent status bar appears at the bottom of your terminal:
Machine: ved-nvme0n1p2-12345 PID: 67890 Ctrl+A then: [S]top [R]eboot [P]oweroff [Q]uit ved
- Normal typing passes through to the container (no key conflicts)
- Ctrl+A enters command mode — the bar changes to show available actions
- In command mode: S = terminate, R = reboot, P = poweroff, Q = quit ved
- Esc / Ctrl+C / Ctrl+A cancels command mode
This works like tmux's prefix key or vi's command mode — accessible, no accidental keypresses, and you never need another terminal to manage the container.
Common options:
-b, --backend NAME qemu, chroot or nspawn (default: qemu)
-L, --lang CODE Language code: en, es (default: from the environment)
-d, --disk DEVICE Physical disk, for example /dev/nvme0n1
-i, --image FILE Disk image file (QEMU backend)
-p, --partition DEV Linux root partition (chroot/nspawn backend)
--root-partition DEV
Alias for --partition
--esp DEVICE EFI system partition (auto-detected by default)
--os TYPE auto, linux or windows (default: auto)
--efi-loader PATH EFI loader relative to the ESP (no `..`), or empty for the UEFI menu
--storage MODE temp or persistent (default: ask)
--ram SIZE Positive memory size, for example 8G or 8192M
--cpus N Number of virtual CPUs
--accel MODE auto, kvm or tcg (default: auto)
--tpm Enable TPM passthrough or a software TPM
--no-tpm Disable the TPM
--secure-boot Use the Secure Boot OVMF/AAVMF firmware
--no-secure-boot Disable Secure Boot
--boot-menu Show the QEMU boot menu (default)
--no-boot-menu Hide the QEMU boot menu
--network MODE user or none (default: user)
--usb VID:PID Pass a USB device to the VM; can be repeated
--usb-auto MODE auto, all or none (default: auto)
--no-usb Do not auto-detect USB devices
--reset-bluetooth VID:PID
Reset a USB device shortly after the VM starts
--display MODE auto, spice-gl, spice, gtk-gl, gtk, sdl, vnc or none
--gl Force OpenGL acceleration
--no-gl Disable OpenGL acceleration
--render-node DEV Force a DRM render node, for example /dev/dri/renderD128
--user NAME User to use inside a chroot or nspawn container
-c, --command CMD Command to run in the chroot or nspawn backend
--graphical Bind the host display and audio into chroot/nspawn
--boot Boot the guest with systemd-nspawn instead of a command
--session MODE shell, app, desktop or system (default: ask)
--app CMD Run one graphical application (implies --session app)
--gui TARGET Host or nested Xephyr display: host or nested (default: ask)
--gui-target TARGET
Alias for --gui
--nested Shortcut for --gui nested
--host-display Shortcut for --gui host
--arch ARCH QEMU guest architecture: auto, x86_64 or aarch64
Other options:
--config FILE Load a configuration file before applying options (trusted Bash code)
--no-install Do not install missing packages
-y, --yes Answer yes to confirmation prompts
-n, --dry-run Print the command instead of running it
--keep-temp Keep temporary files after exit
-v, --verbose Show debugging output
-h, --help Show this help
-V, --version Show the version
Configuration security:
--configsources the file as Bash, including after privilege escalation. Only load configuration files you control and trust.
# Interactive menu
./ved
# Boot a physical disk as a VM with 16 GB of RAM and 10 CPUs
./ved --disk /dev/nvme0n1 --ram 16G --cpus 10
# Boot a Windows disk with TPM and Secure Boot
./ved --disk /dev/nvme0n1 --os windows --tpm --secure-boot
# Boot a disk image, force software rendering
./ved --image ~/win11.qcow2 --no-gl
# Print the QEMU command without running it
./ved --disk /dev/nvme0n1 --dry-run
# Enter an installed Linux system with chroot, sharing the host display
./ved --backend chroot --partition /dev/nvme0n1p2 --graphical
# Launch a full desktop from an installed system (chroot).
# GNOME 49+ is detected automatically and started with gnome-shell --devkit.
./ved --backend chroot --partition /dev/nvme0n1p2 --graphical --session desktop
# Same with nspawn (experimental for GNOME 49+)
./ved --backend nspawn --partition /dev/nvme0n1p2 --graphical --session desktop
# Inspect the exact command that would be used for a desktop session
./ved --backend chroot --partition /dev/nvme0n1p2 --graphical --session desktop --dry-run
# Boot a Linux install as a container with systemd (status bar with Ctrl+A)
./ved --backend nspawn --partition /dev/nvme0n1p2 --boot
# Run a single graphical app (e.g. Firefox) from an installed system
./ved --backend nspawn --partition /dev/nvme0n1p2 --graphical --session app --command firefox
# Force a specific desktop command instead of the automatic selection
./ved --backend chroot --partition /dev/nvme0n1p2 --graphical --session desktop \
--command 'dbus-run-session -- gnome-shell --devkit --wayland --wayland-display ved-0'
# Pass a specific USB device and reset the Bluetooth adapter
./ved --disk /dev/nvme0n1 --usb 413c:301a --reset-bluetooth 8087:0033ved single entry point
lib/
core.sh globals, CLI parsing, privilege handling, dispatch
i18n.sh locale loading and message helpers
platform.sh distribution detection and dependency installation
ui.sh menus, prompts, configuration summary
disks.sh disk/partition/EFI-loader/guest-OS detection
firmware.sh OVMF, TPM, audio, render nodes, USB detection
display.sh display mode selection and viewer launch
session.sh guest desktop probing and session setup
backends/
qemu.sh QEMU/KVM backend
chroot.sh chroot backend
nspawn.sh systemd-nspawn backend
locale/
en.sh English messages
es.sh Spanish messages
tests/
cli_test.sh non-privileged CLI regression tests
.github/workflows/
ci.yml GitHub Actions validation workflow
The checks do not mount devices, request sudo, start QEMU, or modify guest
systems. Run them locally from the repository root:
make syntax # Parse every Bash file
make test # Run non-privileged CLI regression tests
make lint # Run ShellCheck (requires shellcheck)
make check # Run syntax, tests and ShellCheckGitHub Actions runs make check for every push and pull request. On Fedora/Nobara,
install ShellCheck locally with sudo dnf install ShellCheck; on Debian/Ubuntu use
sudo apt install shellcheck.
- "Hardware virtualization is not available" — enable VT-x/AMD-V in your BIOS/UEFI
(
/dev/kvmmust exist). Without itveduses TCG, which is very slow. - No SPICE viewer found — install
virt-viewer(or usespicy), or use--display nonefor a headless run. - OpenGL context errors — try
--no-gl, or force a specific GPU with--render-node /dev/dri/renderD128. - Bluetooth disappears after the VM exits —
vedresets the adapter automatically; if it is still missing, re-run or pass--reset-bluetooth VID:PID. - Run
./ved --dry-run …to inspect the exact command and diagnose flags. - Black screen with a nested Xephyr window and GNOME 49+ (Ubuntu 25.10+) —
GNOME Shell 49 no longer supports X11, so
gnome-sessionin Xephyr starts services but no shell. ved detects this and usesgnome-shell --devkiton the host display. The guest needsmutter-devkit(Ubuntu:mutter-dev-bin). Check the guest's GNOME version withchroot <root> gnome-shell --version. GNOME 49+ needs mutter-devkit in the guest— install the package that provides/usr/libexec/mutter-devkitin the installed system (Ubuntu:mutter-dev-bin;dpkg -S /usr/libexec/mutter-devkittells you which one).Failed to start X Wayland: Directory "/tmp/.X11-unix" is not writable(nspawn) — the X11 socket directory must be bound read-write; the shell creates its own Xwayland socket there. ved binds it read-write.Created surfaceless renderer without GPU(nspawn) —/dev/driwas bound read-only or the container is not allowed to use the DRM devices. ved binds it read-write; if the GPU is still missing, bind the render node explicitly and allow it with--property=DeviceAllow.Couldn't connect to system bus/endSessionDialog.jsexception, thencould not connect to wayland server(nspawn) — the guest shell needs the system D-Bus (accounts service). Without--bootthere is no system bus in the container, so ved binds the host's/run/dbus.- Mixed GNOME versions in the guest (e.g.
gnome-shell 49~betawithmutter 49.0) — happens on systems installed from a beta ISO and never fully upgraded. Runapt full-upgrade(or the equivalent) inside the installed system before reporting desktop problems. - GNOME/KDE falls back to "Something has gone wrong" screen — ensure your user
exists in the guest (UID 1000), use
--gui host, or checkjournalctl --user -finside the session for gnome-shell errors. For GNOME < 49,--gui nested(Xephyr) can help to isolate Wayland issues. Failed to connect to user scope bus via local transport: No data available—systemd-run --userwas used in a container that was not booted with--boot. There is no systemd user manager inside it;vedfalls back todbus-run-session.DRI3 error/GL Helper exited with code 512in Xephyr — expected with a nested server.vedforcesLIBGL_ALWAYS_SOFTWARE=1for thenestedtarget. Use--gui hostif you need GPU acceleration.ERROR: NVIDIA driver is not loaded— the guest ships NVIDIA userspace libraries but the NVIDIA devices are not available inside the chroot/container. Harmless; the session falls back to Mesa on the other GPU.couldn't connect to control socket ... /keyring/controlorfuse: failed to access mountpoint .../gvfs— the guest was reusing the host's/run/user/$UID.vedcreates a private runtime directory for the guest instead.- nspawn status bar not appearing — requires a real TTY (
-t); it won't show in scripts or pipes. Use--dry-runto verify the command.
This project is released under the GNU General Public License v3.0 (GPLv3). See the full text at https://www.gnu.org/licenses/gpl-3.0.html.
You are free to use, study, share and improve it; derivative works must remain under the same license, keeping these community freedoms for everyone.
Made by @bitstuffing with love.