Shed your VM state. Emerge fresh.
A reproducible, low-click workflow for creating and managing Ubuntu 24.04 Desktop VMs optimized for AI agent work. Run agents with full autonomy, then molt back to a pristine golden image.
This toolkit provides:
- Automated OS Installation: Cloud-init/autoinstall for hands-free Ubuntu Desktop setup
- Comprehensive Bootstrap: Security-hardened development environment with agent tooling
- Snapshot Management: Pre/post agent-run snapshots for clean state management
- Idempotent Operations: Re-runnable scripts with phase markers
- Local Customization: Override defaults without modifying core scripts
- Pre-authenticated AI CLIs: Claude Code, Codex, Gemini, and GitHub CLI ready to use
If you already have a golden image with authentication baked in:
# One command to spin up an agent VM and connect
./agent.sh
# That's it! You're now in a shell with all CLIs ready:
claude "explain this codebase"
codex "fix the tests"
gh pr createOr using make:
make agent # Spin up and connect
make agent-list # List running clones
make agent-kill CLONE=moltdown-clone-xxx # Clean upNo authentication needed - everything is inherited from the golden image.
git clone https://github.com/williamzujkowski/moltdown.git
cd moltdown
./setup_cloud.shThis uses Ubuntu Cloud Images for fast VM creation (~8 minutes to desktop-ready).
./setup.shThis uses the traditional Ubuntu ISO installer (slower, ~20 minutes).
make install-deps # Install host dependencies
make setup-cloud # Create VM using cloud images (RECOMMENDED)
make setup # Create VM using ISO installer
make golden # Create golden snapshots after bootstrap
make gui # Open VM desktop with virt-viewer
make ssh # SSH into VMIf you prefer more control:
# 1. Generate cloud-init seed ISO
./generate_nocloud_iso.sh --customize
# 2. Create VM with automated installation
./virt_install_agent_vm.sh --seed-iso ./seed.iso
# 3. Wait for installation (~10-15 min), then SSH in
ssh agent@<vm-ip>
# 4. Run bootstrap inside VM
./bootstrap_agent_vm.sh
gh auth login
# 5. Create golden snapshots
sudo shutdown -h now
./snapshot_manager.sh golden ubuntu2404-agentmoltdown/
├── README.md # This file
├── CLAUDE.md # Development guidelines
├── RESOURCES.md # Memory planning for parallel agents
├── CHANGELOG.md # Release history
├── Makefile # Common operations
├── agent.sh # One-command agent VM creation
├── setup_cloud.sh # One-command setup (cloud images, RECOMMENDED)
├── setup.sh # One-command setup (ISO installer)
├── update-golden.sh # Update golden image CLIs and auth
├── sync-ai-auth.sh # Sync AI CLI auth to VMs
├── code-connect.sh # VS Code Remote SSH connection
├── generate_cloud_seed.sh # Create seed ISO for cloud images
├── generate_nocloud_iso.sh # Create seed ISO for ISO installer
├── virt_install_agent_vm.sh # Create VM with virt-install
├── run_bootstrap_on_vm.sh # Push bootstrap via SSH
├── snapshot_manager.sh # Manage VM snapshots
├── clone_manager.sh # Manage VM clones for parallel workflows
├── cloud-init/
│ ├── user-data # Cloud-init config (for cloud images)
│ └── meta-data # Cloud-init metadata
├── autoinstall/
│ ├── user-data # Autoinstall config (for ISO installer)
│ └── meta-data # Cloud-init metadata
├── guest/
│ └── bootstrap_agent_vm.sh # Run inside VM (includes health check)
├── docs/
│ └── CLOUD_IMAGES.md # Cloud image workflow docs
├── examples/
│ ├── bootstrap_local.sh # Local customization template
│ └── user-data-custom.yaml # Customized autoinstall example
└── .github/
└── workflows/
└── lint.yml # ShellCheck + yamllint CI
VMs are created with SPICE graphics for full desktop access.
# Connect with virt-viewer (minimal)
virt-viewer ubuntu2404-agent
# Or use virt-manager for full GUI management
virt-managerInstall GUI tools: sudo apt install virt-viewer or sudo apt install virt-manager
Once you have a dev-ready snapshot, use this workflow for each agent run:
# Before agent work
./snapshot_manager.sh pre-run ubuntu2404-agent
# ... do agent work ...
# Export any artifacts from VM
scp agent@<vm-ip>:~/work/artifacts/* ./local-artifacts/
# Reset to clean state
./snapshot_manager.sh post-run ubuntu2404-agentRun multiple agents simultaneously using VM clones:
# Create linked clones (instant, copy-on-write)
./clone_manager.sh create ubuntu2404-agent --linked
./clone_manager.sh create ubuntu2404-agent --linked
./clone_manager.sh create ubuntu2404-agent --linked
# Start clones
./clone_manager.sh start moltdown-clone-ubuntu2404-agent-20250201-143052
# List all clones
./clone_manager.sh list
# Connect to each clone
virt-viewer <clone-name> # GUI
ssh agent@<clone-ip> # SSH
# Cleanup when done
./clone_manager.sh cleanup ubuntu2404-agentClone types:
- Linked clone (
--linked): Instant creation, uses copy-on-write. Best for parallel work. - Full clone: Complete disk copy. Slower but fully independent.
Using Make:
make clone-linked # Create linked clone
make clone # Create full clone
make clone-list # List all clones
make clone-cleanup # Delete all clonesAfter creating a clone, sync your AI CLI credentials and git config from your host:
# Get clone IP
./clone_manager.sh start moltdown-clone-xxx
VM_IP=$(virsh domifaddr moltdown-clone-xxx | grep -oE '192\.168\.[0-9]+\.[0-9]+')
# Sync all auth (Claude, Codex, Gemini, Git, SSH keys)
./sync-ai-auth.sh $VM_IP agent
# or
make sync-auth VM_IP=$VM_IP
# SSH in - CLIs are ready
ssh agent@$VM_IP
claude --version
codex --version
gemini --version| Tool | Config Location | Contains |
|---|---|---|
| Claude Code | ~/.claude.json, ~/.claude/ |
Settings (auth via browser on first use) |
| Codex | ~/.codex/ |
OAuth tokens, config |
| Gemini | ~/.gemini/ |
OAuth tokens, settings |
| Git | ~/.gitconfig |
Identity, signing config |
| SSH | ~/.ssh/ |
Keys for git auth + commit signing |
| GPG | Exported/imported | Keys for commit signing (if used) |
Some CLIs require browser-based authentication on first use:
- Claude Code: Run
claudeand follow the browser prompt - GitHub CLI: Run
gh auth login(token is in host keyring, not synced)
These files contain authentication tokens. They are copied directly to the VM
(which is local to your machine) and are NOT committed to git. The VM disk
images stay in /var/lib/libvirt/images/ and are excluded by .gitignore.
VMs are hardened for multi-day or multi-week agent sessions:
- Swap file: 8GB for memory pressure (Claude CLI can leak to 13GB+)
- Journal limits: 100MB max, prevents disk fill
- No auto-reboot: Security updates don't restart
- Cloud-init disabled: Prevents reconfiguration
- Memory watchdog: Auto-kills runaway Claude CLI processes at 13GB threshold
- cgroups limits: Optional hard memory limits via
run-claude-limited
Monitor health inside VM:
vm-health-check # Quick status with Claude memory tracking
vm-health-check --watch # Live monitoring (30s refresh)
vm-health-check --trend # Memory trend analysis with OOM prediction
run-claude-limited # Run Claude with 12GB memory limit
run-claude-limited 8G # Run with custom limit
agent-session # Persistent tmux session with auto-reattachSee RESOURCES.md for detailed memory planning and parallel agent deployment guidance.
Transforms a fresh Ubuntu 24.04 Desktop into an agent-ready environment.
Features:
- Phased execution with idempotent markers
- Security hardening (SSH, firewall, fail2ban)
- Development tools (git, gh, Node.js, Docker, Python)
- Browser automation (Chrome, Playwright deps)
- Agent tooling (Claude CLI, workspace structure)
- VM performance optimization
- Package manifest generation
Configuration flags (edit in script):
INSTALL_NODEJS="true"
INSTALL_DOCKER="true"
INSTALL_PLAYWRIGHT_DEPS="true"
INSTALL_CLAUDE_CLI="true"
REMOVE_DESKTOP_FLUFF="true"
ENABLE_UNATTENDED_UPGRADES="true"Manage libvirt snapshots for the golden image workflow.
# List all VMs
./snapshot_manager.sh vms
# List snapshots
./snapshot_manager.sh list ubuntu2404-agent
# Create snapshot (offline recommended)
./snapshot_manager.sh create ubuntu2404-agent my-snap --offline
# Revert to snapshot
./snapshot_manager.sh revert ubuntu2404-agent dev-ready
# Pre-agent-run workflow
./snapshot_manager.sh pre-run ubuntu2404-agent
# Post-agent-run (revert to dev-ready)
./snapshot_manager.sh post-run ubuntu2404-agent
# Interactive golden image creation
./snapshot_manager.sh golden ubuntu2404-agentGenerate cloud-init seed ISO for automated installation.
# Interactive mode
./generate_nocloud_iso.sh --customize
# Command-line customization
./generate_nocloud_iso.sh \
--username myuser \
--password mysecretpass \
--hostname my-agent-vm \
--ssh-key ~/.ssh/id_ed25519.pub
# Custom output path
./generate_nocloud_iso.sh /tmp/my-seed.isoCreate VMs with virt-install and automated installation.
# Default configuration
./virt_install_agent_vm.sh --seed-iso ./seed.iso
# Full customization
./virt_install_agent_vm.sh \
--name my-agent-vm \
--vcpus 8 \
--memory 16384 \
--disk-size 100 \
--seed-iso ./seed.iso
# Dry run (show command without executing)
./virt_install_agent_vm.sh --seed-iso ./seed.iso --dry-runPush and execute bootstrap script via SSH.
# Basic usage
./run_bootstrap_on_vm.sh 192.168.122.100 username
# Copy SSH key first
./run_bootstrap_on_vm.sh 192.168.122.100 username --copy-ssh-key
# Dry run
./run_bootstrap_on_vm.sh 192.168.122.100 username --dry-runThe bootstrap script implements:
-
SSH Hardening
- Root login disabled
- Password authentication disabled
- Public key authentication only
- Limited auth attempts
-
Firewall (UFW)
- Default deny incoming
- Default allow outgoing
- SSH allowed
-
Fail2ban
- SSH brute-force protection
- 1-hour ban after 3 failures
-
Unattended Upgrades
- Automatic security patches
- No automatic reboot
Instead of modifying core scripts, use bootstrap_local.sh:
# Copy template to VM
scp examples/bootstrap_local.sh agent@vm:~/
# Edit to add your packages, dotfiles, etc.
ssh agent@vm vim ~/bootstrap_local.sh
# Run bootstrap - it will source your local config automatically
ssh agent@vm ./bootstrap_agent_vm.shExample bootstrap_local.sh:
# Additional packages
LOCAL_APT_PACKAGES=("zsh" "neovim" "tmuxinator")
LOCAL_NPM_PACKAGES=("@anthropic-ai/claude-code")
LOCAL_PIPX_PACKAGES=("pdm" "pre-commit")
phase_local_customizations() {
# Clone dotfiles
git clone https://github.com/myuser/dotfiles ~/.dotfiles
~/.dotfiles/install.sh
}Edit bootstrap_agent_vm.sh and add to the appropriate phase function. For example, to add VS Code:
phase_dev_tools() {
# ... existing code ...
# VS Code
log_info "Installing VS Code..."
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" > /etc/apt/sources.list.d/vscode.list'
sudo apt update
sudo apt install -y code
}Edit autoinstall/user-data:
identity:
hostname: your-hostname
username: your-username
password: "your-password-hash"Generate password hash:
echo 'yourpassword' | openssl passwd -6 -stdinOption 1: Edit autoinstall/user-data:
ssh:
authorized-keys:
- ssh-ed25519 AAAA... your-keyOption 2: Use generate script:
./generate_nocloud_iso.sh --ssh-key ~/.ssh/id_ed25519.pub# Check VM state
sudo virsh domstate ubuntu2404-agent
# Force off and try again
sudo virsh destroy ubuntu2404-agent
sudo virsh snapshot-revert ubuntu2404-agent dev-ready
sudo virsh start ubuntu2404-agent# Get VM IP
sudo virsh domifaddr ubuntu2404-agent
# Check if SSH is running in VM
sudo virsh console ubuntu2404-agent
# Then: systemctl status ssh
# Check firewall
sudo ufw statusThe script uses idempotent markers. Just re-run:
./bootstrap_agent_vm.shTo force re-run a phase:
rm ~/.bootstrap_markers/05-browser-automation.done
./bootstrap_agent_vm.sh- Verify seed ISO is attached:
sudo virsh dumpxml ubuntu2404-agent | grep -A5 cdrom- Check cloud-init logs in VM:
cat /var/log/cloud-init-output.logHost system:
- libvirt + QEMU/KVM
- virt-install (
virtinstpackage) - virt-viewer (for GUI access)
- genisoimage, mkisofs, or xorriso
- SSH client
Install on Ubuntu/Debian:
sudo apt install \
qemu-kvm \
libvirt-daemon-system \
libvirt-clients \
virtinst \
virt-manager \
virt-viewer \
genisoimage \
cloud-image-utils \
openssh-clientOr use: make install-deps
| Content | Location | In Git? |
|---|---|---|
| Shell scripts, docs | This repo | ✅ Yes |
| VM disk images (.qcow2) | /var/lib/libvirt/images/ |
❌ No |
| Cloud-init seed ISOs | Generated locally | ❌ No |
| Your SSH keys | Inside VM disk + ~/.ssh/ |
❌ No |
| Passwords/credentials | Inside VM disk | ❌ No |
Your VM disk images never leave your machine. The golden image containing your SSH keys, user accounts, and any work done inside VMs is stored only in libvirt's local image directory.
The .gitignore explicitly excludes:
*.qcow2- VM disk images*.img- Disk images*.iso- ISO images including cloud-init seeds
When you add your SSH public key to the golden image:
- Only your public key is added (safe to share by design)
- Your private key never leaves
~/.ssh/on your host - The public key lives inside the VM's disk image (not in git)
The cloud-init/user-data template in this repo contains example credentials. When you run generate_cloud_seed.sh, you provide your own credentials which are written to a local seed.iso that is not committed to git.
| Command | Description |
|---|---|
./agent.sh |
Create new agent VM and connect (one command!) |
./agent.sh --list |
List all agent clones |
./agent.sh --attach <name> |
Attach to existing clone |
./agent.sh --stop <name> |
Stop a clone gracefully |
./agent.sh --kill <name> |
Delete a clone completely |
./agent.sh --gui <name> |
Open GUI viewer for clone |
./agent.sh --health <name> |
Run health check on clone |
| Command | Description |
|---|---|
./update-golden.sh |
Full update (packages + CLIs + auth) |
./update-golden.sh --quick |
Quick update (CLIs only) |
./update-golden.sh --auth-only |
Re-sync auth from host |
./code-connect.sh |
Open VS Code connected to agent VM |
./sync-ai-auth.sh <ip> |
Sync auth to specific VM |
alias ma='~/git/moltdown/agent.sh' # New agent
alias mal='~/git/moltdown/agent.sh --list' # List agents
alias mak='~/git/moltdown/agent.sh --kill' # Kill agent
alias mac='~/git/moltdown/code-connect.sh' # VS Code connectMIT License - feel free to adapt for your workflows.