Ephemeral VM manager for macOS.
kodemachine start myproject # Create clone, boot, SSH in
kodemachine suspend myproject # Instant pause
kodemachine start myproject # Instant resume
kodemachine delete myproject # Gone- Development environments accumulate cruft
- Docker helps but isn't always enough
- Full VMs are clean but slow to provision
Kodemachine gives you disposable Linux VMs that boot in seconds:
- Instant clones - APFS copy-on-write, zero disk overhead
- Headless - VMs run as background processes
- SSH-native -
startdrops you into a shell - Persistent storage - Optional encrypted LUKS disk
AI coding assistants (Claude Code, Aider, Copilot) are powerful but risky. They execute code, install packages, and access your filesystem. In November 2025, Anthropic documented the first large-scale AI-orchestrated cyberattack - threat actors used Claude Code to compromise thirty organizations with minimal human involvement.
The attack exploited three AI capabilities: intelligence (complex instructions), agency (autonomous operation), and tools (filesystem and network access). Attackers accomplished 80-90% of the campaign through AI automation.
Meanwhile, researchers like Dr. Anish Mohammed (talk) warn about broader risks: LLMs with access to lab equipment, sequencers, or critical infrastructure. The same autonomy that makes AI assistants useful makes them dangerous when compromised or misused.
Kodemachine provides the first layer of defense: disposable VMs where AI tools run isolated from your host. Combined with container sandboxing (see testman), you get defense in depth - AI agents can only access the project directory, not your SSH keys, credentials, or other projects.
# 1. Setup host (once per Mac)
./setup-host.rb
# 2. Create base image (every ~6 months)
./create-base.rb
# 3. Daily workflow
kodemachine start myproject┌───────────────────────────────────────────────────────────┐
│ macOS Host │
│ │
│ setup-host.rb One-time: Install UTM, dependencies │
│ │ │
│ ▼ │
│ create-base.rb Every ~6 months: Build golden image │
│ │ - Ubuntu + GUI + browsers │
│ │ - SSH key baked in │
│ ▼ │
│ kodemachine.rb Daily: Clone, start, stop, delete │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ km-myproject (APFS clone) │ │
│ │ └── Your code, containers, etc. │ │
│ └───────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
| File | Purpose | When to Run |
|---|---|---|
setup-host.rb |
Install UTM, qemu-img, create symlinks | Once per Mac |
create-base.rb |
Build golden VM image | Every ~6 months |
kodemachine.rb |
VM lifecycle (start/stop/clone) | Daily |
Run once on a new Mac:
./setup-host.rbThis will:
- Check/install Homebrew
- Install UTM (VM hypervisor)
- Install qemu-img (disk tools)
- Create config directory
- Setup
kodemachinecommand symlink
Run every ~6 months (or when you want a fresh golden image):
# Standard: provisions Ubuntu VM with your SSH key auto-detected
./create-base.rb
# With dotfiles
./create-base.rb --dotfiles [email protected]:you/dotfiles.git
# Skip GUI for headless-only use
./create-base.rb --skip-gui --skip-browsers-n, --name NAME Base image name (default: kodeimage-vYYYY.MM)
-u, --user USER SSH username (default: kodeman)
-k, --host-ssh-key PATH Host's public key (default: ~/.ssh/id_ed25519.pub)
-d, --dotfiles REPO Git repo URL for dotfiles
--ip ADDRESS Manual IP if auto-detection fails
--skip-gui Skip XFCE installation
--skip-browsers Skip Firefox/Chromium
-v, --verbose Show SSH commands
| Category | Packages |
|---|---|
| Core | qemu-guest-agent, openssh, curl, wget, git, build-essential |
| GUI | XFCE4, xfce4-goodies, xfce4-terminal |
| Browsers | Firefox, Chromium |
| Fonts | Noto, Liberation, CaskaydiaCove Nerd Font |
| Tools | htop, btop, tree, jq, xclip |
| Shell | zsh (set as default) |
start <label> Create/start VM and SSH in
start base Start base image directly (for modifications)
stop <label> Graceful shutdown
suspend <label> Pause to memory (instant resume)
delete <label> Remove VM
status System overview
status <label> VM details with live metrics
list List all VMs
attach <label> Serial console (rescue/debug)
doctor Check system health
--gui Show VM window (limit: one GUI VM)
--no-disk Skip shared disk attachment
# Daily workflow
kodemachine start work
# ... code ...
kodemachine suspend work # Instant pause
kodemachine start work # Instant resume
# Multiple projects (concurrent)
kodemachine start api
kodemachine start frontend
kodemachine list
# Modify base image
kodemachine stop work # Stop clones first
kodemachine start base
# ... install stuff ...
kodemachine stop base
# Future clones include changes
# Debug
kodemachine attach api # Serial console
kodemachine status api # Resource detailsLocation: ~/.config/kodemachine/config.json
{
"base_image": "kodeimage-v2025.01",
"ssh_user": "kodeman",
"prefix": "km-",
"headless": true,
"shared_disk": "Shared/projects-luks.qcow2"
}| Key | Description |
|---|---|
base_image |
Golden image name in UTM |
ssh_user |
SSH username |
prefix |
Clone name prefix |
headless |
Hide VM window |
shared_disk |
Shared disk path (relative to UTM docs) |
Encrypted disk that persists across ephemeral VMs.
See LUKS_DRIVE_SETUP.md for full setup.
Quick version:
# Inside VM
sudo cryptsetup luksFormat /dev/vdb
sudo cryptsetup luksOpen /dev/vdb projects
sudo mkfs.ext4 /dev/mapper/projects
sudo mkdir -p /mnt/projects
sudo mount /dev/mapper/projects /mnt/projectsAny dotfiles repository works with kodemachine:
./create-base.rb --dotfiles [email protected]:you/dotfiles.gitThe repo is cloned to ~/dotfiles. If a bootstrap.sh script exists, it runs automatically. Otherwise, the repo is simply cloned for manual setup.
Example: jikkujose/dotfiles - cross-platform config for zsh, fish, neovim, and tmux.
Add to ~/.bashrc or ~/.zshrc:
# Zsh only:
autoload -Uz bashcompinit && bashcompinit
_kodemachine() {
local cur=${COMP_WORDS[COMP_CWORD]}
local cmd=${COMP_WORDS[1]}
if [[ $COMP_CWORD -eq 1 ]]; then
COMPREPLY=($(compgen -W \
"start resume stop suspend delete status list attach doctor" \
-- "$cur"))
elif [[ "$cmd" =~ ^(start|stop|suspend|delete|status|attach)$ ]]; then
local labels=$(utmctl list 2>/dev/null \
| grep 'km-' | awk '{print $3}' | sed 's/^km-//')
COMPREPLY=($(compgen -W "base $labels" -- "$cur"))
fi
}
complete -F _kodemachine kodemachineSave to ~/.config/fish/completions/kodemachine.fish:
complete -c kodemachine -f
complete -c kodemachine -n "__fish_use_subcommand" \
-a "start resume stop suspend delete status list attach doctor"
complete -c kodemachine -n "__fish_seen_subcommand_from start stop suspend delete status attach" \
-a "base (utmctl list 2>/dev/null | grep 'km-' | awk '{print \$3}' | sed 's/^km-//')"SSH fails after start
kodemachine attach <label>
# Check: systemctl status qemu-guest-agentIP not detected
- Use
--ipflag with create-base.rb - Check:
utmctl ip-address <vm-name>
"Device busy" errors
- Force quit UTM, retry
OSStatus -1712 / -10004
- Apple Events timeout during I/O
- Usually transient, script retries
- No gem dependencies: All scripts use Ruby standard library only (
json,fileutils,open3,optparse). Works with macOS system Ruby. - No Brewfile: Dependencies (UTM, qemu) installed imperatively by setup-host.rb.
- Stateless scripts: No daemon, no database. Config is a single JSON file.
- testman - Container sandboxing layer
- Blog: Disposable Dev Environments - Architecture overview
MIT