Batch Deleting Windows Registry Entries

After uninstalling software, it sometimes leaves behind leftover entries sharing the same keyword in the registry. The scripts below use PowerShell to find and export those keys first, and then back up and delete them once the list has been reviewed.

Registry operations can break the system or the software on it. Only delete entries you are sure belong to the target software, and create a system restore point or another reliable backup before you start. The scripts preview first, then back up, and only then delete; skipping the confirmation step is not recommended.

1. Find the registry keys

The script below searches key names only and deletes nothing. It tries both the 64-bit and 32-bit registry views and saves the results as a TXT file and a CSV file.

Save it as Find-WincompressRegistryKeys.ps1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
param(
    [string]$Keyword = 'wincompress',
    [string]$OutTxt = ".\wincompress_registry_keys.txt",
    [string]$OutCsv = ".\wincompress_registry_keys.csv"
)

$IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
    [Security.Principal.WindowsBuiltInRole]::Administrator
)
if (-not $IsAdmin) { Write-Warning "Run PowerShell as Administrator to reduce missed keys." }

$Is64OS = [Environment]::Is64BitOperatingSystem
$roots = @('HKLM','HKCU','HKCR','HKU','HKCC')
$results = New-Object System.Collections.Generic.List[string]

function Search-Keys {
    param([string]$Root, [string]$Keyword, [string]$RegView)
    $args = @('query', $Root, '/f', $Keyword, '/k', '/s')
    if ($RegView) { $args += "/reg:$RegView" }
    $out = & reg.exe @args 2>$null
    if ($LASTEXITCODE -eq 0 -and $out) {
        foreach ($line in $out) {
            if ($line -match '^HKEY_') { $results.Add($line.Trim()) }
        }
    }
}

foreach ($root in $roots) {
    Search-Keys -Root $root -Keyword $Keyword -RegView '64'
    if ($Is64OS) { Search-Keys -Root $root -Keyword $Keyword -RegView '32' }
}

$uniq = $results | Sort-Object -Unique
if ($uniq.Count -eq 0) {
    Write-Host "No registry key name contains '$Keyword'."
} else {
    $uniq | Set-Content -Path $OutTxt -Encoding UTF8
    $uniq | ForEach-Object { [pscustomobject]@{ RegistryKey = $_ } } |
        Export-Csv -NoTypeInformation -Encoding UTF8 -Path $OutCsv
    Write-Host "Found $($uniq.Count) keys."
    Write-Host "Output files:`n$OutTxt`n$OutCsv"
}

2. Back up and delete

Once you have confirmed that every entry in the list can be deleted, save the script below as Remove-WincompressRegistryKeys.ps1. It exports .reg backups by default and supports -WhatIf for a dry run.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
[CmdletBinding(SupportsShouldProcess=$true)]
param(
    [Parameter(Mandatory=$true)]
    [string]$InputList,
    [string]$BackupDir = ".\Backup_wincompress",
    [switch]$NoBackup
)

if (-not (Test-Path -LiteralPath $InputList)) { throw "List not found: $InputList" }
if (-not $NoBackup) { New-Item -ItemType Directory -Force -Path $BackupDir | Out-Null }

$Is64OS = [Environment]::Is64BitOperatingSystem
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
$log = ".\remove_wincompress_$stamp.log"
"Start: $(Get-Date)" | Out-File -FilePath $log -Encoding UTF8

$keys = Get-Content -LiteralPath $InputList |
    Where-Object { $_ -and ($_ -match '^HKEY_') } |
    ForEach-Object { $_.Trim() } |
    Sort-Object -Unique

function SafeName([string]$path) { return ($path -replace '[\\/:*?""<>|]','_') }

$success = 0
$failed = 0
foreach ($key in $keys) {
    $base = Join-Path $BackupDir (SafeName $key)

    if (-not $NoBackup) {
        & reg.exe export "$key" "$base.x64.reg" /y /reg:64 2>$null | Out-Null
        if ($LASTEXITCODE -ne 0) { "Backup FAIL (x64): $key" | Tee-Object -FilePath $log -Append | Out-Null }
        if ($Is64OS) {
            & reg.exe export "$key" "$base.x86.reg" /y /reg:32 2>$null | Out-Null
            if ($LASTEXITCODE -ne 0) { "Backup FAIL (x86): $key" | Tee-Object -FilePath $log -Append | Out-Null }
        }
    }

    if ($PSCmdlet.ShouldProcess("$key [x64]", 'Delete')) {
        & reg.exe delete "$key" /f /reg:64 2>$null | Out-Null
        if ($LASTEXITCODE -eq 0) { $success++; "Delete OK (x64): $key" | Tee-Object -FilePath $log -Append | Out-Null }
        else { $failed++; "Delete FAIL (x64): $key" | Tee-Object -FilePath $log -Append | Out-Null }
    }
    if ($Is64OS -and $PSCmdlet.ShouldProcess("$key [x86]", 'Delete')) {
        & reg.exe delete "$key" /f /reg:32 2>$null | Out-Null
        if ($LASTEXITCODE -eq 0) { $success++; "Delete OK (x86): $key" | Tee-Object -FilePath $log -Append | Out-Null }
        else { $failed++; "Delete FAIL (x86): $key" | Tee-Object -FilePath $log -Append | Out-Null }
    }
}

