diff --git a/lib/images/windows/templates/generalize.nix b/lib/images/windows/templates/generalize.nix
index b38811b..d5eafae 100644
--- a/lib/images/windows/templates/generalize.nix
+++ b/lib/images/windows/templates/generalize.nix
@@ -42,6 +42,16 @@ in
# delayOobeRun = true: sysprep only, OOBE + activation on real hardware
# delayOobeRun = false: sysprep + OOBE + activation in build VM
delayOobeRun ? false,
+ # Skip sysprep's SID reset (use /oobe without /generalize), so every rebuild
+ # of the layers above the cached base install carries the same machine SID --
+ # and therefore the same account SID. A profile kept on a persistent disk then
+ # matches on file ACLs, its NTUSER.DAT hive, and ProfileList across rebuilds,
+ # with no ownership fixups. As a side effect MountedDevices survives too, so
+ # the data disk keeps its drive letter without a boot-time reassign.
+ #
+ # Correct only for an image that is always this one machine; a fleet that
+ # deploys the same image to many hosts wants the default generalization.
+ keepMachineSid ? false,
}: let
# Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry
hexToRgbStr = hex: let
@@ -229,6 +239,17 @@ in
${profilesDirectory}
'';
+ # ProfilesDirectory as an offline .reg merge, for the keepMachineSid path
+ # where the specialize pass (and its FolderLocations) does not run. virt-win-reg
+ # applies this before the Audit Mode boot, so it is in place when OOBE creates
+ # the account. Backslashes are doubled for .reg syntax.
+ profileListRegistry = lib.optionalString (keepMachineSid && profilesDirectory != null) ''
+ Windows Registry Editor Version 5.00
+
+ [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList]
+ "ProfilesDirectory"="${builtins.replaceStrings [''\''] [''\\''] profilesDirectory}"
+ '';
+
dataDiskXml = lib.optionalString (dataDisk != null) ''