Skip to content

Windows Component Cleanup (DISM) (Package)

This Tanium package makes the Windows component store smaller. The component store is the folder C:\Windows\WinSxS.

Each Windows update keeps the old versions of the files that it replaces. The package tells Windows to remove the old versions that Windows does not need. It uses this Microsoft command:

dism.exe /Online /Cleanup-Image /StartComponentCleanup

The package does not restart the endpoint.

Test status

The package was run through Tanium on an endpoint with ResetBase set to Yes. The run completed with exit code 0 and freed 5.05 GB. A run with ResetBase set to No was not tested. Deploy to one endpoint first and read the action log.

The ResetBase option

The ResetBase option frees more space. But after you use it, you cannot uninstall the updates that are installed now. Updates that you install later are not affected.

What the package does

  1. Makes sure that Windows does not wait for a restart. If Windows must restart to complete an update installation, the package stops. It uses exit code 2.
  2. Asks Windows how much space the cleanup can free. It writes the result to the action log.
  3. Starts the cleanup.
  4. Writes the free space before and after the cleanup to the action log.

The cleanup can take from a few minutes to more than one hour.

When to use ResetBase

Windows does a component cleanup automatically on a schedule. On an endpoint that Windows cleans regularly, the cleanup without ResetBase frees little space. Most of the space comes from ResetBase.

Setting Use it when
No (default) You want a safe cleanup. You can run it on many endpoints at a time.
Yes The current monthly updates are stable, and you do not plan to uninstall them. For example, wait one to two weeks after the monthly updates install.

Requirements

  • Windows 10 or Windows 11
  • Windows PowerShell 5.1
  • The Tanium Client

Create the package

  1. In the Tanium Console, go to Administration > Content > Packages.
  2. Click Create Package.
  3. Fill in the fields with the values in the table below.
  4. In the Files section, add the script file.
  5. In the Parameters section, add the parameter in the second table below.
  6. Click Save.
Field Value
Package Name Windows Component Cleanup (DISM)
Display Name Windows Component Cleanup (DISM)
Description Shrinks the Windows component store (C:\Windows\WinSxS) by removing old versions of files replaced by updates, using DISM /StartComponentCleanup. Reports how much can be freed first. Skips the computer if Windows is waiting for a restart. Optional ResetBase frees more, but updates installed today can no longer be uninstalled. Does not restart the computer. Windows only.
Content Set Your custom content set
Command cmd.exe /d /c powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -NoProfile -File Invoke-ComponentCleanup.ps1 "$1"
Command Timeout 120 minutes
Download Timeout 10 minutes
Ignore Action Lock Clear (not selected)
Launch this package command in a process group Selected
Files Invoke-ComponentCleanup.ps1

Parameter

Key Type Label Values Default
$1 Drop Down List Use ResetBase (installed updates can no longer be uninstalled) No, Yes No

Deploy the package

  1. In the Tanium Console, go to Interact.
  2. In the question bar, type this question:

    Get Computer Name and Disk Free Space from all machines with Is Windows equals True
    
  3. Press Enter.

  4. Select the endpoints that you want to clean.
  5. Click Deploy Action.
  6. In the Deployment Package field, type Windows Component Cleanup (DISM). Select the package.
  7. Set Use ResetBase to No or Yes. Read When to use ResetBase first.
  8. Click Show preview to continue.
  9. Make sure that the list shows the correct endpoints.
  10. Click Deploy Action.

Tip

The cleanup uses much processor and disk time. Deploy it outside of work hours when possible.

Exit codes

Code Meaning
0 The cleanup completed.
1 The cleanup failed. Read the action log.
2 Windows waits for a restart. The cleanup did not start. Restart the endpoint, then deploy again.

Read the results

The action log shows the DISM analysis, the cleanup result, and the free space before and after.

To read the action log from the Tanium Console:

  1. Go to Interact.
  2. Type this question. Replace <action ID> with the ID of your action.

    Get Tanium Action Log[<action ID>] from all machines with Computer Name equals ENDPOINT-01
    
  3. Press Enter.

Example log

[INFO] Free space on C: before: 132.89 GB
[INFO] Asking Windows how much can be cleaned up (this can take a few minutes).
    Actual Size of Component Store : 17.68 GB
    Backups and Disabled Features : 9.19 GB
    Component Store Cleanup Recommended : Yes
[INFO] Starting the cleanup with /ResetBase. Updates that are installed now can no longer be uninstalled afterwards.
    The operation completed successfully.
[INFO] Free space on C: after: 137.95 GB (change: 5.05 GB)
[INFO] Windows component cleanup finished with exit code 0.

In the analysis, Backups and Disabled Features shows the old update files. Component Store Cleanup Recommended shows if Windows thinks that a cleanup is necessary.

Troubleshooting

Log message Cause Action
Windows is waiting for a restart An update installation is not complete. Restart the endpoint. Then deploy again.
The cleanup failed with DISM code ... DISM could not complete the cleanup. The full details are in C:\Windows\Logs\DISM\dism.log on the endpoint.
Windows asks for a restart to complete it The cleanup completed, but Windows must restart to complete some work. Restart the endpoint at a convenient time.
The command timeout occurs The cleanup takes more than 120 minutes. Increase the command timeout. Then deploy again.

Script

Invoke-ComponentCleanup.ps1

<#
.SYNOPSIS
    Tanium package script. Shrinks the Windows component store (C:\Windows\WinSxS) with DISM.

.DESCRIPTION
    Every Windows update leaves the older versions of the files it replaced in the component
    store. This script asks Windows to remove the versions that are no longer needed, using
    the built-in, Microsoft-supported command:

        dism.exe /Online /Cleanup-Image /StartComponentCleanup

    It first asks Windows how much can be freed and writes that to the action log.
    It does not restart the computer.

