#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # defaults SSH_PORT=2222 MEMORY=8G CPUS=4 PROJECTS="" CLAUDE_DIR="" SSH_KEYS=() SEED_ISO="" IMAGE="" GUEST_ARCH="" usage() { cat < [options] Options: --arch Guest architecture (auto-detected from image name) --ssh-key SSH public key (repeatable, auto-generates seed ISO) --seed-iso Pre-built seed ISO (alternative to --ssh-key) --projects Mount host directory as ~/projects in VM --claude-dir Mount host .claude directory for auth --memory VM memory (default: 8G) --cpus VM CPUs (default: 4) --ssh-port SSH port forward (default: 2222) EOF exit 1 } [ "${1:-}" ] || usage IMAGE="$1" shift while [ $# -gt 0 ]; do case "$1" in --arch) GUEST_ARCH="$2"; shift 2 ;; --ssh-key) SSH_KEYS+=("$2"); shift 2 ;; --seed-iso) SEED_ISO="$2"; shift 2 ;; --projects) PROJECTS="$2"; shift 2 ;; --claude-dir) CLAUDE_DIR="$2"; shift 2 ;; --memory) MEMORY="$2"; shift 2 ;; --cpus) CPUS="$2"; shift 2 ;; --ssh-port) SSH_PORT="$2"; shift 2 ;; *) echo "unknown option: $1"; usage ;; esac done if [ ! -f "$IMAGE" ]; then echo "error: image not found: $IMAGE" exit 1 fi # detect guest architecture from image filename if not specified if [ -z "$GUEST_ARCH" ]; then case "$IMAGE" in *aarch64*|*arm64*) GUEST_ARCH="aarch64" ;; *x86_64*|*amd64*) GUEST_ARCH="x86_64" ;; *) # fallback to host architecture case "$(uname -m)" in x86_64|amd64) GUEST_ARCH="x86_64" ;; aarch64|arm64) GUEST_ARCH="aarch64" ;; *) echo "error: cannot detect guest arch, use --arch"; exit 1 ;; esac ;; esac fi # normalize case "$GUEST_ARCH" in x86_64|amd64) GUEST_ARCH="x86_64" ;; aarch64|arm64) GUEST_ARCH="aarch64" ;; *) echo "error: unsupported architecture: $GUEST_ARCH"; exit 1 ;; esac # platform detection HOST_ARCH=$(uname -m) OS=$(uname -s) ACCEL="tcg" case "$OS" in Linux) [ -r /dev/kvm ] && ACCEL="kvm" ;; Darwin) # hvf only works when guest matches host case "$HOST_ARCH" in aarch64|arm64) [ "$GUEST_ARCH" = "aarch64" ] && ACCEL="hvf" ;; x86_64|amd64) [ "$GUEST_ARCH" = "x86_64" ] && ACCEL="hvf" ;; esac ;; esac case "$GUEST_ARCH" in x86_64) QEMU_BIN="qemu-system-x86_64" ;; aarch64) QEMU_BIN="qemu-system-aarch64" ;; esac # auto-generate seed ISO from SSH key CLEANUP_SEED="" if [ "${#SSH_KEYS[@]}" -gt 0 ] && [ -z "$SEED_ISO" ]; then SEED_ISO="$(mktemp -d)/seed.iso" CLEANUP_SEED="$SEED_ISO" bash "$SCRIPT_DIR/make-seed.sh" "$SEED_ISO" "${SSH_KEYS[@]}" fi cleanup() { [ -n "$CLEANUP_SEED" ] && rm -rf "$(dirname "$CLEANUP_SEED")" } trap cleanup EXIT # build qemu command QEMU_ARGS=( "$QEMU_BIN" -accel "$ACCEL" -m "$MEMORY" -smp "$CPUS" -drive "file=$IMAGE,format=qcow2,snapshot=on" -nic "user,hostfwd=tcp::${SSH_PORT}-:22" -nographic ) # aarch64 guest needs machine type and uefi firmware if [ "$GUEST_ARCH" = "aarch64" ]; then if [ "$ACCEL" = "hvf" ]; then QEMU_ARGS+=(-machine virt -cpu host) else QEMU_ARGS+=(-machine virt -cpu max) fi EFI_CODE="" for p in \ /opt/homebrew/share/qemu/edk2-aarch64-code.fd \ /usr/local/share/qemu/edk2-aarch64-code.fd \ /usr/share/qemu-efi-aarch64/QEMU_EFI.fd \ /usr/share/AAVMF/AAVMF_CODE.fd; do [ -f "$p" ] && EFI_CODE="$p" && break done if [ -z "$EFI_CODE" ]; then echo "error: aarch64 EFI firmware not found" echo " macos: brew install qemu" echo " linux: apt install qemu-efi-aarch64" exit 1 fi QEMU_ARGS+=(-bios "$EFI_CODE") fi # seed ISO if [ -n "$SEED_ISO" ]; then QEMU_ARGS+=(-drive "file=$SEED_ISO,format=raw,media=cdrom,readonly=on") fi # 9p mounts — none lets the guest control ownership via dfltuid/dfltgid SECURITY_MODEL="none" if [ -n "$PROJECTS" ]; then QEMU_ARGS+=( -virtfs "local,path=$PROJECTS,mount_tag=projects,security_model=$SECURITY_MODEL,id=fs0" ) fi if [ -n "$CLAUDE_DIR" ]; then QEMU_ARGS+=( -virtfs "local,path=$CLAUDE_DIR,mount_tag=hostclaude,security_model=$SECURITY_MODEL,id=fs1" ) # also mount parent home for .claude.json QEMU_ARGS+=( -virtfs "local,path=$(dirname "$CLAUDE_DIR"),mount_tag=hosthome,security_model=$SECURITY_MODEL,id=fs2,readonly=on" ) fi echo "---" echo "Guest: $GUEST_ARCH | Accel: $ACCEL" echo "SSH: ssh -p $SSH_PORT sandbox@localhost" echo "---" exec "${QEMU_ARGS[@]}"