9 Commits

Author SHA1 Message Date
Jeffrey
4d9da4749b Merge branch 'master' of https://github.com/Raphire/Win11Debloat 2026-05-20 16:29:41 +02:00
Jeffrey
5cf9ac4082 Bump version 2026-05-20 16:29:33 +02:00
Jeffrey
924c192ca5 Add Registry write fall-back in case applying registry file fails (#592)
* Continue on registry failures and show details after execution

* Temporarily remove DisableSearchHighlights and DisableSearchHistory settings

* Remove widget-related registry changes as they're no longer required for disabling widgets

* Update tooltip for DisableTelemetry feature to clarify impact on Windows Insider updates
2026-05-20 16:29:06 +02:00
Jeffrey
2a5cb986c9 Merge branch 'master' of https://github.com/Raphire/Win11Debloat 2026-05-17 17:56:01 +02:00
Jeffrey
66982ada28 Limit backup restore files to json only 2026-05-17 17:55:59 +02:00
Ahmad Z. Shatnawi
489af33a8b Fix: Increase System Restore point creation timeout to 90 seconds (#586) 2026-05-17 17:50:36 +02:00
Jeffrey
51aa288dfd Bump version 2026-05-12 00:01:57 +02:00
Jeffrey
24a6f1bcf8 Fix capture and restore of signed dword/qword registry values
Co-authored-by: Copilot <copilot@github.com>
2026-05-11 19:14:08 +02:00
Jeffrey
8ac664e45f Add restart instructions to registry restore success message 2026-05-10 23:26:22 +02:00
14 changed files with 348 additions and 109 deletions

View File

@@ -366,7 +366,7 @@
{ {
"FeatureId": "DisableTelemetry", "FeatureId": "DisableTelemetry",
"Label": "Disable telemetry, tracking & targeted ads", "Label": "Disable telemetry, tracking & targeted ads",
"ToolTip": "This setting disables telemetry, diagnostic data collection, activity history, app-launch tracking, targeted ads and more. It limits the data that is sent to Microsoft about your device and usage.", "ToolTip": "This setting disables telemetry, diagnostic data collection, activity history, app-launch tracking, targeted ads and more. It limits the data that is sent to Microsoft about your device and usage. If you are a Windows Insider, updates may be blocked until optional diagnostic data collection is turned back on.",
"Category": "Privacy & Suggested Content", "Category": "Privacy & Suggested Content",
"RegistryKey": "Disable_Telemetry.reg", "RegistryKey": "Disable_Telemetry.reg",
"ApplyText": "Disabling telemetry, diagnostic data, activity history, app-launch tracking and targeted ads...", "ApplyText": "Disabling telemetry, diagnostic data, activity history, app-launch tracking and targeted ads...",
@@ -601,28 +601,6 @@
"MinVersion": 22621, "MinVersion": 22621,
"MaxVersion": null "MaxVersion": null
}, },
{
"FeatureId": "DisableSearchHighlights",
"Label": "Disable Search Highlights in the taskbar search box",
"ToolTip": "This will turn off Search Highlights, which shows dynamically curated branded content and trending topics in the Windows search box on the taskbar.",
"Category": "Start Menu & Search",
"RegistryKey": "Disable_Search_Highlights.reg",
"ApplyText": "Disabling Search Highlights in the Windows search box...",
"RegistryUndoKey": "Enable_Search_Highlights.reg",
"MinVersion": 22621,
"MaxVersion": null
},
{
"FeatureId": "DisableSearchHistory",
"Label": "Disable local Windows Search history",
"ToolTip": "This setting disables local search history in Windows Search. This does not affect web search history or the search history saved in Microsoft Edge.",
"Category": "Start Menu & Search",
"RegistryKey": "Disable_Search_History.reg",
"ApplyText": "Disabling search history...",
"RegistryUndoKey": "Enable_Search_History.reg",
"MinVersion": null,
"MaxVersion": null
},
{ {
"FeatureId": "DisableSettings365Ads", "FeatureId": "DisableSettings365Ads",
"Label": "Hide Microsoft 365 Copilot ads in Settings Home", "Label": "Hide Microsoft 365 Copilot ads in Settings Home",
@@ -878,12 +856,12 @@
{ {
"FeatureId": "DisableWidgets", "FeatureId": "DisableWidgets",
"Label": "Disable widgets on the taskbar & lock screen", "Label": "Disable widgets on the taskbar & lock screen",
"ToolTip": "This will disable the widgets features in Windows, including the widgets button on the taskbar and the widgets that can appear on the lock screen. This feature uses policies, which will lock down certain settings.", "ToolTip": "This will disable the widgets features in Windows, including the widgets button on the taskbar and the widgets that can appear on the lock screen.",
"Category": "Taskbar", "Category": "Taskbar",
"Priority": 4, "Priority": 4,
"RegistryKey": "Disable_Widgets_Service.reg", "RegistryKey": null,
"ApplyText": "Disabling widgets on the taskbar & lock screen...", "ApplyText": null,
"RegistryUndoKey": "Enable_Widgets_Service.reg", "RegistryUndoKey": null,
"MinVersion": null, "MinVersion": null,
"MaxVersion": null "MaxVersion": null
}, },

Binary file not shown.

View File

@@ -226,13 +226,20 @@ function Convert-RegistryValueToSnapshot {
$valueKind = $RegistryKey.GetValueKind($ValueName) $valueKind = $RegistryKey.GetValueKind($ValueName)
$value = $RegistryKey.GetValue($ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) $value = $RegistryKey.GetValue($ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
try {
$normalizedValue = switch ($valueKind) { $normalizedValue = switch ($valueKind) {
([Microsoft.Win32.RegistryValueKind]::Binary) { @($value | ForEach-Object { [int]$_ }) } ([Microsoft.Win32.RegistryValueKind]::Binary) { @($value | ForEach-Object { [int]$_ }) }
([Microsoft.Win32.RegistryValueKind]::MultiString) { @($value) } ([Microsoft.Win32.RegistryValueKind]::MultiString) { @($value) }
([Microsoft.Win32.RegistryValueKind]::DWord) { [uint32]$value } ([Microsoft.Win32.RegistryValueKind]::DWord) { [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$value), 0) }
([Microsoft.Win32.RegistryValueKind]::QWord) { [uint64]$value } ([Microsoft.Win32.RegistryValueKind]::QWord) { [BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$value), 0) }
default { if ($null -ne $value) { [string]$value } else { $null } } default { if ($null -ne $value) { [string]$value } else { $null } }
} }
}
catch {
$valueType = if ($null -ne $value) { $value.GetType().FullName } else { '<null>' }
$valueForLog = if ($null -eq $value) { '<null>' } elseif ($value -is [array]) { ($value -join ',') } else { [string]$value }
throw "Failed to normalize registry value for backup. Key='$($RegistryKey.Name)' Name='$ValueName' Kind='$valueKind' RawType='$valueType' RawValue='$valueForLog'. InnerError: $($_.Exception.Message)"
}
return @{ return @{
Name = $ValueName Name = $ValueName

View File

@@ -6,7 +6,7 @@ function CreateSystemRestorePoint {
# In GUI mode, skip the prompt and just try to enable it # In GUI mode, skip the prompt and just try to enable it
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') { if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
try { try {
$enableResult = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock { $enableResult = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
try { try {
Enable-ComputerRestore -Drive "$env:SystemDrive" Enable-ComputerRestore -Drive "$env:SystemDrive"
return $null return $null
@@ -33,7 +33,7 @@ function CreateSystemRestorePoint {
if (-not $failed) { if (-not $failed) {
try { try {
$result = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock { $result = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
try { try {
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) } $recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
} }

View File

@@ -26,10 +26,6 @@ function ExecuteParameter {
# Also remove the app package for Copilot # Also remove the app package for Copilot
RemoveApps 'Microsoft.Copilot' RemoveApps 'Microsoft.Copilot'
} }
'DisableWidgets' {
# Also remove the app packages for Widgets
RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'
}
} }
return return
} }
@@ -86,6 +82,13 @@ function ExecuteParameter {
RemoveApps $appsList RemoveApps $appsList
return return
} }
'DisableWidgets' {
Write-Host "> Disabling widgets on the taskbar & lock screen..."
# Stop widgets related processes before removing the app packages to prevent potential issues
Get-Process *Widget* | Stop-Process
RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'
}
"EnableWindowsSandbox" { "EnableWindowsSandbox" {
Write-Host "> Enabling Windows Sandbox..." Write-Host "> Enabling Windows Sandbox..."
EnableWindowsFeature "Containers-DisposableClientVM" EnableWindowsFeature "Containers-DisposableClientVM"
@@ -138,6 +141,8 @@ function ExecuteParameter {
# Executes all selected parameters/features # Executes all selected parameters/features
function ExecuteAllChanges { function ExecuteAllChanges {
$script:RegistryImportFailures = 0
# Build list of actionable parameters (skip control params and data-only params) # Build list of actionable parameters (skip control params and data-only params)
$actionableKeys = @() $actionableKeys = @()
foreach ($paramKey in $script:Params.Keys) { foreach ($paramKey in $script:Params.Keys) {
@@ -166,20 +171,25 @@ function ExecuteAllChanges {
if ($hasRegistryBackedFeature) { if ($hasRegistryBackedFeature) {
$currentStep++ $currentStep++
if ($script:ApplyProgressCallback) { if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup" & $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..."
} }
Write-Host "> Creating registry backup..." Write-Host "> Creating registry backup..."
try {
New-RegistrySettingsBackup -ActionableKeys $actionableKeys | Out-Null New-RegistrySettingsBackup -ActionableKeys $actionableKeys | Out-Null
} }
catch {
throw "Registry backup failed before applying changes. $($_.Exception.Message)"
}
}
# Create restore point if requested (CLI only - GUI handles this separately) # Create restore point if requested (CLI only - GUI handles this separately)
if ($script:Params.ContainsKey("CreateRestorePoint")) { if ($script:Params.ContainsKey("CreateRestorePoint")) {
$currentStep++ $currentStep++
if ($script:ApplyProgressCallback) { if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point" & $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point, this may take a moment..."
} }
Write-Host "> Attempting to create a system restore point..." Write-Host "> Creating a system restore point..."
CreateSystemRestorePoint CreateSystemRestorePoint
Write-Host "" Write-Host ""
} }
@@ -211,4 +221,9 @@ function ExecuteAllChanges {
ExecuteParameter -paramKey $paramKey ExecuteParameter -paramKey $paramKey
} }
if ($script:RegistryImportFailures -gt 0) {
Write-Host ""
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
}
} }

View File

@@ -8,33 +8,44 @@ function ImportRegistryFile {
Write-Host $message Write-Host $message
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User") $usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
$regFilePath = if ($usesOfflineHive) { $regFileDirectory = if ($usesOfflineHive) {
"$script:RegfilesPath\Sysprep\$path" Join-Path $script:RegfilesPath "Sysprep"
} }
else { else {
"$script:RegfilesPath\$path" $script:RegfilesPath
} }
$regFilePath = Join-Path $regFileDirectory $path
if (-not (Test-Path $regFilePath)) { if (-not (Test-Path $regFilePath)) {
$errorMessage = "Unable to find registry file: $path ($regFilePath)" $errorMessage = "Unable to find registry file: $path ($regFilePath)"
$script:RegistryImportFailures++
Write-Host "Error: $errorMessage" -ForegroundColor Red Write-Host "Error: $errorMessage" -ForegroundColor Red
Write-Host "" Write-Host ""
throw $errorMessage throw $errorMessage
} }
# Reset exit code before running reg.exe for reliable success detection $regResult = $null
$global:LASTEXITCODE = 0 $offlineHiveLoaded = $false
try {
if ($usesOfflineHive) { if ($usesOfflineHive) {
# Sysprep targets Default user, User targets the specified user # Sysprep targets Default user, User targets the specified user
$hiveDatPath = if ($script:Params.ContainsKey("Sysprep")) { $targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
GetUserDirectory -userName "Default" -fileName "NTUSER.DAT" $hiveDatPath = GetUserDirectory -userName $targetUserName -fileName "NTUSER.DAT"
} else {
GetUserDirectory -userName $script:Params.Item("User") -fileName "NTUSER.DAT" $global:LASTEXITCODE = 0
reg load "HKU\Default" $hiveDatPath | Out-Null
$loadExitCode = $LASTEXITCODE
if ($loadExitCode -ne 0) {
throw "Failed importing registry file '$path'. Offline hive load failed: Failed to load user hive at '$hiveDatPath' (exit code: $loadExitCode)"
}
$offlineHiveLoaded = $true
} }
$regResult = Invoke-NonBlocking -ScriptBlock { $regResult = Invoke-NonBlocking -ScriptBlock {
param($hivePath, $targetRegFilePath) param($targetRegFilePath)
$result = @{ $result = @{
Output = @() Output = @()
ExitCode = 0 ExitCode = 0
@@ -43,13 +54,6 @@ function ImportRegistryFile {
try { try {
$global:LASTEXITCODE = 0 $global:LASTEXITCODE = 0
reg load "HKU\Default" $hivePath | Out-Null
$loadExitCode = $LASTEXITCODE
if ($loadExitCode -ne 0) {
throw "Failed to load user hive at '$hivePath' (exit code: $loadExitCode)"
}
$output = reg import $targetRegFilePath 2>&1 $output = reg import $targetRegFilePath 2>&1
$importExitCode = $LASTEXITCODE $importExitCode = $LASTEXITCODE
@@ -66,27 +70,9 @@ function ImportRegistryFile {
$result.Error = $_.Exception.Message $result.Error = $_.Exception.Message
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 } $result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
} }
finally {
$global:LASTEXITCODE = 0
reg unload "HKU\Default" | Out-Null
$unloadExitCode = $LASTEXITCODE
if ($unloadExitCode -ne 0 -and -not $result.Error) {
$result.Error = "Failed to unload registry hive HKU\Default (exit code: $unloadExitCode)"
$result.ExitCode = $unloadExitCode
}
}
return $result return $result
} -ArgumentList @($hiveDatPath, $regFilePath)
}
else {
$regResult = Invoke-NonBlocking -ScriptBlock {
param($targetRegFilePath)
$global:LASTEXITCODE = 0
$output = reg import $targetRegFilePath 2>&1
return @{ Output = @($output); ExitCode = $LASTEXITCODE; Error = $null }
} -ArgumentList $regFilePath } -ArgumentList $regFilePath
}
$regOutput = @($regResult.Output) $regOutput = @($regResult.Output)
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error $hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
@@ -107,11 +93,27 @@ function ImportRegistryFile {
if (-not $hasSuccess) { if (-not $hasSuccess) {
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" } $details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
$errorMessage = "Failed importing registry file '$path'. $details" Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
Write-Host $errorMessage -ForegroundColor Red Invoke-RegistryOperationsFromRegFile -RegFilePath $regFilePath
Write-Host "" Write-Host "Fallback import succeeded for '$path'." -ForegroundColor Yellow
throw $errorMessage
} }
Write-Host "" Write-Host ""
} }
catch {
$script:RegistryImportFailures++
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host ""
}
finally {
if ($offlineHiveLoaded) {
$global:LASTEXITCODE = 0
reg unload "HKU\Default" | Out-Null
$unloadExitCode = $LASTEXITCODE
if ($unloadExitCode -ne 0) {
Write-Warning "Failed to unload registry hive HKU\Default after importing '$path' (exit code: $unloadExitCode)"
}
}
}
}

View File

@@ -157,8 +157,14 @@ function Convert-RegistryValueDataFromBackup {
) )
switch ($Kind) { switch ($Kind) {
([Microsoft.Win32.RegistryValueKind]::DWord) { return [uint32]$Data } ([Microsoft.Win32.RegistryValueKind]::DWord) {
([Microsoft.Win32.RegistryValueKind]::QWord) { return [uint64]$Data } $unsigned = [uint32]$Data
return [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0)
}
([Microsoft.Win32.RegistryValueKind]::QWord) {
$unsigned = [uint64]$Data
return [BitConverter]::ToInt64([BitConverter]::GetBytes($unsigned), 0)
}
([Microsoft.Win32.RegistryValueKind]::MultiString) { return @($Data | ForEach-Object { [string]$_ }) } ([Microsoft.Win32.RegistryValueKind]::MultiString) { return @($Data | ForEach-Object { [string]$_ }) }
([Microsoft.Win32.RegistryValueKind]::Binary) { ([Microsoft.Win32.RegistryValueKind]::Binary) {
$bytes = Convert-BackupDataToByteArray -Data $Data $bytes = Convert-BackupDataToByteArray -Data $Data

View File

@@ -124,6 +124,8 @@ function Show-ApplyModal {
try { try {
ExecuteAllChanges ExecuteAllChanges
$registryImportFailureCount = [int]$script:RegistryImportFailures
# Restart explorer if requested # Restart explorer if requested
if ($RestartExplorer -and -not $script:CancelRequested) { if ($RestartExplorer -and -not $script:CancelRequested) {
RestartExplorer RestartExplorer
@@ -139,7 +141,7 @@ function Show-ApplyModal {
Write-Host "" Write-Host ""
if ($script:CancelRequested) { if ($script:CancelRequested) {
Write-Host "Script execution was cancelled by the user. Some changes may not have been applied." Write-Host "Script execution was cancelled by the user. Some changes may not have been applied."
} else { } elseif ($registryImportFailureCount -eq 0) {
Write-Host "All changes have been applied successfully!" Write-Host "All changes have been applied successfully!"
} }
@@ -153,6 +155,11 @@ function Show-ApplyModal {
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d")) $script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
$script:ApplyCompletionTitleEl.Text = "Cancelled" $script:ApplyCompletionTitleEl.Text = "Cancelled"
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user." $script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
} elseif ($registryImportFailureCount -gt 0) {
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
$script:ApplyCompletionMessageEl.Text = "$registryImportFailureCount registry change(s) failed. See console for details."
} else { } else {
$script:ApplyCompletionTitleEl.Text = "Changes Applied" $script:ApplyCompletionTitleEl.Text = "Changes Applied"

View File

@@ -255,7 +255,7 @@ function Show-RestoreBackupDialog {
$openDialog = New-Object Microsoft.Win32.OpenFileDialog $openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Select Registry Backup File' $openDialog.Title = 'Select Registry Backup File'
$openDialog.Filter = 'Registry backup (*.json)|*.json|All files (*.*)|*.*' $openDialog.Filter = 'Registry backup (*.json)|*.json'
$openDialog.DefaultExt = '.json' $openDialog.DefaultExt = '.json'
$openDialog.InitialDirectory = $script:RegistryBackupsPath $openDialog.InitialDirectory = $script:RegistryBackupsPath

View File

@@ -24,7 +24,7 @@ function Show-RestoreBackupWindow {
Write-Host "User confirmed registry restore for $($backup.Target)." Write-Host "User confirmed registry restore for $($backup.Target)."
Restore-RegistryBackupState -Backup $backup Restore-RegistryBackupState -Backup $backup
$successMessage = 'Registry backup restored successfully.' $successMessage = 'Registry backup restored successfully. Please restart your computer for all changes to take effect.'
} }
elseif ($dialogResult.Result -eq 'RestoreStartMenu') { elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
$scope = $dialogResult.StartMenuScope $scope = $dialogResult.StartMenuScope

View File

@@ -0,0 +1,223 @@
function Get-NormalizedRegistryValueName {
param(
[AllowNull()]
$ValueName
)
if ([string]::IsNullOrEmpty([string]$ValueName)) {
return ''
}
return [string]$ValueName
}
function Convert-RegOperationToValueKind {
param(
[Parameter(Mandatory)]
$Operation
)
$valueName = if ([string]::IsNullOrEmpty([string]$Operation.ValueName)) { '' } else { [string]$Operation.ValueName }
$valueType = [string]$Operation.ValueType
$operationKeyPath = [string]$Operation.KeyPath
switch ($valueType) {
'DWord' {
$unsigned = [uint32]$Operation.ValueData
$value = [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0)
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::DWord; Value = $value }
}
'String' {
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::String; Value = [string]$Operation.ValueData }
}
'Binary' {
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::Binary; Value = [byte[]]$Operation.ValueData }
}
default {
throw "Unsupported value type '$valueType' while applying reg operation for '$operationKeyPath'"
}
}
}
function Remove-RegistrySubKeyTreeIfExists {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RootKey,
[Parameter(Mandatory)]
[string]$SubKeyPath
)
try {
$RootKey.DeleteSubKeyTree($SubKeyPath, $false)
}
catch [System.UnauthorizedAccessException], [System.Security.SecurityException] {
throw
}
catch {
# Best-effort cleanup only; missing keys are fine.
}
}
function Get-RegistryKeyForOperation {
param(
[Parameter(Mandatory)]
[string]$RegistryPath,
[switch]$CreateIfMissing,
[bool]$OpenKey = $true
)
$parts = Split-RegistryPath -path $RegistryPath
if (-not $parts) {
throw "Unsupported registry path: $RegistryPath"
}
$rootKey = Get-RegistryRootKey -hiveName $parts.Hive
if (-not $rootKey) {
throw "Unsupported registry hive '$($parts.Hive)' in path '$RegistryPath'"
}
$subKeyPath = $parts.SubKey
if ([string]::IsNullOrWhiteSpace($subKeyPath)) {
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $null; Key = $rootKey }
}
if (-not $OpenKey) {
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $subKeyPath; Key = $null }
}
$key = if ($CreateIfMissing) {
$rootKey.CreateSubKey($subKeyPath)
}
else {
$rootKey.OpenSubKey($subKeyPath, $true)
}
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $subKeyPath; Key = $key }
}
function Invoke-RegistryDeleteValueOperation {
param(
[Parameter(Mandatory)]
$Operation,
[Parameter(Mandatory)]
$KeyInfo
)
if ($null -eq $KeyInfo.Key) {
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
$displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName }
Write-Verbose "Unable to find or open key '$($Operation.KeyPath)' and value '$displayValueName'"
return
}
try {
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
$KeyInfo.Key.DeleteValue($valueName, $false)
}
finally {
$KeyInfo.Key.Close()
}
}
function Invoke-RegistrySetValueOperation {
param(
[Parameter(Mandatory)]
$Operation,
[Parameter(Mandatory)]
$KeyInfo
)
if ($null -eq $KeyInfo.Key) {
throw [System.UnauthorizedAccessException]::new("Unable to open or create registry key '$($Operation.KeyPath)'")
}
try {
$setArgs = Convert-RegOperationToValueKind -Operation $Operation
$KeyInfo.Key.SetValue($setArgs.Name, $setArgs.Value, $setArgs.Kind)
}
finally {
$KeyInfo.Key.Close()
}
}
function Write-RegistryOperationAccessDeniedWarning {
param(
[Parameter(Mandatory)]
$Operation,
[Parameter(Mandatory)]
[string]$ExceptionMessage
)
$keyPath = [string]$Operation.KeyPath
$operationType = [string]$Operation.OperationType
if ($operationType -eq 'SetValue' -or $operationType -eq 'DeleteValue') {
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
$displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName }
Write-Warning "Skipping operation '$operationType' on key '$keyPath' value '$displayValueName' due to access restrictions: $ExceptionMessage"
return
}
Write-Warning "Skipping operation '$operationType' on key '$keyPath' due to access restrictions: $ExceptionMessage"
}
function Invoke-RegistryOperation {
param(
[Parameter(Mandatory)]
$Operation,
[Parameter(Mandatory)]
[string]$RegFilePath
)
$operationType = [string]$Operation.OperationType
$isSetValueOperation = $operationType -eq 'SetValue'
$isDeleteKeyOperation = $operationType -eq 'DeleteKey'
$keyInfo = Get-RegistryKeyForOperation -RegistryPath $Operation.KeyPath -CreateIfMissing:$isSetValueOperation -OpenKey:(-not $isDeleteKeyOperation)
switch ($operationType) {
'DeleteKey' {
if ($null -ne $keyInfo.SubKeyPath) {
Remove-RegistrySubKeyTreeIfExists -RootKey $keyInfo.RootKey -SubKeyPath $keyInfo.SubKeyPath
}
}
'DeleteValue' {
Invoke-RegistryDeleteValueOperation -Operation $Operation -KeyInfo $keyInfo
}
'SetValue' {
Invoke-RegistrySetValueOperation -Operation $Operation -KeyInfo $keyInfo
}
default {
throw "Unsupported reg operation type '$($Operation.OperationType)' in '$RegFilePath'"
}
}
}
function Invoke-RegistryOperationsFromRegFile {
param(
[Parameter(Mandatory)]
[string]$RegFilePath
)
$accessDeniedCount = 0
$operations = @(Get-RegFileOperations -regFilePath $RegFilePath)
$totalOperations = $operations.Count
foreach ($operation in $operations) {
try {
Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath
}
catch [System.UnauthorizedAccessException], [System.Security.SecurityException] {
$accessDeniedCount++
Write-RegistryOperationAccessDeniedWarning -Operation $operation -ExceptionMessage $_.Exception.Message
}
}
if ($totalOperations -gt 0 -and $accessDeniedCount -eq $totalOperations) {
throw "Registry fallback import could not apply any operations in '$RegFilePath' because all $accessDeniedCount operation(s) were blocked by access restrictions."
}
if ($accessDeniedCount -gt 0) {
Write-Warning "Registry fallback import completed with $accessDeniedCount access-restricted operation(s) skipped in '$RegFilePath'."
}
}

View File

@@ -141,7 +141,7 @@ if (-not $isAdmin) {
} }
# Define script-level variables & paths # Define script-level variables & paths
$script:Version = "2026.05.10" $script:Version = "2026.05.20"
$configPath = Join-Path $PSScriptRoot 'Config' $configPath = Join-Path $PSScriptRoot 'Config'
$logsPath = Join-Path $PSScriptRoot 'Logs' $logsPath = Join-Path $PSScriptRoot 'Logs'
$schemasPath = Join-Path $PSScriptRoot 'Schemas' $schemasPath = Join-Path $PSScriptRoot 'Schemas'
@@ -349,6 +349,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1" . "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1" . "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
. "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1" . "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1"
. "$PSScriptRoot/Scripts/Helpers/ApplyRegistryRegFile.ps1"
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1" . "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
# Threading functions # Threading functions
@@ -401,7 +402,7 @@ else {
} }
if ($script:Params.ContainsKey("Sysprep")) { if ($script:Params.ContainsKey("Sysprep")) {
$defaultUserPath = GetUserDirectory -userName "Default" GetUserDirectory -userName "Default" | Out-Null
# Exit script if run in Sysprep mode on Windows 10 # Exit script if run in Sysprep mode on Windows 10
if ($WinVersion -lt 22000) { if ($WinVersion -lt 22000) {
@@ -412,10 +413,10 @@ if ($script:Params.ContainsKey("Sysprep")) {
# Ensure that target user exists, if User or AppRemovalTarget parameter was provided # Ensure that target user exists, if User or AppRemovalTarget parameter was provided
if ($script:Params.ContainsKey("User")) { if ($script:Params.ContainsKey("User")) {
$userPath = GetUserDirectory -userName $script:Params.Item("User") GetUserDirectory -userName $script:Params.Item("User") | Out-Null
} }
if ($script:Params.ContainsKey("AppRemovalTarget")) { if ($script:Params.ContainsKey("AppRemovalTarget")) {
$userPath = GetUserDirectory -userName $script:Params.Item("AppRemovalTarget") GetUserDirectory -userName $script:Params.Item("AppRemovalTarget") | Out-Null
} }
# Remove LastUsedSettings.json file if it exists and is empty # Remove LastUsedSettings.json file if it exists and is empty