Zero-config CLI for running Home Assistant OS on Apple Silicon using Apple's native Virtualization framework. One command from download to boot.
- No setup required — downloads and prepares HA OS automatically on first run.
No Shortcuts, AppleScript, or manual launchd plists. Works with
brew services. - Starts at login — designed for headless operation as a launchd service. Fire-and-forget: your smart home boots with your Mac.
- Persistent — all HA OS data (configs, add-ons, history) lives on a raw disk image. NVRAM and MAC address survive reboots.
- USB accessories — attach coordinators and other USB devices via the menu bar item. Hot-plug, no restart needed.
- SSH key import — optional virtual CONFIG disk for root SSH on port 22222.
- Graceful shutdown — Supervisor API → SSH → force-stop fallback on SIGTERM.
- Prometheus metrics — built-in HTTP endpoint for monitoring VM state and USB accessories. Feed into Grafana dashboards or alerting rules.
Requires macOS 27 (Golden Gate) or later with Apple Silicon.
https://github.com/user-attachments/assets/aed92929-dfea-4e6f-b62c-4fed7100d34e
Quick Start
Homebrew (recommended)
brew install ingmarstein/havm/havm
havm run
Or run as a background service:
brew services start havm
Build from source
# Build and sign (no paid developer account needed)
./scripts/build.sh release
# Run — downloads HA OS automatically on first run
.build/release/havm run
On first run, havm automatically:
- Downloads the latest stable HA OS release (
haos_generic-aarch64-*.img.xz) - Decompresses the disk image
- Copies the disk image to persistent storage and resizes it
- Boots the VM
Subsequent runs skip straight to boot.
Commands
| Command | Description |
|---|---|
havm run |
Start the VM — auto-downloads HA OS on first run |
havm import-utm |
Import a Home Assistant VM from UTM |
havm cleanup |
Clear cached HA OS downloads from ~/Library/Caches/havm/ |
havm version |
Print version and system info |
Run havm --help or havm <command> --help for flags and options.
Importing from UTM
If you have an existing Home Assistant VM in UTM, you can import it into havm in one command:
havm import-utm ~/Library/Containers/com.utmapp.UTM/Data/Documents/Home\ Assistant.utm
The import copies (not moves) the VM data from the UTM bundle into havm's data directory and generates a config file with matching settings. Your UTM bundle is left intact.
What gets imported:
| UTM data | havm destination |
|---|---|
| HA OS disk image (largest writable drive) | ~/Library/Application Support/havm/vm/haos.img |
EFI variable store (efi_vars.fd) |
NVRAM |
| Machine identifier | MachineIdentifier (stable MAC address) |
| MAC address | MACAddress |
| CPU, memory, network settings | ~/.config/havm/config.yml |
What's NOT imported:
- Auxiliary disks — UTM VMs may have additional data drives. havm imports only the largest writable disk. Auxiliary disks are reported as warnings so you can copy them manually if needed.
- SSH keys — UTM doesn't use havm's CONFIG disk mechanism. If you had SSH
keys configured in UTM, add them to
ssh.authorized_keysin havm's config. - UTM-specific settings — display, audio, clipboard, Rosetta, and other UTM-exclusive features are ignored.
Sparse file handling — HA OS disk images are APFS sparse files (e.g., 21 GB
physical for a 34 GB logical disk). The import uses clonefile(2) for an
instant copy-on-write clone on the same volume, preserving sparseness without
blowing up disk usage. If the source is on a different volume, it falls back
to a sparse-aware copy that skips zero-filled blocks.
If havm data already exists, the command refuses to overwrite it. Use --force
(-f) to overwrite:
havm import-utm ~/path/to/Home\ Assistant.utm --force
After import, run havm run as usual.
Configuration
All fields optional — havm run works with zero config.
Place overrides in ~/.config/havm/config.yml:
vm:
cpu_count: 4 # default: 4 — takes effect on next boot
memory_size: "4 GiB" # default: 4 GiB — takes effect on next boot
disk_size: "32 GiB" # default: 32 GiB — can be increased (not shrunk)
network:
type: nat # nat (default) or bridge
interface: "en0" # override auto-detected bridge interface
mac: "02:00:00:00:00:01" # fixed MAC address (optional, random by default)
hostname: "homeassistant.local" # mDNS hostname or static IP
haos:
release_channel: "pre-release" # stable (default) or pre-release
ssh:
authorized_keys: "~/.ssh/id_ed25519.pub" # imported into HA OS for port 22222
usb:
enabled: true # default: true — enable USB accessory passthrough
ha:
url: "https://homeassistant.local:443" # default: http://<discovered-ip>:8123
api_token: "eyJ..." # HA long-lived access token for REST API calls
logging:
format: text # text (default) or json (NDJSON, one object per line)
level: debug # debug, info (default), warning, error
metrics:
enabled: true # default: false
type: prometheus # prometheus (default) — extensibility point for OTLP
prometheus:
port: 9210 # default: 9210
host: "127.0.0.1" # default: "127.0.0.1" — set to "0.0.0.0" for LAN access
shutdown:
timeout_seconds: 30 # max wait for guest to halt (default: 30)
Data Layout
~/Library/Caches/havm/
haos_generic-aarch64-<version>.img.xz # Cached download (can be deleted)
haos_generic-aarch64-<version>.img # Decompressed cache
~/Library/Application Support/havm/
vm/haos.img # Persistent disk image (raw, VirtIO)
vm/NVRAM # EFI variable store (boot state)
vm/MachineIdentifier # Stable machine ID (consistent MAC)
vm/config.img # SSH key import disk (if configured)
vm/havm.pid # Process PID (while running)
VM Hardware
| Component | Choice | Reason |
|---|---|---|
| Boot | UEFI (VZEFIBootLoader) |
Boots directly from the GPT disk image |
| CPU | 4 cores (configurable) | Sufficient for HA OS + add-ons |
| Memory | 4 GiB (configurable) | Balloon lets macOS reclaim idle guest memory under pressure |
| Entropy | VirtIO entropy device | Random numbers for guest kernel crypto and ASLR |
| Disk | 32 GiB raw image, VirtIO block | APFS sparse on disk (only ~6 GiB used after first boot) |
| Network | NAT with stable MAC | Works without extra entitlements |
| CONFIG disk | USB mass storage (XHCI) | HA OS imports SSH keys from USB, not VirtIO |
| NVRAM | Persisted EFI variable store | GRUB boot state survives reboots |
| Platform | VZGenericPlatformConfiguration |
Stable machine ID → consistent MAC |
USB Accessories
To attach a USB accessory while the VM is running, use the menu bar item
that appears when havm run starts. Select a device to attach it — it
will be re-attached automatically shortly after boot on
the next run. No restart needed.
Logging
havm logs to stdout by default. The format and level are configurable both
in the config file and via CLI flags:
havm run -v # debug level (shorthand for --log-level debug)
havm run -j # NDJSON output (shorthand for --log-format json)
havm run --log-format json --log-level debug
For launchd/Homebrew services, JSON logging to a file is recommended:
logging:
format: json
level: info
To log to a file, direct stdout via launchd's StandardOutPath.
Metrics
havm can expose Prometheus metrics on an HTTP endpoint for monitoring
with Prometheus or any compatible scraper. Enable it in the config:
metrics:
enabled: true
With metrics enabled, havm serves GET /metrics on 127.0.0.1:9210 by default.
A GET /health endpoint is also available for simple liveness checks.
Available metrics:
| Metric | Type | Labels | Description |
|---|---|---|---|
havm_vm_state |
gauge | state |
VM state (running, stopped, paused, starting, …) |
havm_usb_accessories |
gauge | — | Number of connected USB accessories |
Prometheus also adds its synthetic up metric — 1 when the scrape succeeds,
0 when havm is unreachable.
Prometheus scrape config:
scrape_configs:
- job_name: 'havm'
static_configs:
- targets: ['localhost:9210']
The server binds to 127.0.0.1 by default. To allow LAN access (e.g., a
dedicated Prometheus host), set the host to 0.0.0.0:
metrics:
enabled: true
prometheus:
host: "0.0.0.0"
Any host/port configuration works out of the box.
SSH Access
Add your public key to the config and HA OS will import it on boot, enabling root SSH access on port 22222:
ssh:
authorized_keys: "~/.ssh/id_ed25519.pub"
havm creates a small MBR + FAT16 disk image with volume label CONFIG and an
authorized_keys file. HA OS auto-imports it on boot and starts dropbear on
port 22222. Without this file, HA OS disables the debug SSH server.
For the regular SSH add-on (Terminal & SSH or Advanced SSH & Web Terminal), install the add-on via the HA web UI — it listens on port 22.
Graceful Shutdown
On SIGTERM or Ctrl+C, havm tries these shutdown methods in order, falling
through to the next if one fails:
- HA REST API —
POST http://<ip>:8123/api/services/hassio/host_shutdown(requires a long-lived access token inha.api_token) - Debug SSH (port 22222) —
ssh root@<ip> -p 22222 shutdown -h now(requiresssh.authorized_keysfor CONFIG disk import) - SSH add-on (port 22) —
ssh root@<ip> -p 22 ha host shutdown(requires the SSH add-on installed in HA) - Force-stop — if all above fail, the VM is stopped immediately
The shutdown timeout and API token are configurable:
ha:
api_token: "eyJ..." # HA long-lived access token
url: "https://homeassistant.local:443" # default: http://<ip>:8123
shutdown:
timeout_seconds: 30 # max wait for guest to halt (default: 30)
Press Ctrl+C twice to skip the graceful shutdown and stop the VM immediately.
Homebrew Service
brew services start havm
havm run runs in the foreground (ideal for launchd / brew services). The
formula configures keep_alive true so the VM restarts automatically if it exits.
Building from Source
Ad-hoc signing works for basic VM functionality. USB accessories require a paid Apple Developer account (the entitlement is gated by Apple).
git clone https://github.com/username/havm.git
cd havm
cp resources/build.xcconfig.example resources/build.xcconfig
# Set your team ID in build.xcconfig
./scripts/build.sh release
.build/release/havm version
Entitlement tiers:
| Tier | Account | USB | Bridge | File |
|---|---|---|---|---|
| 1 | Free | No | No | entitlements-tier1.plist |
| 2 | Paid | Yes | No | entitlements-tier2.plist |
| 3 | Paid + Apple approval | Yes | Yes | entitlements.plist |
Set ENTITLEMENTS_TIER and DEVELOPMENT_TEAM in resources/build.xcconfig.
Build havm.xcodeproj once in Xcode to generate the provisioning profile
for ch.ingmar.havm.
Comments