Series · The Quiet Machine · Part 7

Mirroring a Dev Machine Safely, on a Windows Box

A tool that installs cleanly today becomes a “why doesn’t it work when I log in as the other account” bug three weeks from now, reported by me, to me, with no memory of having caused it. That’s what happens when you mirror a dev machine into one login profile, on a box with two.

The box was already reachable the ways the earlier parts in this series set up. But being reachable isn’t the same as being a workstation, so the next job was mirroring my Mac’s stack onto it: same Node, same CLIs, same VS Code extensions, same Android SDK, minus iOS, which is Apple’s walled garden and stays on the Mac.

The installs were the easy part: winget, nvm, git clone, done the same day. The discipline underneath was the actual work, and it comes down to one rule: install once, machine-wide, so both login identities share it.

The per-user default is a trap for future-you

Windows dev tooling loves installing into a single user’s profile, which feels helpful and is exactly the landmine described above. So I went hunting for every per-user default I could find and moved it up a level, one tool at a time.

Node came in through nvm and landed in one user’s profile; I ripped it out for a machine-wide Node LTS, with a shared npm prefix both accounts point at. winget’s “portable” CLIs (ffmpeg, jq, ripgrep, uv) all shimmed themselves into a per-user directory, so as a stopgap I copied the real executables into a machine-wide tools folder on the system PATH — it works, but copies like that fall off winget’s upgrade path, so they never get updates on their own. VS Code extensions I mirrored straight off the Mac, one line each way:

# on the Mac
code --list-extensions > ext.txt
# on the box, loop and install
Get-Content ext.txt | ForEach-Object { code --install-extension $_ }

The Android SDK had survived the 27.6 GB cleanup from Part 2, just orphaned on disk. I matched it to the Mac’s platforms and build-tools with sdkmanager, accepted the licenses, and set ANDROID_HOME at the machine level.

The gotchas nobody puts in the README

A stale x86 dotnet was shadowing the real one. I’d installed .NET 10 (x64), typed dotnet --version, and gotten an older answer back: some ancient 32-bit dotnet from a past life sat earlier on the PATH and won the lookup. where.exe dotnet showed two entries; I removed the x86 one. Same PATH-hygiene lesson from Part 2, new costume.

ngrok downloaded fine, then Windows Defender ate it, flagged as a Potentially Unwanted Application. Not worth fighting: cloudflared was already installed and does the same job, so I skipped ngrok instead.

npm resolves fine in cmd but not through PowerShell’s & call operator: a quirk, not a broken install. Annoying and harmless, but exactly the kind of thing that sends you down a forty-minute hole convinced you broke Node.

The nvm uninstaller hung. While I was swapping nvm-managed Node for the machine-wide LTS, it stopped dead on an interactive “are you sure?” prompt over SSH, no TTY to answer it, and just sat there. I killed the process and purged the leftovers by hand.

The assistant’s own safety layer even false-flagged a harmless version check on a cloud CLI, and it twitched anyway.

Machine-wide by default was the rule I aimed for on every tool. VS Code’s extensions and npm’s global packages are per-user by design, so those stayed on the account I was signed into; lining up the second account was a job for later.

Why the emulator runs on the box

The Mac’s emulator felt sluggish enough on Apple Silicon that I moved it, and the heavier Android builds, onto the box entirely, and kept publishing on the Mac itself.

The box runs an x86_64 system image natively, uses WHPX for hardware acceleration, and hands GPU rendering to the RTX 3060 — the emulator’s happy path. Mac is the brain, box is the muscle, and here the muscle feels faster.

Setup was mostly uneventful: install the emulator, pull a Google-APIs x86_64 system image, create an AVD. The one real gotcha is the accelerator: Hyper-V alone isn’t enough, the emulator wants the Windows Hypervisor Platform feature specifically:

Enable-WindowsOptionalFeature -Online -FeatureName HypervisorPlatform -All
# reboot, then:
emulator -accel-check
# accel:
# 0
# WHPX is installed and usable.

That last line is the one you’re chasing — without it, accel-check won’t report WHPX as usable.

The repo I thought I’d deleted

To wire the emulator into a real workflow, I went looking for my old Android auto-setup scripts — AVD provisioning, gradle warm-up, the boilerplate you write once and reuse forever. For a moment I thought they were gone, until I remembered where they were: an old client engagement, kept in its own isolated tree, a boundary I’d set up and half-forgotten about.

They weren’t deleted, just properly isolated, and the boundary held exactly the way it was meant to: I never opened that tree, and nothing from a client project crossed into this one.

Client work, personal work, and this home lab don’t share a folder. The client’s app and its configuration stay in their isolated tree; bringing anything across is my call to make, deliberately, not a side effect of wanting an old script back.

So the emulator got built from scratch: generic and clean, nothing borrowed, nothing carried across.

Next: Claude Code lands on the box, and the memory framework gets a cleanup of its own.