|
| 1 | +--- |
| 2 | +slug: ubuntu-vm-in-virtualbox-for-claude-code |
| 3 | +title: "Ubuntu VM in VirtualBox as a home for Claude Code" |
| 4 | +authors: [stesee] |
| 5 | +tags: [virtualbox, ubuntu, claude code, hyper-v, windows] |
| 6 | +--- |
| 7 | + |
| 8 | +I wanted a disposable Linux machine on my Windows 11 box where Claude Code can run on its own and that I can reach from my phone. This is the complete walkthrough: creating the VM from the command line, an unattended Ubuntu install, the Hyper-V trap that froze the installer, and the wiring that starts Claude Code Remote Control on every boot. |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | +<!-- truncate --> |
| 13 | + |
| 14 | +## What I used |
| 15 | + |
| 16 | +- Windows 11 Pro host, 6 cores, 16 GB RAM |
| 17 | +- VirtualBox 7.2.20 |
| 18 | +- `ubuntu-26.04.1-desktop-amd64.iso` |
| 19 | +- PowerShell 7 on the host |
| 20 | + |
| 21 | +Everything below is done with `VBoxManage`, so it can be repeated without clicking through dialogs. |
| 22 | + |
| 23 | +```powershell |
| 24 | +$m = 'C:\Program Files\Oracle\VirtualBox\VBoxManage.exe' |
| 25 | +$n = 'ClaudeAgent' |
| 26 | +$iso = "$env:USERPROFILE\Downloads\ubuntu-26.04.1-desktop-amd64.iso" |
| 27 | +``` |
| 28 | + |
| 29 | +## Create the VM |
| 30 | + |
| 31 | +4 GB RAM, 4 cores, EFI firmware, NAT networking and a 60 GB dynamically allocated disk. |
| 32 | + |
| 33 | +```powershell |
| 34 | +$disk = "$env:USERPROFILE\VirtualBox VMs\$n\$n.vdi" |
| 35 | +
|
| 36 | +& $m createvm --name $n --ostype Ubuntu_64 --register |
| 37 | +& $m modifyvm $n --memory 4096 --cpus 4 --vram 128 --graphicscontroller vmsvga ` |
| 38 | + --firmware efi --ioapic on --nic1 nat --clipboard-mode bidirectional ` |
| 39 | + --boot1 dvd --boot2 disk --boot3 none --boot4 none |
| 40 | +
|
| 41 | +& $m createmedium disk --filename $disk --size 61440 --format VDI --variant Standard |
| 42 | +& $m storagectl $n --name SATA --add sata --controller IntelAhci --portcount 2 --bootable on |
| 43 | +& $m storageattach $n --storagectl SATA --port 0 --device 0 --type hdd --medium $disk --nonrotational on |
| 44 | +& $m storageattach $n --storagectl SATA --port 1 --device 0 --type dvddrive --medium $iso |
| 45 | +``` |
| 46 | + |
| 47 | +A dynamically allocated disk only uses what the guest has written (about 9 GB after the install). The 60 GB is a limit, not a reservation - if the host has less free space than that, the host runs full before the VM does. An existing disk can be grown later with `VBoxManage modifymedium disk <file> --resize <MB>`. |
| 48 | + |
| 49 | +## Unattended install |
| 50 | + |
| 51 | +VirtualBox can install Ubuntu without a single click. First check that it recognizes the ISO: |
| 52 | + |
| 53 | +```powershell |
| 54 | +& $m unattended detect --iso $iso |
| 55 | +``` |
| 56 | + |
| 57 | +It reported `Unattended installation supported = yes` and the OS type `Ubuntu25_64` (7.2.20 has no profile for 26.04 yet). Then start the install. The password comes from a file, so it does not end up in the shell history: |
| 58 | + |
| 59 | +```powershell |
| 60 | +$pwFile = "$env:USERPROFILE\VirtualBox VMs\$n\initial-password.txt" |
| 61 | +Set-Content -Path $pwFile -Value 'choose-a-password' -NoNewline -Encoding ascii |
| 62 | +
|
| 63 | +& $m modifyvm $n --ostype Ubuntu25_64 |
| 64 | +& $m unattended install $n --iso $iso ` |
| 65 | + --user stefan --full-user-name 'Stefan' --password-file $pwFile ` |
| 66 | + --hostname claudeagent.local --locale en_US --country AT --time-zone Europe/Vienna ` |
| 67 | + --install-additions --start-vm=gui |
| 68 | +``` |
| 69 | + |
| 70 | +`--install-additions` also installs the VirtualBox Guest Additions, which are needed for clipboard sharing, screen resizing and for running commands in the guest from the host (see below). |
| 71 | + |
| 72 | +Delete the password file once the install is done and the password is changed. |
| 73 | + |
| 74 | +## The Hyper-V trap: installer hangs at `raid6: avx2x4 gen()` |
| 75 | + |
| 76 | +The VM booted the installer and then sat there. The last line on screen stayed `raid6: avx2x4 gen()` for minutes, with one CPU core at 100 %. |
| 77 | + |
| 78 | +The reason is in `VBox.log`: |
| 79 | + |
| 80 | +```text |
| 81 | +HM: HMR3Init: Attempting fall back to NEM: VT-x is not available |
| 82 | +``` |
| 83 | + |
| 84 | +Hyper-V was active on the host (Hyper-V services, WSL 2 and virtualization-based security all need it). VirtualBox then cannot use VT-x directly and runs the VM through the Windows Hypervisor Platform instead. With the default paravirtualization interface, the Ubuntu kernel never gets out of an early timing loop. |
| 85 | + |
| 86 | +The fix is one setting on the VM - the host stays untouched: |
| 87 | + |
| 88 | +```powershell |
| 89 | +& $m controlvm $n poweroff |
| 90 | +& $m modifyvm $n --paravirt-provider hyperv |
| 91 | +& $m startvm $n --type gui |
| 92 | +``` |
| 93 | + |
| 94 | +After that the installer ran through, rebooted into the installed system and ended at the login screen. |
| 95 | + |
| 96 | +Two more things I learned on the way: |
| 97 | + |
| 98 | +- Disabling x2APIC (`--x2apic off`) does not help. The VM refuses to start with `Cannot disable the APIC for a 64-bit guest`. |
| 99 | +- VMs in this mode are noticeably slower than with native VT-x. Full speed needs Hyper-V turned off on the host, which also takes WSL 2 away. |
| 100 | + |
| 101 | +## Running commands in the guest from the host |
| 102 | + |
| 103 | +With Guest Additions running, `VBoxManage guestcontrol` executes commands inside the VM. That is enough to do the whole post-install setup without touching the VM window. |
| 104 | + |
| 105 | +```powershell |
| 106 | +& $m guestcontrol $n --username stefan --password 'your-password' ` |
| 107 | + run --exe /bin/bash -- -c "id; uname -r" |
| 108 | +``` |
| 109 | + |
| 110 | +In VirtualBox 7.2 the arguments after `--` are the real arguments, without a leading program name. Writing `-- bash -c "..."` ends in `cannot execute binary file`. |
| 111 | + |
| 112 | +For anything longer than one line, copy a script into the guest and run it: |
| 113 | + |
| 114 | +```powershell |
| 115 | +& $m guestcontrol $n --username stefan --password 'your-password' ` |
| 116 | + copyto .\setup.sh --target-directory /tmp/ |
| 117 | +& $m guestcontrol $n --username stefan --password 'your-password' ` |
| 118 | + run --exe /bin/bash -- /tmp/setup.sh |
| 119 | +``` |
| 120 | + |
| 121 | +Save such scripts with LF line endings, otherwise bash trips over the `\r`. |
| 122 | + |
| 123 | +## Post-install setup |
| 124 | + |
| 125 | +Run as root inside the VM (`sudo bash root.sh`): |
| 126 | + |
| 127 | +```bash |
| 128 | +export DEBIAN_FRONTEND=noninteractive |
| 129 | + |
| 130 | +# German keyboard, system-wide and on the login screen |
| 131 | +localectl set-x11-keymap de pc105 |
| 132 | + |
| 133 | +# The unattended install did not apply the time zone |
| 134 | +timedatectl set-timezone Europe/Vienna |
| 135 | + |
| 136 | +# Third-party codecs and fonts, with the Microsoft font licence pre-accepted |
| 137 | +apt-get -y update |
| 138 | +echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" | debconf-set-selections |
| 139 | +apt-get -y install curl ubuntu-restricted-extras |
| 140 | + |
| 141 | +# Additional drivers |
| 142 | +ubuntu-drivers install |
| 143 | + |
| 144 | +# Access to VirtualBox shared folders |
| 145 | +usermod -aG vboxsf stefan |
| 146 | + |
| 147 | +# New password |
| 148 | +echo 'stefan:your-new-password' | chpasswd |
| 149 | +``` |
| 150 | + |
| 151 | +The GNOME desktop keeps its own keyboard layout per user, and the installer leaves it at `us`. Setting it before the first login does not stick - the first login overwrites it. So log in once, then run this in a terminal as the normal user: |
| 152 | + |
| 153 | +```bash |
| 154 | +gsettings set org.gnome.desktop.input-sources sources "[('xkb', 'de')]" |
| 155 | +sudo sed -i 's/^xkb=us$/xkb=de/' /var/lib/AccountsService/users/stefan |
| 156 | +sudo systemctl restart accounts-daemon |
| 157 | +``` |
| 158 | + |
| 159 | +The first line switches the running desktop immediately, the other two change the layout stored for the account. |
| 160 | + |
| 161 | +Notes: |
| 162 | + |
| 163 | +- `ubuntu-drivers install` found nothing but `open-vm-tools-desktop` for this virtual hardware. Those are the VMware guest tools - harmless, but of no use under VirtualBox. |
| 164 | +- To check the Guest Additions: `systemctl is-active vboxadd vboxadd-service` and `lsmod | grep vbox` should show `vboxguest`, `vboxsf` and `vboxvideo`. |
| 165 | +- The keyboard layout on the login screen changes after the next restart. |
| 166 | + |
| 167 | +## Install Claude Code |
| 168 | + |
| 169 | +As the normal user: |
| 170 | + |
| 171 | +```bash |
| 172 | +curl -fsSL https://claude.ai/install.sh | bash |
| 173 | +~/.local/bin/claude --version |
| 174 | +``` |
| 175 | + |
| 176 | +The installer puts `claude` into `~/.local/bin`. Ubuntu only adds that folder to the `PATH` at login, so `claude` is not found until you log out and in again (or reboot). |
| 177 | + |
| 178 | +## Use it from the phone or a browser: Remote Control |
| 179 | + |
| 180 | +Remote Control links a Claude Code session running in the VM to the Claude mobile app. The session, its files and its tools stay in the VM; the phone is a window onto it. |
| 181 | + |
| 182 | +```bash |
| 183 | +claude auth login # once, with a claude.ai subscription account |
| 184 | +claude remote-control # shows a link and a QR code |
| 185 | +``` |
| 186 | + |
| 187 | +It only makes outbound HTTPS connections. The VM's NAT networking is enough - no port forwarding, no bridged adapter. |
| 188 | + |
| 189 | +### Open the session on Android |
| 190 | + |
| 191 | +1. Install the Claude app from the Play Store and sign in with the same claude.ai account that is signed in inside the VM. |
| 192 | +2. Open the Code section of the app. The session running in the VM is listed there - tap it. |
| 193 | +3. Alternatively scan the QR code: in the terminal where `claude remote-control` runs, press the space bar to show it. With the autostart setup below, `tmux attach -t claude` in the VM gets you to that terminal. |
| 194 | + |
| 195 | +The same works with the Claude app on iOS. |
| 196 | + |
| 197 | +### Open the session in any browser |
| 198 | + |
| 199 | +1. Go to [claude.ai/code](https://claude.ai/code) and sign in with the same account. |
| 200 | +2. Pick the session from the list. It is named after the VM, for example `claudeagent-optimized-bird`. |
| 201 | +3. Or open the link directly. `claude remote-control` prints it in its terminal: |
| 202 | + |
| 203 | +```text |
| 204 | +·✔︎· Connected · workspace · HEAD |
| 205 | + claudeagent-optimized-bird |
| 206 | +Continue coding in the Claude mobile app or https://claude.ai/code?environment=env_... |
| 207 | +space to show QR code |
| 208 | +``` |
| 209 | + |
| 210 | +With the autostart setup below, that terminal is the tmux session: run `tmux attach -t claude` in the VM to see the link, and leave again with `Ctrl+B`, then `D`. |
| 211 | + |
| 212 | +No app, extension or VPN is needed - any current browser on any device works, including the phone's browser. |
| 213 | + |
| 214 | +### Good to know |
| 215 | + |
| 216 | +- **Same account everywhere:** the session only shows up for the account that is signed in inside the VM. |
| 217 | +- **One session, many windows:** phone, browser and the terminal in the VM show the same conversation. What you type in one appears in the others. |
| 218 | +- **Everything runs in the VM:** commands, file edits and permission prompts happen there. The phone only sends your messages and shows the output. |
| 219 | +- **The VM has to be up:** if the VM or the Windows host is off or asleep, the session is not listed or shows as disconnected. It comes back when the VM is running again. |
| 220 | +- **Not listed?** Check in the VM with `tmux attach -t claude` whether Remote Control is running or waiting at a prompt, and run `claude auth login` if it reports that it is not signed in. |
| 221 | + |
| 222 | +## Start Remote Control at boot |
| 223 | + |
| 224 | +The goal: the VM boots, nobody logs in, and the session is reachable anyway. |
| 225 | + |
| 226 | +Install `tmux` and allow services of the user to run without a login: |
| 227 | + |
| 228 | +```bash |
| 229 | +sudo apt-get -y install tmux |
| 230 | +sudo loginctl enable-linger stefan |
| 231 | +mkdir -p ~/workspace ~/.local/bin ~/.config/systemd/user |
| 232 | +``` |
| 233 | + |
| 234 | +A small loop that restarts Remote Control if it exits, saved as `~/.local/bin/claude-remote-loop` and made executable with `chmod +x`: |
| 235 | + |
| 236 | +```bash |
| 237 | +#!/bin/bash |
| 238 | +# Keeps Claude Code Remote Control running; restarts it if it exits (e.g. not signed in yet). |
| 239 | +# Ctrl+C stops claude but not this loop. |
| 240 | +trap : INT |
| 241 | +cd "$HOME/workspace" || exit 1 |
| 242 | +while true; do |
| 243 | + "$HOME/.local/bin/claude" remote-control |
| 244 | + echo "claude remote-control exited ($?), retrying in 30s - run 'claude auth login' if not signed in" |
| 245 | + sleep 30 |
| 246 | +done |
| 247 | +``` |
| 248 | + |
| 249 | +The user service, saved as `~/.config/systemd/user/claude-remote.service`: |
| 250 | + |
| 251 | +```ini |
| 252 | +[Unit] |
| 253 | +Description=Claude Code Remote Control (in tmux session "claude") |
| 254 | +After=network-online.target |
| 255 | + |
| 256 | +[Service] |
| 257 | +Type=forking |
| 258 | +ExecStart=/usr/bin/tmux new-session -d -s claude %h/.local/bin/claude-remote-loop |
| 259 | +ExecStop=/usr/bin/tmux kill-session -t claude |
| 260 | +Restart=always |
| 261 | +RestartSec=10 |
| 262 | + |
| 263 | +[Install] |
| 264 | +WantedBy=default.target |
| 265 | +``` |
| 266 | + |
| 267 | +Enable it and stop the VM from going to sleep: |
| 268 | + |
| 269 | +```bash |
| 270 | +systemctl --user daemon-reload |
| 271 | +systemctl --user enable --now claude-remote.service |
| 272 | +gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing' |
| 273 | +``` |
| 274 | + |
| 275 | +Running it inside `tmux` has a second benefit: `tmux attach -t claude` shows the live session in a terminal, for example to confirm the first-run prompts. Detach again with `Ctrl+B`, then `D`. |
| 276 | + |
| 277 | +## Start the VM with Windows |
| 278 | + |
| 279 | +To start the VM in the background at every Windows logon: |
| 280 | + |
| 281 | +```powershell |
| 282 | +schtasks /Create /TN "ClaudeAgent VM" /SC ONLOGON /TR "'C:\Program Files\Oracle\VirtualBox\VBoxManage.exe' startvm ClaudeAgent --type headless" |
| 283 | +``` |
| 284 | + |
| 285 | +A headless VM has no window, but one can be attached at any time: select the VM in the VirtualBox Manager and click **Show**, or run `VBoxManage startvm ClaudeAgent --type separate`. When closing that window, pick **Continue running in the background** - the other options stop the VM and with it the Remote Control session. |
| 286 | + |
| 287 | +Remove the task again with `schtasks /Delete /TN "ClaudeAgent VM"`. |
| 288 | + |
| 289 | +## What to keep in mind |
| 290 | + |
| 291 | +- **Memory:** the VM reserves its 4 GB while it runs. On a 16 GB host that is noticeable; during the install my host ran critically low on free memory. |
| 292 | +- **Disk:** the dynamic disk grows up to 60 GB. Keep an eye on free space on the host. |
| 293 | +- **Speed:** with Hyper-V active on the host, the VM runs in the slower fallback mode. It works, but it is not fast. |
| 294 | +- **Host must stay awake:** if Windows sleeps, the VM and the session are gone until it wakes up. |
| 295 | + |
| 296 | +For questions or feedback, contact us at [Codeuctivity@gmail.com](mailto:Codeuctivity@gmail.com). |
0 commit comments