Skip to content

Commit b5faceb

Browse files
authored
Merge pull request #281 from Codeuctivity/blog-ubuntu-vm-virtualbox-claude-code
Add blog post: Ubuntu VM in VirtualBox as a home for Claude Code
2 parents 4fed31c + 1a50280 commit b5faceb

2 files changed

Lines changed: 296 additions & 0 deletions

File tree

161 KB
Loading
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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+
![Claude Code session of the VM, opened in a browser](image/claude-code-session-in-browser.png)
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

Comments
 (0)