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.