.PARAMETER ResetBase
    $1  Yes or No. Default No.
    Yes adds /ResetBase. That frees more space, but the updates that are installed right now
    can no longer be uninstalled afterwards. Updates installed later are not affected.

.NOTES
    Tanium command line:
      cmd.exe /d /c powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -NoProfile -File Invoke-ComponentCleanup.ps1 "$1"

    Exit codes:
      0 = cleanup finished
      1 = cleanup failed (see the action log and C:\Windows\Logs\DISM\dism.log)
      2 = skipped because Windows is waiting for a restart. Restart, then run again.
#>
[CmdletBinding()]
param(
    [Parameter(Position = 0)][string]$ResetBase = 'No'
)

$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($ResetBase)) { $ResetBase = 'No' }

# If the Tanium Client started the 32-bit PowerShell on 64-bit Windows, start again in the
# 64-bit one. DISM has to be the 64-bit version to work on 64-bit Windows.
if ([Environment]::Is64BitOperatingSystem -and -not [Environment]::Is64BitProcess) {
    $powershell64 = Join-Path $env:windir 'Sysnative\WindowsPowerShell\v1.0\powershell.exe'
    if (Test-Path -LiteralPath $powershell64) {
        & $powershell64 -ExecutionPolicy Bypass -NoProfile -NonInteractive -File $PSCommandPath $ResetBase
        exit $LASTEXITCODE
    }
}

$ExitCode = 0

function Write-Log {
    param([string]$Message, [string]$Level = 'INFO')
    Write-Host ('{0} [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message)
}

function Get-FreeBytes {
    return (New-Object System.IO.DriveInfo($env:SystemDrive)).AvailableFreeSpace
}

function Format-Size {
    param([double]$Bytes)
    if ($Bytes -ge 1GB) { return '{0:N2} GB' -f ($Bytes / 1GB) }
    if ($Bytes -ge 1MB) { return '{0:N1} MB' -f ($Bytes / 1MB) }
    if ($Bytes -ge 1KB) { return '{0:N0} KB' -f ($Bytes / 1KB) }
    return '{0:N0} bytes' -f $Bytes
}

function Test-RestartPending {
    # True when Windows is waiting for a restart to finish installing updates.
    try {
        $machine = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64)
        foreach ($keyPath in @(
                'SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending',
                'SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired')) {
            $key = $machine.OpenSubKey($keyPath)
            if ($key) { $key.Close(); return $true }
        }
    }
    catch { }
    return $false
}

function Write-DismOutput {
    # Copies what DISM said into the action log, without the progress bar lines.
    param($Lines)
    foreach ($line in @($Lines)) {
        $text = ([string]$line).Trim()
        if ($text.Length -eq 0) { continue }
        if ($text -match '^\[[=\s\d\.%]*\]$') { continue }
        Write-Host ('    ' + $text)
    }
}

try {
    Write-Log 'Windows component cleanup started.'

    $text = $ResetBase.Trim().ToLowerInvariant()
    if     (@('yes', 'y', 'true', '1', 'on') -contains $text)  { $useResetBase = $true }
    elseif (@('no', 'n', 'false', '0', 'off') -contains $text) { $useResetBase = $false }
    else   { throw "ResetBase must be Yes or No. Received: '$ResetBase'" }

    if (Test-RestartPending) {
        $ExitCode = 2
        Write-Log 'Windows is waiting for a restart to finish installing updates. The cleanup cannot run until then. Restart this computer, then run the package again.' 'WARN'
    }
    else {
        $dism       = Join-Path $env:windir 'System32\dism.exe'
        $freeBefore = Get-FreeBytes
        Write-Log ("Free space on {0} before: {1}" -f $env:SystemDrive, (Format-Size $freeBefore))

        Write-Log 'Asking Windows how much can be cleaned up (this can take a few minutes).'
        $analysis = & $dism /Online /Cleanup-Image /AnalyzeComponentStore
        Write-DismOutput -Lines $analysis
        if ($LASTEXITCODE -ne 0) {
            Write-Log ("The check ended with DISM code {0}. The cleanup is still attempted." -f $LASTEXITCODE) 'WARN'
        }

        if ($useResetBase) {
            Write-Log 'Starting the cleanup with /ResetBase. Updates that are installed now can no longer be uninstalled afterwards.'
            $output = & $dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase
        }
        else {
            Write-Log 'Starting the cleanup. This can take from a few minutes to more than an hour.'
            $output = & $dism /Online /Cleanup-Image /StartComponentCleanup
        }
        $dismCode = $LASTEXITCODE
        Write-DismOutput -Lines $output

        $freeAfter = Get-FreeBytes
        Write-Log ("Free space on {0} after: {1} (change: {2})" -f $env:SystemDrive, (Format-Size $freeAfter), (Format-Size ([math]::Max([long]0, $freeAfter - $freeBefore))))

        if ($dismCode -eq 0) {
            Write-Log 'The cleanup finished.'
        }
        elseif ($dismCode -eq 3010) {
            Write-Log 'The cleanup finished. Windows asks for a restart to complete it.'
        }
        else {
            $ExitCode = 1
            Write-Log ("The cleanup failed with DISM code {0}. Details are in C:\Windows\Logs\DISM\dism.log on this computer." -f $dismCode) 'ERROR'
        }
    }
}
catch {
    $ExitCode = 1
    Write-Log ("Windows component cleanup failed: {0}" -f $_.Exception.Message) 'ERROR'
}

Write-Log ("Windows component cleanup finished with exit code {0}." -f $ExitCode)
exit $ExitCode