Compare commits

...

10 Commits

Author SHA1 Message Date
Jeffrey
33b77f19a0
Add confirmation dialogs & warning for Windows Terminal Removal
Removal of this app can cause Win11Debloat to fail, if the script is launched via Windows Terminal
2026-06-01 22:53:28 +02:00
Jeffrey
37872b2030
Update CONTRIBUTING.md 2026-05-26 15:57:34 +02:00
Jeffrey
abfc5db2c3
Improve log output in Get.ps1 and clean up file exclusions 2026-05-25 14:35:39 +02:00
Jeffrey
1d828d6a78
Fix typo in Disable_Game_Bar_Integration Sysprep registry file 2026-05-24 14:53:12 +02:00
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
18 changed files with 383 additions and 129 deletions

View File

@ -109,7 +109,7 @@ Avoid these common mistakes when contributing:
Placing files in the wrong directory will cause the script to fail when trying to apply or undo changes.
6. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes. A feature that can't be undone will frustrate users.
6. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes.
7. **Not Testing User/Sysprep Functionality**: Always test that your feature works when applied to another user or to the Windows default user with Sysprep. Sysprep changes can be tested by creating new users after running the script.

View File

@ -781,9 +781,9 @@
{
"FriendlyName": "Windows Terminal",
"AppId": "Microsoft.WindowsTerminal",
"Description": "Default terminal app in windows 11 (Command Prompt, PowerShell, WSL)",
"Description": "Default terminal app in windows 11 (Command Prompt, PowerShell, WSL), WARNING: Win11Debloat if it is launched via Windows Terminal.",
"SelectedByDefault": false,
"Recommendation": "optional"
"Recommendation": "unsafe"
},
{
"FriendlyName": "Xbox TCUI Framework",

View File

@ -288,7 +288,7 @@
"Label": "Remove the apps specified with the 'Apps' parameter",
"Category": null,
"RegistryKey": null,
"ApplyText": null,
"ApplyText": "Removing selected apps...",
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
@ -298,7 +298,7 @@
"Label": "The selection of apps to remove, specified as a comma separated list. Use 'Default' (or omit) to use the default apps list",
"Category": null,
"RegistryKey": null,
"ApplyText": null,
"ApplyText": "Removing selected apps...",
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
@ -308,7 +308,7 @@
"Label": "Remove custom selection of apps",
"Category": null,
"RegistryKey": null,
"ApplyText": null,
"ApplyText": "Removing selected apps...",
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
@ -318,7 +318,7 @@
"Label": "Remove the Mail, Calendar, and People apps",
"Category": null,
"RegistryKey": null,
"ApplyText": null,
"ApplyText": "Removing selected apps...",
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
@ -328,7 +328,7 @@
"Label": "Remove the new Outlook for Windows app",
"Category": null,
"RegistryKey": null,
"ApplyText": null,
"ApplyText": "Removing selected apps...",
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
@ -338,7 +338,7 @@
"Label": "Remove the Xbox App and Xbox Gamebar",
"Category": null,
"RegistryKey": null,
"ApplyText": null,
"ApplyText": "Removing selected apps...",
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
@ -348,7 +348,7 @@
"Label": "Remove HP OEM applications",
"Category": null,
"RegistryKey": null,
"ApplyText": null,
"ApplyText": "Removing selected apps...",
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
@ -366,7 +366,7 @@
{
"FeatureId": "DisableTelemetry",
"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",
"RegistryKey": "Disable_Telemetry.reg",
"ApplyText": "Disabling telemetry, diagnostic data, activity history, app-launch tracking and targeted ads...",
@ -601,28 +601,6 @@
"MinVersion": 22621,
"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",
"Label": "Hide Microsoft 365 Copilot ads in Settings Home",
@ -878,12 +856,12 @@
{
"FeatureId": "DisableWidgets",
"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",
"Priority": 4,
"RegistryKey": "Disable_Widgets_Service.reg",
"ApplyText": "Disabling widgets on the taskbar & lock screen...",
"RegistryUndoKey": "Enable_Widgets_Service.reg",
"RegistryKey": null,
"ApplyText": null,
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
},

Binary file not shown.

View File

@ -6,7 +6,7 @@ function CreateSystemRestorePoint {
# 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') {
try {
$enableResult = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
$enableResult = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
try {
Enable-ComputerRestore -Drive "$env:SystemDrive"
return $null
@ -33,7 +33,7 @@ function CreateSystemRestorePoint {
if (-not $failed) {
try {
$result = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
$result = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
try {
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
}
@ -92,4 +92,4 @@ function CreateSystemRestorePoint {
Write-Host "Warning: Continuing without restore point" -ForegroundColor Yellow
}
}
}

View File

@ -26,10 +26,6 @@ function ExecuteParameter {
# Also remove the app package for Copilot
RemoveApps 'Microsoft.Copilot'
}
'DisableWidgets' {
# Also remove the app packages for Widgets
RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'
}
}
return
}
@ -86,6 +82,13 @@ function ExecuteParameter {
RemoveApps $appsList
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" {
Write-Host "> Enabling Windows Sandbox..."
EnableWindowsFeature "Containers-DisposableClientVM"
@ -138,6 +141,8 @@ function ExecuteParameter {
# Executes all selected parameters/features
function ExecuteAllChanges {
$script:RegistryImportFailures = 0
# Build list of actionable parameters (skip control params and data-only params)
$actionableKeys = @()
foreach ($paramKey in $script:Params.Keys) {
@ -166,7 +171,7 @@ function ExecuteAllChanges {
if ($hasRegistryBackedFeature) {
$currentStep++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup"
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..."
}
Write-Host "> Creating registry backup..."
@ -182,9 +187,9 @@ function ExecuteAllChanges {
if ($script:Params.ContainsKey("CreateRestorePoint")) {
$currentStep++
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
Write-Host ""
}
@ -216,4 +221,9 @@ function ExecuteAllChanges {
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
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
$regFilePath = if ($usesOfflineHive) {
"$script:RegfilesPath\Sysprep\$path"
$regFileDirectory = if ($usesOfflineHive) {
Join-Path $script:RegfilesPath "Sysprep"
}
else {
"$script:RegfilesPath\$path"
$script:RegfilesPath
}
$regFilePath = Join-Path $regFileDirectory $path
if (-not (Test-Path $regFilePath)) {
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
$script:RegistryImportFailures++
Write-Host "Error: $errorMessage" -ForegroundColor Red
Write-Host ""
throw $errorMessage
}
# Reset exit code before running reg.exe for reliable success detection
$global:LASTEXITCODE = 0
$regResult = $null
$offlineHiveLoaded = $false
if ($usesOfflineHive) {
# Sysprep targets Default user, User targets the specified user
$hiveDatPath = if ($script:Params.ContainsKey("Sysprep")) {
GetUserDirectory -userName "Default" -fileName "NTUSER.DAT"
} else {
GetUserDirectory -userName $script:Params.Item("User") -fileName "NTUSER.DAT"
try {
if ($usesOfflineHive) {
# Sysprep targets Default user, User targets the specified user
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
$hiveDatPath = GetUserDirectory -userName $targetUserName -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 {
param($hivePath, $targetRegFilePath)
param($targetRegFilePath)
$result = @{
Output = @()
ExitCode = 0
@ -43,13 +54,6 @@ function ImportRegistryFile {
try {
$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
$importExitCode = $LASTEXITCODE
@ -66,52 +70,50 @@ function ImportRegistryFile {
$result.Error = $_.Exception.Message
$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
} -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
}
$regOutput = @($regResult.Output)
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
if ($regOutput) {
foreach ($line in $regOutput) {
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
if ($lineText -and $lineText.Length -gt 0) {
if ($hasSuccess) {
Write-Host $lineText
}
else {
Write-Host $lineText -ForegroundColor Red
$regOutput = @($regResult.Output)
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
if ($regOutput) {
foreach ($line in $regOutput) {
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
if ($lineText -and $lineText.Length -gt 0) {
if ($hasSuccess) {
Write-Host $lineText
}
else {
Write-Host $lineText -ForegroundColor Red
}
}
}
}
}
if (-not $hasSuccess) {
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
$errorMessage = "Failed importing registry file '$path'. $details"
Write-Host $errorMessage -ForegroundColor Red
if (-not $hasSuccess) {
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
Invoke-RegistryOperationsFromRegFile -RegFilePath $regFilePath
Write-Host "Fallback import succeeded for '$path'." -ForegroundColor Yellow
}
Write-Host ""
throw $errorMessage
}
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
Write-Host ""
if ($unloadExitCode -ne 0) {
Write-Warning "Failed to unload registry hive HKU\Default after importing '$path' (exit code: $unloadExitCode)"
}
}
}
}

View File

@ -130,12 +130,8 @@ function Show-AppSelectionWindow {
return
}
if ($selectedApps -contains "Microsoft.WindowsStore" -and -not $Silent) {
$result = Show-MessageBox -Message 'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $window
if ($result -eq 'No') {
return
}
if (-not (ConfirmUnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) {
return
}
SaveCustomAppsListToFile -appsList $selectedApps

View File

@ -123,6 +123,8 @@ function Show-ApplyModal {
$applyWindow.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{
try {
ExecuteAllChanges
$registryImportFailureCount = [int]$script:RegistryImportFailures
# Restart explorer if requested
if ($RestartExplorer -and -not $script:CancelRequested) {
@ -139,7 +141,7 @@ function Show-ApplyModal {
Write-Host ""
if ($script:CancelRequested) {
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!"
}
@ -153,6 +155,11 @@ function Show-ApplyModal {
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
$script:ApplyCompletionTitleEl.Text = "Cancelled"
$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 {
$script:ApplyCompletionTitleEl.Text = "Changes Applied"

View File

@ -1773,15 +1773,11 @@ function Show-MainWindow {
$selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique)
if ($selectedApps.Count -gt 0) {
# Check if Microsoft Store is selected
if ($selectedApps -contains "Microsoft.WindowsStore") {
$result = Show-MessageBox -Message 'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning'
if ($result -eq 'No') {
return
}
if (-not (ConfirmUnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) {
return
}
AddParameter 'RemoveApps'
AddParameter 'Apps' ($selectedApps -join ',')

View File

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

View File

@ -136,12 +136,12 @@ catch {
Exit
}
Write-Output ""
Write-Output "> Cleaning up old Win11Debloat folder..."
# Remove old script folder if it exists, but keep config and log files
# Remove old script folder if it exists, but keep configs, logs and backups
if (Test-Path $tempWorkPath) {
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs,Backups | Remove-Item -Recurse -Force
Write-Output ""
Write-Output "> Cleaning up old Win11Debloat folder..."
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
}
$configDir = Join-Path $tempWorkPath 'Config'
@ -149,6 +149,9 @@ $backupDir = Join-Path $tempWorkPath 'ConfigOld'
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
if (Test-Path "$configDir") {
Write-Output ""
Write-Output "> Backing up existing config files..."
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
$filesToKeep = @(
@ -179,6 +182,9 @@ if (Test-Path "$backupDir") {
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
}
Write-Output ""
Write-Output "> Restoring existing config files..."
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
Remove-Item "$backupDir" -Recurse -Force
}
@ -219,13 +225,13 @@ if ($null -ne $debloatProcess) {
$debloatProcess.WaitForExit()
}
# Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files
# Remove all remaining script files, except for configs, logs and backups
if (Test-Path $tempWorkPath) {
Write-Output ""
Write-Output "> Cleaning up..."
# Cleanup, remove Win11Debloat directory
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs,Backups | Remove-Item -Recurse -Force
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
}
Write-Output ""

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

@ -0,0 +1,34 @@
# Shows confirmation dialogs for apps that require extra caution before removal.
# Returns $true if the user confirmed all warnings (or if no warnings were triggered),
# $false if the user declined any warning.
function ConfirmUnsafeAppRemoval {
param (
[string[]]$SelectedApps,
$Owner = $null
)
# Skip all warnings in Silent mode
if ($Silent) {
return $true
}
# Microsoft Store warning
if ($SelectedApps -contains "Microsoft.WindowsStore") {
$result = Show-MessageBox -Message 'Are you sure that you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $Owner
if ($result -eq 'No') {
return $false
}
}
# Windows Terminal warning
if ($SelectedApps -contains "Microsoft.WindowsTerminal") {
$result = Show-MessageBox -Message 'Are you sure that you wish to remove Windows Terminal? Windows Terminal is the default command-line app for Windows. Ensure you are not running Win11Debloat via Windows Terminal before proceeding to avoid a mid-process failure.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $Owner
if ($result -eq 'No') {
return $false
}
}
return $true
}

View File

@ -141,7 +141,7 @@ if (-not $isAdmin) {
}
# Define script-level variables & paths
$script:Version = "2026.05.11"
$script:Version = "2026.05.20"
$configPath = Join-Path $PSScriptRoot 'Config'
$logsPath = Join-Path $PSScriptRoot 'Logs'
$schemasPath = Join-Path $PSScriptRoot 'Schemas'
@ -349,7 +349,9 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
. "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1"
. "$PSScriptRoot/Scripts/Helpers/ApplyRegistryRegFile.ps1"
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
. "$PSScriptRoot/Scripts/Helpers/ConfirmUnsafeAppRemoval.ps1"
# Threading functions
. "$PSScriptRoot/Scripts/Threading/DoEvents.ps1"
@ -401,7 +403,7 @@ else {
}
if ($script:Params.ContainsKey("Sysprep")) {
$defaultUserPath = GetUserDirectory -userName "Default"
GetUserDirectory -userName "Default" | Out-Null
# Exit script if run in Sysprep mode on Windows 10
if ($WinVersion -lt 22000) {
@ -412,10 +414,10 @@ if ($script:Params.ContainsKey("Sysprep")) {
# Ensure that target user exists, if User or AppRemovalTarget parameter was provided
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")) {
$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