Skip to content

Instantly share code, notes, and snippets.

@Yunuuuu
Last active July 18, 2026 12:50
Show Gist options
  • Select an option

  • Save Yunuuuu/167cea034ec1ff03c839c745db66c522 to your computer and use it in GitHub Desktop.

Select an option

Save Yunuuuu/167cea034ec1ff03c839c745db66c522 to your computer and use it in GitHub Desktop.
Interactive PowerShell function for finding and removing stale Windows uninstall registry entries.
<#
.SYNOPSIS
Finds and removes Windows uninstall registry entries interactively.
.DESCRIPTION
Scans user and machine uninstall registry locations, detects stale MSI
entries, and lets you select entries before deletion.
Selection:
- Press Enter to select detected stale MSI entries.
- Enter 0 to select all matching entries.
- Enter one or more indexes, such as 1 or 1,3.
- Enter q to cancel.
- At the deletion prompt, press Enter or enter y to confirm.
This function only removes uninstall registry entries. It does not remove
application files, settings, services, scheduled tasks, or other leftovers.
Machine-level entries require an elevated PowerShell session.
.PARAMETER Name
The application display name to search for. Partial matching is used by default.
.PARAMETER Version
Optionally limits results to an exact DisplayVersion value.
.PARAMETER Exact
Requires the display name to match exactly.
.EXAMPLE
Remove-UninstallEntry 'draw.io'
.EXAMPLE
Remove-UninstallEntry 'QuickLook' -Exact
.EXAMPLE
Remove-UninstallEntry 'draw.io' -Version '26.1.1.0'
#>
function Remove-UninstallEntry {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0)]
[ValidateNotNullOrEmpty()]
[string] $Name,
[string] $Version,
[switch] $Exact
)
$locations = @(
@{
Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall'
Scope = 'User'
View = '64-bit'
}
@{
Path = 'HKCU:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
Scope = 'User'
View = '32-bit'
}
@{
Path = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall'
Scope = 'Machine'
View = '64-bit'
}
@{
Path = 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
Scope = 'Machine'
View = '32-bit'
}
)
$guidRegex = [regex]::new(
'\{[0-9A-Fa-f]{8}-(?:[0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}\}'
)
try {
$installer = New-Object -ComObject WindowsInstaller.Installer
}
catch {
Write-Warning 'Unable to access Windows Installer. MSI states will be unknown.'
$installer = $null
}
function Test-NameMatch {
param (
[AllowNull()]
[string] $DisplayName
)
if ([string]::IsNullOrWhiteSpace($DisplayName)) {
return $false
}
if ($Exact) {
return $DisplayName -eq $Name
}
return $DisplayName.IndexOf(
$Name,
[StringComparison]::OrdinalIgnoreCase
) -ge 0
}
function Get-MsiStatus {
param (
[AllowNull()]
[Nullable[int]] $State
)
if ($null -eq $State) {
return 'MSI state unknown'
}
switch ($State) {
-1 { return 'Stale MSI entry' }
0 { return 'Broken MSI installation' }
1 { return 'Advertised MSI product' }
2 { return 'MSI product absent' }
3 { return 'MSI installed locally' }
4 { return 'MSI running from source' }
5 { return 'MSI installed' }
default { return "MSI state $State" }
}
}
function Get-MatchingEntries {
$results = @()
foreach ($location in $locations) {
if (-not (Test-Path -LiteralPath $location.Path)) {
continue
}
foreach (
$key in Get-ChildItem `
-LiteralPath $location.Path `
-ErrorAction SilentlyContinue
) {
$entry = Get-ItemProperty `
-LiteralPath $key.PSPath `
-ErrorAction SilentlyContinue
if (-not (Test-NameMatch $entry.DisplayName)) {
continue
}
if (
-not [string]::IsNullOrWhiteSpace($Version) -and
[string]$entry.DisplayVersion -ne $Version
) {
continue
}
$uninstallString = [string]$entry.UninstallString
$quietString = [string]$entry.QuietUninstallString
$modifyPath = [string]$entry.ModifyPath
$searchText = @(
$key.PSChildName
$uninstallString
$quietString
$modifyPath
) -join ' '
$guidMatch = $guidRegex.Match($searchText)
$productCode = if ($guidMatch.Success) {
$guidMatch.Value
}
else {
$null
}
$isMsi = (
$entry.WindowsInstaller -eq 1 -or
$uninstallString -match '(?i)\bmsiexec(?:\.exe)?\b' -or
$quietString -match '(?i)\bmsiexec(?:\.exe)?\b'
)
$msiState = $null
if ($isMsi -and $productCode -and $null -ne $installer) {
try {
$msiState = [int]$installer.ProductState($productCode)
}
catch {
$msiState = $null
}
}
$isResidual = $isMsi -and $msiState -eq -1
$results += [pscustomobject]@{
DisplayName = [string]$entry.DisplayName
DisplayVersion = [string]$entry.DisplayVersion
Scope = $location.Scope
RegistryView = $location.View
Status = if ($isMsi) {
Get-MsiStatus $msiState
}
else {
'Non-MSI entry'
}
IsResidual = $isResidual
ProductCode = $productCode
InstallLocation = [string]$entry.InstallLocation
UninstallString = $uninstallString
RegistryPath = $key.Name
PSPath = $key.PSPath
}
}
}
return $results
}
$found = @(Get-MatchingEntries)
if ($found.Count -eq 0) {
Write-Warning "No uninstall entries matched '$Name'."
return
}
$items = @()
$index = 0
foreach (
$item in $found |
Sort-Object `
@{ Expression = 'IsResidual'; Descending = $true },
DisplayName,
DisplayVersion,
Scope,
RegistryView
) {
$index++
$items += [pscustomobject]@{
Default = if ($item.IsResidual) { '*' } else { '' }
Index = $index
DisplayName = $item.DisplayName
DisplayVersion = $item.DisplayVersion
Scope = $item.Scope
RegistryView = $item.RegistryView
Status = $item.Status
IsResidual = $item.IsResidual
ProductCode = $item.ProductCode
InstallLocation = $item.InstallLocation
UninstallString = $item.UninstallString
RegistryPath = $item.RegistryPath
PSPath = $item.PSPath
}
}
Write-Host
Write-Host '* = selected by default when Enter is pressed.'
Write-Host
$items |
Format-Table `
Default,
Index,
DisplayName,
DisplayVersion,
Scope,
RegistryView,
Status,
ProductCode `
-AutoSize `
-Wrap
Write-Host
$selection = (
Read-Host 'Enter indexes, press Enter for stale entries, 0 for all, or q to cancel'
).Trim()
if ($selection -match '^(?i:q|quit|cancel)$') {
Write-Host 'Cancelled.'
return
}
if ([string]::IsNullOrWhiteSpace($selection)) {
$selected = @(
$items |
Where-Object IsResidual
)
if ($selected.Count -eq 0) {
Write-Warning 'No stale MSI entries were detected.'
return
}
}
elseif ($selection -eq '0') {
$selected = @($items)
}
else {
$tokens = @(
$selection -split '[,\s]+' |
Where-Object {
-not [string]::IsNullOrWhiteSpace($_)
}
)
$invalidTokens = @(
$tokens |
Where-Object {
$_ -notmatch '^\d+$'
}
)
if ($invalidTokens.Count -gt 0) {
Write-Warning "Invalid selection: $($invalidTokens -join ', ')"
return
}
$numbers = @(
$tokens |
ForEach-Object { [int]$_ } |
Select-Object -Unique
)
if ($numbers -contains 0) {
Write-Warning 'Use 0 by itself.'
return
}
$invalidNumbers = @(
$numbers |
Where-Object {
$_ -lt 1 -or
$_ -gt $items.Count
}
)
if ($invalidNumbers.Count -gt 0) {
Write-Warning "Indexes out of range: $($invalidNumbers -join ', ')"
return
}
$selected = @(
$items |
Where-Object {
$_.Index -in $numbers
}
)
}
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
$isAdministrator = $principal.IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator
)
foreach ($item in $selected) {
Write-Host
Write-Host ('=' * 72)
Write-Host "Name: $($item.DisplayName)"
Write-Host "Version: $($item.DisplayVersion)"
Write-Host "Status: $($item.Status)"
Write-Host "Scope: $($item.Scope) / $($item.RegistryView)"
Write-Host "Product code: $($item.ProductCode)"
Write-Host "Install location: $($item.InstallLocation)"
Write-Host "Uninstall string: $($item.UninstallString)"
Write-Host "Registry path: $($item.RegistryPath)"
Write-Host
while ($true) {
$confirmation = (
Read-Host 'Delete this uninstall registry entry? [Y/n]'
).Trim()
if (
[string]::IsNullOrWhiteSpace($confirmation) -or
$confirmation -match '^(?i:y|yes)$'
) {
$shouldDelete = $true
break
}
if ($confirmation -match '^(?i:n|no|q|quit|cancel)$') {
$shouldDelete = $false
break
}
Write-Warning 'Enter y, n, or press Enter to delete.'
}
if (-not $shouldDelete) {
Write-Host 'Skipped.'
continue
}
$isMachineEntry = $item.RegistryPath.StartsWith(
'HKEY_LOCAL_MACHINE\',
[StringComparison]::OrdinalIgnoreCase
)
if ($isMachineEntry -and -not $isAdministrator) {
Write-Error 'Run PowerShell as administrator to remove this entry.'
continue
}
try {
Remove-Item `
-LiteralPath $item.PSPath `
-Recurse `
-Force `
-ErrorAction Stop
if (Test-Path -LiteralPath $item.PSPath) {
Write-Error 'The registry entry still exists.'
}
else {
Write-Host 'Removed.'
}
}
catch {
Write-Error "Removal failed: $($_.Exception.Message)"
}
}
$remaining = @(Get-MatchingEntries)
Write-Host
if ($remaining.Count -eq 0) {
Write-Host "No matching uninstall entries remain for '$Name'."
}
else {
Write-Host 'Remaining matching entries:'
$remaining |
Format-Table `
DisplayName,
DisplayVersion,
Scope,
RegistryView,
Status,
RegistryPath `
-AutoSize `
-Wrap
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment