SCRIPT LIBRARY · POWERSHELL
Uninstall a Problem Windows Update by KB Number with DISM
Find the servicing packages behind a KB number, including cumulative updates that hide it, remove them with DISM, keep a transcript, and optionally hide the update so it doesn't come straight back.
- What it does
- Maps each KB number to its installed servicing packages with Get-WindowsPackage (checking package details for cumulative updates, whose names don't include the KB), removes them with Remove-WindowsPackage, writes a transcript of the whole run, and can hide the update in Windows Update and restart when it's done.
- Requires
- Windows PowerShell 5.1 or PowerShell 7+
- The Dism module (built into Windows)
- Permissions
- Local administrator, in an elevated PowerShell.
- Runs on
- Windows 10/11, Windows Server 2016+
- Tested
- Parse-checked, then run in PowerShell 7.4 with mocked Get-WindowsPackage, Remove-WindowsPackage and Get-HotFix, covering a KB in the package name, a KB found only in a rollup package's details, a failed removal, a KB listed by Get-HotFix with no package, a KB that isn't installed, and -WhatIf. The real DISM calls and the Windows Update hide step weren't run
Part 10 of the thread Windows Update, untangled
Patch Tuesday, Wednesday morning: one update has broken printing, or a VPN client, or a line-of-business app, and you need it off a handful of machines before lunch. The old answer was wusa /uninstall /kb:..., and on current versions of Windows it mostly doesn't work any more. Cumulative updates now ship combined with the servicing stack update, and Microsoft's own advice is to remove the cumulative part with DISM.
That's awkward by hand. dism /online /get-packages gives you a long list of names like Package_for_RollupFix~31bf3856ad364e35~amd64~~22621.4317.1.12, and the KB number you're after isn't in any of them. This started as a one-off script for exactly one bad update, with the KB number baked in. This version takes any KB, works out which packages belong to it, and removes them.
Every run leaves a transcript, -WhatIf shows exactly which packages would go, and -Hide asks Windows Update not to offer it again. The examples use KB5000000 as a stand-in; put in the one that's hurting you.
<#
.SYNOPSIS
Uninstalls a Windows update by KB number with DISM, logging everything to a transcript.
.DESCRIPTION
Looks up the installed servicing packages with Get-WindowsPackage and finds the ones that belong
to each KB. Older updates carry the KB in the package name. Cumulative updates don't (they show up
as Package_for_RollupFix~...), so for those it checks each package's description and support link
for the KB number instead. Each match is removed with Remove-WindowsPackage -NoRestart.
Every run writes a transcript, including -WhatIf runs, so there's a record of what was found.
With -Hide it also asks Windows Update to hide that KB so it isn't simply reinstalled tonight.
With -Restart it restarts the machine if a removal needs one. Returns one object per package.
.PARAMETER KB
One or more KB numbers, with or without the KB prefix (KB5000000 or 5000000).
.PARAMETER LogFolder
Where the transcript goes. Default: %ProgramData%\UpdateRemoval.
.PARAMETER Hide
After removal, hide the update in Windows Update so it isn't offered again. Often only works after
the restart, once Windows Update sees the update as not installed. Run the script again then; a KB
that's already gone is skipped straight to the hide step.
.PARAMETER Restart
Restart the computer at the end if any removal asked for it.
.EXAMPLE
.\Remove-WindowsUpdatePackage.ps1 -KB KB5000000 -WhatIf
.EXAMPLE
.\Remove-WindowsUpdatePackage.ps1 -KB 5000000 -Hide -Restart
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('HotFixID')]
[ValidatePattern('^(KB)?\d{6,8}$')]
[string[]]$KB,
[string]$LogFolder = (Join-Path $env:ProgramData 'UpdateRemoval'),
[switch]$Hide,
[switch]$Restart
)
begin {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
if (-not ([Security.Principal.WindowsPrincipal]$identity).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Removing updates needs an elevated PowerShell. Right-click, Run as administrator, and try again.'
}
if (-not (Test-Path -LiteralPath $LogFolder)) { $null = New-Item -ItemType Directory -Path $LogFolder -Force -WhatIf:$false -Confirm:$false }
$logFile = Join-Path $LogFolder ('Remove-Update-{0}-{1:yyyyMMdd-HHmmss}.log' -f $env:COMPUTERNAME, (Get-Date))
Start-Transcript -Path $logFile -WhatIf:$false -Confirm:$false | Out-Null
Write-Verbose 'Reading installed packages. This takes a minute on a machine with a long update history.'
try { $installed = @(Get-WindowsPackage -Online -ErrorAction Stop | Where-Object { $_.PackageState -in 'Installed', 'InstallPending' }) }
catch { Stop-Transcript -WhatIf:$false -Confirm:$false | Out-Null; throw "Couldn't read the installed packages: $($_.Exception.Message)" }
$rollupDetail = $null # filled in on first need; reading package details is slow
$numbers = [System.Collections.Generic.List[string]]::new()
$restartNeeded = $false
}
process {
foreach ($item in $KB) {
$num = $item -replace '^KB', ''
if ($numbers.Contains($num)) { continue }
$numbers.Add($num)
$found = @($installed | Where-Object { $_.PackageName -match "KB$num\b" })
if (-not $found) {
if ($null -eq $rollupDetail) {
$rollupDetail = @($installed | Where-Object { $_.PackageName -like 'Package_for_RollupFix*' -or $_.PackageName -like 'Package_for_DotNetRollup*' } |
ForEach-Object { Get-WindowsPackage -Online -PackageName $_.PackageName -ErrorAction SilentlyContinue })
}
$found = @($rollupDetail | Where-Object { "$($_.Description) $($_.SupportInformation) $($_.InstallPackageName)" -match "(KB|kbid=)$num\b" })
}
if (-not $found) {
$hotfix = Get-HotFix -Id "KB$num" -ErrorAction SilentlyContinue
[pscustomobject]@{
KB = "KB$num"
PackageName = $null
Action = if ($hotfix) { 'NoRemovablePackage' } else { 'NotInstalled' }
RestartNeeded = $false
Detail = if ($hotfix) { 'Listed by Get-HotFix but no matching package. Servicing stack updates, for one, can''t be removed.' } else { $null }
}
continue
}
foreach ($pkg in $found) {
$row = [pscustomobject]@{ KB = "KB$num"; PackageName = $pkg.PackageName; Action = $null; RestartNeeded = $false; Detail = $null }
if (-not $PSCmdlet.ShouldProcess("$($pkg.PackageName) (KB$num)", 'Remove Windows package')) {
$row.Action = 'WhatIf'
$row
continue
}
try {
Write-Verbose "Removing $($pkg.PackageName). DISM gives no progress here; 5-20 minutes is normal."
$removal = Remove-WindowsPackage -Online -PackageName $pkg.PackageName -NoRestart -ErrorAction Stop
$row.Action = 'Removed'
$row.RestartNeeded = [bool]$removal.RestartNeeded
if ($row.RestartNeeded) { $restartNeeded = $true }
}
catch {
$row.Action = 'Failed'
$row.Detail = $_.Exception.Message
Write-Warning "KB$num / $($pkg.PackageName): $($_.Exception.Message)"
}
$row
}
}
}
end {
try {
if ($Hide -and $numbers.Count) {
try {
Write-Verbose 'Searching Windows Update for the removed KBs so they can be hidden. This does an online scan.'
$searcher = (New-Object -ComObject Microsoft.Update.Session).CreateUpdateSearcher()
$offered = $searcher.Search("IsInstalled=0 and IsHidden=0 and Type='Software'").Updates
$hidden = @()
foreach ($update in $offered) {
$match = @($update.KBArticleIDs) | Where-Object { $numbers -contains $_ } | Select-Object -First 1
if ($match -and $PSCmdlet.ShouldProcess($update.Title, 'Hide in Windows Update')) {
$update.IsHidden = $true
$hidden += $match
[pscustomobject]@{ KB = "KB$match"; PackageName = $null; Action = 'Hidden'; RestartNeeded = $false; Detail = $update.Title }
}
}
foreach ($n in $numbers | Where-Object { $hidden -notcontains $_ }) {
Write-Warning "KB$n isn't being offered by Windows Update yet, so it couldn't be hidden. Run again with -Hide after the restart."
}
}
catch {
Write-Warning "Couldn't hide updates: $($_.Exception.Message)"
}
}
if ($restartNeeded) {
if ($Restart -and $PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Restart to finish removing updates')) {
Write-Verbose 'Restarting in 10 seconds.'
Start-Sleep -Seconds 10
Restart-Computer -Force -Confirm:$false
}
else {
Write-Warning 'A restart is needed to finish the removal.'
}
}
}
finally {
Stop-Transcript -WhatIf:$false -Confirm:$false | Out-Null
Write-Verbose "Log: $logFile"
}
}
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-KB | string[] | — | One or more KB numbers, with or without the KB prefix. Required. Takes pipeline input, including HotFixID from Get-HotFix. |
-LogFolder | string | %ProgramData%\UpdateRemoval | Where the transcript is written. One file per run, named after the computer and time. |
-Hide | switch | — | Hide the update in Windows Update afterwards so it isn't reinstalled. Often needs a second run after the restart. |
-Restart | switch | — | Restart at the end if any removal asked for it. Otherwise you get a warning and restart when it suits you. |
Run it
See which packages belong to the KB, without removing anything.
.\Remove-WindowsUpdatePackage.ps1 -KB KB5000000 -WhatIfRemove it, no prompt, and restart if needed.
.\Remove-WindowsUpdatePackage.ps1 -KB 5000000 -Restart -Confirm:$falseAfter the restart, run it again to hide the update from Windows Update.
.\Remove-WindowsUpdatePackage.ps1 -KB 5000000 -Hide -Confirm:$falseThe same on a few remote machines over PowerShell remoting, without copying the file anywhere.
$code = Get-Content .\Remove-WindowsUpdatePackage.ps1 -Raw; Invoke-Command -ComputerName PC-0142, PC-0187 -ScriptBlock { & ([scriptblock]::Create($using:code)) -KB KB5000000 -Confirm:$false }What you'll see
KB PackageName Action RestartNeeded Detail
-- ----------- ------ ------------- ------
KB5000000 Package_for_RollupFix~31bf3856ad364e35~amd64~~22621.4317.1.12 Removed True
KB5000123 NotInstalled False
WARNING: A restart is needed to finish the removal.
How it works
- Check elevation and start logging. DISM needs admin rights, so it stops early with a clear message if it doesn't have them. Then it starts a transcript in
-LogFolder, even for a-WhatIfrun, so there's always a record of what was found. - Read the installed packages once.
Get-WindowsPackage -Onlinelists every servicing package; only ones in the Installed or InstallPending state are kept. - Match each KB. First the easy way: a package name containing
KB5000000. If nothing matches, it reads the details of eachPackage_for_RollupFixandPackage_for_DotNetRolluppackage and checks the description and support link for the KB number. That's slower, so it only happens when it's needed, and only once per run. - Remove, one package at a time. Each match goes through
ShouldProcessand thenRemove-WindowsPackage -NoRestart. A failure is recorded against that package and the rest carry on. - Explain the misses. A KB with no package gets checked with
Get-HotFix, so you can tell "not installed" from "installed but not removable". - Hide and restart, if asked.
-Hideuses the Windows Update Agent API to find the KB among available updates and marks it hidden.-Restartrestarts only if a removal said it needs one.
If updates are going wrong often enough that you need this regularly, it's worth reading how to roll updates out in rings with a way back. And if you're not sure whether a machine listens to Windows Update, WSUS or Intune (which decides where to block the update), check where it gets its updates.
Take it further
- Leave a marker for your inventory. Write a registry value or a file with the KB and date after a successful removal, and your inventory tool can report which machines have been rolled back.
- Target by symptom. Pipe
Get-HotFix | Where-Object InstalledOn -gt (Get-Date).AddDays(-3)into the script to remove whatever landed in the last three days, with-WhatIffirst. - Package it. It runs fine as SYSTEM from ConfigMgr, Intune or any other deployment tool. Deploy it with the KB as an argument, and the transcripts in ProgramData tell you what happened on each machine.
Things that'll trip you up
- Servicing stack updates can't be removed. The servicing stack part of a combined update is permanent by design. If Get-HotFix lists a KB but there's no matching package, you'll see NoRemovablePackage, and that's usually why.
- It will come back. A removed update is just a missing update as far as Windows Update is concerned, and it'll reinstall at the next scan. Hide it with -Hide, pause updates, or, if the machine gets its updates from WSUS, Intune or ConfigMgr, decline or pause it there instead. Hiding on the client does nothing about a WSUS approval.
- Removal is slow and quiet. Remove-WindowsPackage shows no progress for a cumulative update, and 10 to 20 minutes is normal. The restart afterwards can take a while too, with "Working on updates" on screen. Don't pull the plug.
- A newer update may already cover the problem. If a later cumulative update is installed, removing an older one might not be possible or might not help. Check what's actually current with Get-HotFix before you start.