"End: $(Get-Date)" | Out-File -FilePath $log -Append -Encoding UTF8
"Summary: success=$success, failed=$failed, see log: $log" | Write-Host

3. How to use it

  1. Open PowerShell as administrator and allow scripts to run for this session only:

    1
    
    Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
    
  2. Run the search script and write the results to the desktop:

    1
    2
    3
    
    .\Find-WincompressRegistryKeys.ps1 -Keyword 'wincompress' `
      -OutTxt "$env:USERPROFILE\Desktop\wincompress_keys.txt" `
      -OutCsv "$env:USERPROFILE\Desktop\wincompress_keys.csv"
    
  3. Open the TXT or CSV file and confirm that every key belongs to the software you are removing.

  4. Do a dry run first; nothing is actually deleted:

    1
    2
    
    .\Remove-WincompressRegistryKeys.ps1 `
      -InputList "$env:USERPROFILE\Desktop\wincompress_keys.txt" -WhatIf
    
  5. Once you are sure, run the deletion. By default the backups from both registry views are saved to the given directory:

    1
    2
    3
    
    .\Remove-WincompressRegistryKeys.ps1 `
      -InputList "$env:USERPROFILE\Desktop\wincompress_keys.txt" `
      -BackupDir "$env:USERPROFILE\Desktop\Backup_wincompress"
    

Save the script files as ASCII or UTF-8 without a BOM for compatibility with older Windows PowerShell. PowerShell 7 can normally read UTF-8 files directly.

4. When SYSTEM privileges are needed

If administrator rights are still not enough, you can use Task Scheduler, which ships with Windows, to run the deletion script as SYSTEM. Put the wrapper script below in the same directory as Remove-WincompressRegistryKeys.ps1 and save it as Invoke-RemoveWincompressAsSystem.ps1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[CmdletBinding()]
param(
    [Parameter(Mandatory=$true)] [string]$InputList,
    [string]$BackupDir = "$env:USERPROFILE\Desktop\Backup_wincompress",
    [switch]$NoBackup
)

function Q([string]$s) { '"' + $s.Replace('"','""') + '"' }

$removeScript = Join-Path $PSScriptRoot 'Remove-WincompressRegistryKeys.ps1'
if (-not (Test-Path $removeScript)) { throw "Missing removal script." }
if (-not (Test-Path $InputList)) { throw "Input list not found." }
if (-not (Test-Path $BackupDir)) { New-Item -ItemType Directory -Force $BackupDir | Out-Null }

$ps = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
$tokens = @('-NoProfile','-ExecutionPolicy','Bypass','-File',(Resolve-Path $removeScript).Path,
    '-InputList',(Resolve-Path $InputList).Path,'-BackupDir',(Resolve-Path $BackupDir).Path)
if ($NoBackup) { $tokens += '-NoBackup' }
$argString = ($tokens | ForEach-Object { if ($_ -like '-*') { $_ } else { Q $_ } }) -join ' '

$service = New-Object -ComObject 'Schedule.Service'
$service.Connect()
$root = $service.GetFolder('\')
$taskDefinition = $service.NewTask(0)
$taskDefinition.Principal.UserId = 'SYSTEM'
$taskDefinition.Principal.LogonType = 5
$taskDefinition.Principal.RunLevel = 1
$taskDefinition.Settings.Hidden = $true
$taskDefinition.Settings.AllowDemandStart = $true

$action = $taskDefinition.Actions.Create(0)
$action.Path = $ps
$action.Arguments = $argString
$action.WorkingDirectory = Split-Path -Parent (Resolve-Path $removeScript).Path

$name = 'Temp_RemoveWincompress_' + [Guid]::NewGuid().ToString('N')
$null = $root.RegisterTaskDefinition("\$name", $taskDefinition, 6, $null, $null, 5, $null)
$task = $root.GetTask("\$name")
$running = $task.Run($null)

$deadline = (Get-Date).AddMinutes(30)
while ((Get-Date) -lt $deadline) {
    try { $state = $running.State } catch { break }
    if ($state -eq 4 -or $state -eq 2 -or $state -eq 3) { Start-Sleep -Seconds 2 } else { break }
}
try { $root.DeleteTask("\$name", 0) } catch {}

Running the wrapper as administrator is enough. If the key still cannot be deleted, it may be owned by TrustedInstaller; do not force-delete system components, confirm where the key comes from first and keep a full backup. You can also run as SYSTEM with tools such as Sysinternals PsExec, but with third-party tools you have to verify the source and the risk yourself.

5. Restoring a backup

To restore a registry key, double-click the corresponding .reg file, or run the following in an administrator PowerShell:

1
reg.exe import "path\to\backup.reg"