@@ -4,50 +4,38 @@ function Invoke-WinUtilISOScript {
44 Applies WinUtil modifications to a mounted Windows 11 install.wim image.
55
66 . DESCRIPTION
7- Performs the following operations against an already-mounted WIM image:
8-
9- 1. Removes provisioned AppX bloatware packages via DISM.
10- 2. Removes OneDriveSetup.exe from the system image.
11- 3. Loads offline registry hives (COMPONENTS, DEFAULT, NTUSER, SOFTWARE, SYSTEM)
12- and applies the following tweaks:
13- - Bypasses hardware requirement checks (CPU, RAM, SecureBoot, Storage, TPM).
14- - Disables sponsored-app delivery and ContentDeliveryManager features.
15- - Enables local-account OOBE path (BypassNRO).
16- - Writes autounattend.xml to the Sysprep directory inside the WIM and,
17- optionally, to the ISO/USB root so Windows Setup picks it up at boot.
18- - Disables reserved storage.
19- - Disables BitLocker device encryption.
20- - Hides the Chat (Teams) taskbar icon.
21- - Disables OneDrive folder backup (KFM).
22- - Disables telemetry, advertising ID, and input personalization.
23- - Blocks post-install delivery of DevHome, Outlook, and Teams.
24- - Disables Windows Copilot.
25- - Disables Windows Update during OOBE.
26- 4. Deletes unwanted scheduled-task XML definition files (CEIP, Appraiser, etc.).
27- 5. Removes the support\ folder from the ISO contents directory (if supplied).
28-
29- Mounting and dismounting the WIM is the responsibility of the caller
30- (e.g. Invoke-WinUtilISO).
7+ Removes AppX bloatware and OneDrive, optionally injects all drivers exported from
8+ the running system into install.wim and boot.wim (controlled by the
9+ -InjectCurrentSystemDrivers switch), applies offline registry tweaks (hardware
10+ bypass, privacy, OOBE, telemetry, update suppression), deletes CEIP/WU
11+ scheduled-task definition files, and optionally writes autounattend.xml to the ISO
12+ root and removes the support\ folder from the ISO contents directory.
13+
14+ All setup scripts embedded in the autounattend.xml <Extensions><File> nodes are
15+ written directly into the WIM at their target paths under C:\Windows\Setup\Scripts\
16+ to ensure they survive Windows Setup stripping unrecognised-namespace XML elements
17+ from the Panther copy of the answer file.
18+
19+ Mounting/dismounting the WIM is the caller's responsibility (e.g. Invoke-WinUtilISO).
3120
3221 . PARAMETER ScratchDir
3322 Mandatory. Full path to the directory where the Windows image is currently mounted.
34- Example: C:\Users\USERNAME\AppData\Local\Temp\WinUtil_Win11ISO_20260222\wim_mount
3523
3624 . PARAMETER ISOContentsDir
37- Optional. Root directory of the extracted ISO contents.
38- When supplied, autounattend.xml is also written here so Windows Setup picks it
39- up automatically at boot, and the support\ folder is deleted from that location.
25+ Optional. Root directory of the extracted ISO contents. When supplied,
26+ autounattend.xml is written here and the support\ folder is removed.
4027
4128 . PARAMETER AutoUnattendXml
42- Optional. Full XML content for autounattend.xml.
43- In compiled winutil.ps1 this is the embedded $WinUtilAutounattendXml here-string;
44- in dev mode it is read from tools\autounattend.xml.
45- If empty, the OOBE bypass file is skipped and a warning is logged.
29+ Optional. Full XML content for autounattend.xml. If empty, the OOBE bypass
30+ file is skipped and a warning is logged.
31+
32+ . PARAMETER InjectCurrentSystemDrivers
33+ Optional. When $true, exports all drivers from the running system and injects
34+ them into install.wim and boot.wim index 2 (Windows Setup PE).
35+ Defaults to $false.
4636
4737 . PARAMETER Log
48- Optional ScriptBlock used for progress/status logging.
49- Receives a single [string] message argument.
50- Defaults to { param($m) Write-Output $m } when not supplied.
38+ Optional ScriptBlock for progress/status logging. Receives a single [string] argument.
5139
5240 . EXAMPLE
5341 Invoke-WinUtilISOScript -ScratchDir "C:\Temp\wim_mount"
@@ -62,24 +50,19 @@ function Invoke-WinUtilISOScript {
6250 . NOTES
6351 Author : Chris Titus @christitustech
6452 GitHub : https://github.com/ChrisTitusTech
65- Version : 26.02.22
53+ Version : 26.03.02
6654 #>
6755 param (
6856 [Parameter (Mandatory )][string ]$ScratchDir ,
69- # Root directory of the extracted ISO contents. When supplied, autounattend.xml
70- # is written here so Windows Setup picks it up automatically at boot.
7157 [string ]$ISOContentsDir = " " ,
72- # Autounattend XML content. In compiled winutil.ps1 this comes from the embedded
73- # $WinUtilAutounattendXml here-string; in dev mode it is read from tools\autounattend.xml.
7458 [string ]$AutoUnattendXml = " " ,
59+ [bool ]$InjectCurrentSystemDrivers = $false ,
7560 [scriptblock ]$Log = { param ($m ) Write-Output $m }
7661 )
7762
78- # ── Resolve admin group name (for takeown / icacls) ──────────────────────
7963 $adminSID = New-Object System.Security.Principal.SecurityIdentifier(' S-1-5-32-544' )
8064 $adminGroup = $adminSID.Translate ([System.Security.Principal.NTAccount ])
8165
82- # ── Local helpers ─────────────────────────────────────────────────────────
8366 function Set-ISOScriptReg {
8467 param ([string ]$path , [string ]$name , [string ]$type , [string ]$value )
8568 try {
@@ -100,15 +83,37 @@ function Invoke-WinUtilISOScript {
10083 }
10184 }
10285
103- # ═════════════════════════════════════════════════════════════════════════
104- # 1. Remove provisioned AppX packages
105- # ═════════════════════════════════════════════════════════════════════════
86+ function Add-DriversToImage {
87+ param ([string ]$MountPath , [string ]$DriverDir , [string ]$Label = " image" , [scriptblock ]$Logger )
88+ & dism / English " /image:$MountPath " / Add-Driver " /Driver:$DriverDir " / Recurse 2>&1 |
89+ ForEach-Object { & $Logger " dism[$Label ]: $_ " }
90+ }
91+
92+ function Invoke-BootWimInject {
93+ param ([string ]$BootWimPath , [string ]$DriverDir , [scriptblock ]$Logger )
94+ Set-ItemProperty - Path $BootWimPath - Name IsReadOnly - Value $false - ErrorAction SilentlyContinue
95+ $mountDir = Join-Path $env: TEMP " WinUtil_BootMount_$ ( Get-Random ) "
96+ New-Item - Path $mountDir - ItemType Directory - Force | Out-Null
97+ try {
98+ & $Logger " Mounting boot.wim (index 2) for driver injection..."
99+ Mount-WindowsImage - ImagePath $BootWimPath - Index 2 - Path $mountDir - ErrorAction Stop | Out-Null
100+ Add-DriversToImage - MountPath $mountDir - DriverDir $DriverDir - Label " boot" - Logger $Logger
101+ & $Logger " Saving boot.wim..."
102+ Dismount-WindowsImage - Path $mountDir - Save - ErrorAction Stop | Out-Null
103+ & $Logger " boot.wim driver injection complete."
104+ } catch {
105+ & $Logger " Warning: boot.wim driver injection failed: $_ "
106+ try { Dismount-WindowsImage - Path $mountDir - Discard - ErrorAction SilentlyContinue | Out-Null } catch {}
107+ } finally {
108+ Remove-Item - Path $mountDir - Recurse - Force - ErrorAction SilentlyContinue
109+ }
110+ }
111+
112+ # ── 1. Remove provisioned AppX packages ──────────────────────────────────
106113 & $Log " Removing provisioned AppX packages..."
107114
108115 $packages = & dism / English " /image:$ScratchDir " / Get-ProvisionedAppxPackages |
109- ForEach-Object {
110- if ($_ -match ' PackageName : (.*)' ) { $matches [1 ] }
111- }
116+ ForEach-Object { if ($_ -match ' PackageName : (.*)' ) { $matches [1 ] } }
112117
113118 $packagePrefixes = @ (
114119 ' AppUp.IntelManagementandSecurityStatus' ,
@@ -155,25 +160,46 @@ function Invoke-WinUtilISOScript {
155160 ' MicrosoftTeams'
156161 )
157162
158- $packagesToRemove = $packages | Where-Object {
159- $pkg = $_
160- $packagePrefixes | Where-Object { $pkg -like " *$_ *" }
161- }
162- foreach ($package in $packagesToRemove ) {
163- & dism / English " /image:$ScratchDir " / Remove-ProvisionedAppxPackage " /PackageName:$package "
163+ $packages | Where-Object { $pkg = $_ ; $packagePrefixes | Where-Object { $pkg -like " *$_ *" } } |
164+ ForEach-Object { & dism / English " /image:$ScratchDir " / Remove-ProvisionedAppxPackage " /PackageName:$_ " }
165+
166+ # ── 2. Inject current system drivers (optional) ───────────────────────────
167+ if ($InjectCurrentSystemDrivers ) {
168+ & $Log " Exporting all drivers from running system..."
169+ $driverExportRoot = Join-Path $env: TEMP " WinUtil_DriverExport_$ ( Get-Random ) "
170+ New-Item - Path $driverExportRoot - ItemType Directory - Force | Out-Null
171+ try {
172+ Export-WindowsDriver - Online - Destination $driverExportRoot | Out-Null
173+
174+ & $Log " Injecting current system drivers into install.wim..."
175+ Add-DriversToImage - MountPath $ScratchDir - DriverDir $driverExportRoot - Label " install" - Logger $Log
176+ & $Log " install.wim driver injection complete."
177+
178+ if ($ISOContentsDir -and (Test-Path $ISOContentsDir )) {
179+ $bootWim = Join-Path $ISOContentsDir " sources\boot.wim"
180+ if (Test-Path $bootWim ) {
181+ & $Log " Injecting current system drivers into boot.wim..."
182+ Invoke-BootWimInject - BootWimPath $bootWim - DriverDir $driverExportRoot - Logger $Log
183+ } else {
184+ & $Log " Warning: boot.wim not found — skipping boot.wim driver injection."
185+ }
186+ }
187+ } catch {
188+ & $Log " Error during driver export/injection: $_ "
189+ } finally {
190+ Remove-Item - Path $driverExportRoot - Recurse - Force - ErrorAction SilentlyContinue
191+ }
192+ } else {
193+ & $Log " Driver injection skipped."
164194 }
165195
166- # ═════════════════════════════════════════════════════════════════════════
167- # 2. Remove OneDrive
168- # ═════════════════════════════════════════════════════════════════════════
196+ # ── 3. Remove OneDrive ────────────────────────────────────────────────────
169197 & $Log " Removing OneDrive..."
170198 & takeown / f " $ScratchDir \Windows\System32\OneDriveSetup.exe" | Out-Null
171199 & icacls " $ScratchDir \Windows\System32\OneDriveSetup.exe" / grant " $ ( $adminGroup.Value ) :(F)" / T / C | Out-Null
172200 Remove-Item - Path " $ScratchDir \Windows\System32\OneDriveSetup.exe" - Force - ErrorAction SilentlyContinue
173201
174- # ═════════════════════════════════════════════════════════════════════════
175- # 3. Registry tweaks
176- # ═════════════════════════════════════════════════════════════════════════
202+ # ── 4. Registry tweaks ────────────────────────────────────────────────────
177203 & $Log " Loading offline registry hives..."
178204 reg load HKLM\zCOMPONENTS " $ScratchDir \Windows\System32\config\COMPONENTS"
179205 reg load HKLM\zDEFAULT " $ScratchDir \Windows\System32\config\default"
@@ -222,14 +248,37 @@ function Invoke-WinUtilISOScript {
222248 Set-ISOScriptReg ' HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' ' BypassNRO' ' REG_DWORD' ' 1'
223249
224250 if ($AutoUnattendXml ) {
225- # ── Place autounattend.xml inside the WIM (Sysprep) ──────────────────
226- $sysprepDest = " $ScratchDir \Windows\System32\Sysprep\autounattend.xml"
227- Set-Content - Path $sysprepDest - Value $AutoUnattendXml - Encoding UTF8 - Force
228- & $Log " Written autounattend.xml to Sysprep directory."
229-
230- # ── Place autounattend.xml at the ISO / USB root ──────────────────────
231- # Windows Setup reads this file first (before booting into the OS),
232- # which is what drives the local-account / OOBE bypass at install time.
251+ try {
252+ $xmlDoc = [xml ]::new()
253+ $xmlDoc.LoadXml ($AutoUnattendXml )
254+
255+ $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable )
256+ $nsMgr.AddNamespace (" sg" , " https://schneegans.de/windows/unattend-generator/" )
257+
258+ $fileNodes = $xmlDoc.SelectNodes (" //sg:File" , $nsMgr )
259+ if ($fileNodes -and $fileNodes.Count -gt 0 ) {
260+ foreach ($fileNode in $fileNodes ) {
261+ $absPath = $fileNode.GetAttribute (" path" )
262+ $relPath = $absPath -replace ' ^[A-Za-z]:[/\\]' , ' '
263+ $destPath = Join-Path $ScratchDir $relPath
264+ New-Item - Path (Split-Path $destPath - Parent) - ItemType Directory - Force - ErrorAction SilentlyContinue | Out-Null
265+
266+ $ext = [IO.Path ]::GetExtension($destPath ).ToLower()
267+ $encoding = switch ($ext ) {
268+ { $_ -in ' .ps1' , ' .xml' } { [System.Text.Encoding ]::UTF8 }
269+ { $_ -in ' .reg' , ' .vbs' , ' .js' } { [System.Text.UnicodeEncoding ]::new($false , $true ) }
270+ default { [System.Text.Encoding ]::Default }
271+ }
272+ [System.IO.File ]::WriteAllBytes($destPath , ($encoding.GetPreamble () + $encoding.GetBytes ($fileNode.InnerText.Trim ())))
273+ & $Log " Pre-staged setup script: $relPath "
274+ }
275+ } else {
276+ & $Log " Warning: no <Extensions><File> nodes found in autounattend.xml — setup scripts not pre-staged."
277+ }
278+ } catch {
279+ & $Log " Warning: could not pre-stage setup scripts from autounattend.xml: $_ "
280+ }
281+
233282 if ($ISOContentsDir -and (Test-Path $ISOContentsDir )) {
234283 $isoDest = Join-Path $ISOContentsDir " autounattend.xml"
235284 Set-Content - Path $isoDest - Value $AutoUnattendXml - Encoding UTF8 - Force
@@ -272,8 +321,8 @@ function Invoke-WinUtilISOScript {
272321 Remove-ISOScriptReg ' HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate'
273322
274323 & $Log " Disabling Copilot..."
275- Set-ISOScriptReg ' HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' ' TurnOffWindowsCopilot' ' REG_DWORD' ' 1'
276- Set-ISOScriptReg ' HKLM\zSOFTWARE\Policies\Microsoft\Edge' ' HubsSidebarEnabled' ' REG_DWORD' ' 0'
324+ Set-ISOScriptReg ' HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' ' TurnOffWindowsCopilot' ' REG_DWORD' ' 1'
325+ Set-ISOScriptReg ' HKLM\zSOFTWARE\Policies\Microsoft\Edge' ' HubsSidebarEnabled' ' REG_DWORD' ' 0'
277326 Set-ISOScriptReg ' HKLM\zSOFTWARE\Policies\Microsoft\Windows\Explorer' ' DisableSearchBoxSuggestions' ' REG_DWORD' ' 1'
278327
279328 & $Log " Disabling Windows Update during OOBE (re-enabled on first logon via FirstLogon.ps1)..."
@@ -304,12 +353,9 @@ function Invoke-WinUtilISOScript {
304353 reg unload HKLM\zSOFTWARE
305354 reg unload HKLM\zSYSTEM
306355
307- # ═════════════════════════════════════════════════════════════════════════
308- # 4. Delete scheduled task definition files
309- # ═════════════════════════════════════════════════════════════════════════
356+ # ── 5. Delete scheduled task definition files ─────────────────────────────
310357 & $Log " Deleting scheduled task definition files..."
311358 $tasksPath = " $ScratchDir \Windows\System32\Tasks"
312-
313359 Remove-Item " $tasksPath \Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" - Force - ErrorAction SilentlyContinue
314360 Remove-Item " $tasksPath \Microsoft\Windows\Customer Experience Improvement Program" - Recurse - Force - ErrorAction SilentlyContinue
315361 Remove-Item " $tasksPath \Microsoft\Windows\Application Experience\ProgramDataUpdater" - Force - ErrorAction SilentlyContinue
@@ -321,12 +367,9 @@ function Invoke-WinUtilISOScript {
321367 Remove-Item " $tasksPath \Microsoft\Windows\WaaSMedic" - Recurse - Force - ErrorAction SilentlyContinue
322368 Remove-Item " $tasksPath \Microsoft\Windows\WindowsUpdate" - Recurse - Force - ErrorAction SilentlyContinue
323369 Remove-Item " $tasksPath \Microsoft\WindowsUpdate" - Recurse - Force - ErrorAction SilentlyContinue
324-
325370 & $Log " Scheduled task files deleted."
326371
327- # ═════════════════════════════════════════════════════════════════════════
328- # 5. Remove ISO support folder (fresh-install only; not needed)
329- # ═════════════════════════════════════════════════════════════════════════
372+ # ── 6. Remove ISO support folder ─────────────────────────────────────────
330373 if ($ISOContentsDir -and (Test-Path $ISOContentsDir )) {
331374 & $Log " Removing ISO support\ folder..."
332375 Remove-Item - Path (Join-Path $ISOContentsDir " support" ) - Recurse - Force - ErrorAction SilentlyContinue
0 commit comments