Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

A Safer Windows Disk Cleanup Script

Clear old temp files from the usual suspects and run Disk Cleanup unattended, with an age threshold and a -WhatIf that shows how much you'd get back.

AT A GLANCEInvoke-DiskCleanup.ps1
What it does
Deletes files older than a set number of days from the Windows temp folder and user temp folders, skipping anything in use, and can run the built-in Disk Cleanup (cleanmgr) silently with a chosen set of handlers. Reports per location and shows free space before and after.
Requires
  • Windows PowerShell 5.1 or PowerShell 7+
  • No modules
Permissions
None for your own temp folder. Local admin (elevated) for C:\Windows\Temp, -AllProfiles and -RunCleanMgr.
Runs on
Windows 10/11, Windows Server 2016+ (cleanmgr needs the Desktop Experience on Server)
Tested
Parse-checked and dry-run with mocked cmdlets in PowerShell 7.4

Part 1 of the thread Spring cleaning for Windows PCs

The C: drive is at 98%, the user can't save anything, and you've got about four minutes before the next call. You know where the junk usually is: temp folders, update leftovers, error reports nobody will ever read.

The first version of this script went after all of it with Remove-Item -Recurse -Force, emptied the Recycle Bin, hid its own window and beeped when it was done. It also deleted everything in the temp folders regardless of age, including files belonging to an installer that was running at that moment. Not ideal.

This version is more careful. It only removes temp files older than a threshold (7 days by default), skips anything that's locked instead of fighting it, and -WhatIf tells you how many megabytes you'd get back before anything is deleted. For the Windows-managed stuff like update cleanup and Delivery Optimization files, it drives the built-in Disk Cleanup rather than deleting those folders by hand. The Recycle Bin has its own script now.

Invoke-DiskCleanup.ps1Download
<#
.SYNOPSIS
    Clears old temp files from the usual places and can run the built-in Disk Cleanup
    (cleanmgr) unattended.
.DESCRIPTION
    Removes files older than -OlderThanDays from the Windows temp folder and user temp
    folders (yours, or every profile with -AllProfiles). Files that are locked are skipped
    and counted, not forced. With -RunCleanMgr it registers a set of Disk Cleanup handlers
    under a sageset number and runs cleanmgr /sagerun against it. Returns a summary per
    location, and -WhatIf shows how much would go without deleting anything.
.PARAMETER OlderThanDays
    Only delete files that haven't been written to in this many days. Default: 7.
.PARAMETER AllProfiles
    Clean the temp folder of every local profile, not just yours. Needs to run elevated.
.PARAMETER RunCleanMgr
    Also run Disk Cleanup silently with the handlers in -CleanMgrHandlers. Needs to run elevated.
.PARAMETER CleanMgrHandlers
    Disk Cleanup handler names (the key names under ...\Explorer\VolumeCaches).
.PARAMETER SageSet
    The sageset number to store the handler selection under. Default: 4242.
.EXAMPLE
    .\Invoke-DiskCleanup.ps1 -WhatIf
.EXAMPLE
    .\Invoke-DiskCleanup.ps1 -AllProfiles -OlderThanDays 14 -RunCleanMgr
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [ValidateRange(1, 3650)]
    [int]$OlderThanDays = 7,

    [switch]$AllProfiles,

    [switch]$RunCleanMgr,

    [string[]]$CleanMgrHandlers = @('Temporary Files', 'Thumbnail Cache', 'Update Cleanup', 'Delivery Optimization Files', 'Windows Error Reporting Files', 'Temporary Setup Files'),

    [ValidateRange(0, 9999)]
    [int]$SageSet = 4242
)

function Get-FreeGB {
    $disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$($env:SystemDrive)'"
    [math]::Round($disk.FreeSpace / 1GB, 2)
}

$cutoff = (Get-Date).AddDays(-$OlderThanDays)
$freeBefore = Get-FreeGB

$locations = @(Join-Path $env:SystemRoot 'Temp')
if ($AllProfiles) {
    $locations += Get-CimInstance -ClassName Win32_UserProfile -Filter 'Special = FALSE' |
        ForEach-Object { Join-Path $_.LocalPath 'AppData\Local\Temp' }
}
else {
    $locations += [System.IO.Path]::GetTempPath().TrimEnd('\')
}

foreach ($location in $locations | Select-Object -Unique) {
    if (-not (Test-Path -LiteralPath $location)) { continue }

    $old = @(Get-ChildItem -LiteralPath $location -File -Recurse -Force -ErrorAction SilentlyContinue |
        Where-Object { $_.LastWriteTime -lt $cutoff })
    $bytes = ($old | Measure-Object Length -Sum).Sum
    $summary = [pscustomobject]@{
        Location     = $location
        FilesFound   = $old.Count
        FilesRemoved = 0
        SkippedInUse = 0
        MBFreed      = 0
        MBFound      = [math]::Round($bytes / 1MB, 1)
    }

    if ($old.Count -and $PSCmdlet.ShouldProcess($location, "Delete $($old.Count) file(s), $($summary.MBFound) MB, older than $OlderThanDays days")) {
        $freed = 0L
        foreach ($file in $old) {
            try {
                Remove-Item -LiteralPath $file.FullName -Force -Confirm:$false -ErrorAction Stop
                $summary.FilesRemoved++
                $freed += $file.Length
            }
            catch {
                # Almost always "in use by another process". That's fine; it's somebody's live temp file.
                $summary.SkippedInUse++
            }
        }
        $summary.MBFreed = [math]::Round($freed / 1MB, 1)
    }
    $summary
}

if ($RunCleanMgr) {
    $cacheRoot = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches'
    $flagName = 'StateFlags{0:D4}' -f $SageSet

    if ($PSCmdlet.ShouldProcess("Disk Cleanup handlers: $($CleanMgrHandlers -join ', ')", "Run cleanmgr /sagerun:$SageSet")) {
        foreach ($handler in $CleanMgrHandlers) {
            $key = Join-Path $cacheRoot $handler
            if (Test-Path -LiteralPath $key) {
                # 2 = "checked" for this sageset. Other sagesets are left alone.
                Set-ItemProperty -LiteralPath $key -Name $flagName -Value 2 -Type DWord -Confirm:$false
            }
            else {
                Write-Warning "Disk Cleanup handler '$handler' doesn't exist on this version of Windows. Skipping it."
            }
        }
        Write-Verbose "Running cleanmgr /sagerun:$SageSet. This can take a while."
        Start-Process -FilePath (Join-Path $env:SystemRoot 'System32\cleanmgr.exe') -ArgumentList "/sagerun:$SageSet" -Wait -WindowStyle Hidden
    }
}

Write-Host ("Free space on {0} {1} GB before, {2} GB now." -f $env:SystemDrive, $freeBefore, (Get-FreeGB))

Parameters

ParameterTypeDefaultWhat it's for
-OlderThanDaysint7Only delete temp files that haven't been written to in this many days.
-AllProfilesswitch—Clean the temp folder in every local user profile, not just yours.
-RunCleanMgrswitch—Also run Disk Cleanup silently with the handlers in -CleanMgrHandlers.
-CleanMgrHandlersstring[]Temporary Files, Thumbnail Cache, Update Cleanup, Delivery Optimization Files, Windows Error Reporting Files, Temporary Setup FilesWhich Disk Cleanup categories to run. These are the key names under HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches.
-SageSetint4242The profile number the handler selection is saved under. Pick one nothing else uses.

Run it

How much would a cleanup free on this machine?

.\Invoke-DiskCleanup.ps1 -AllProfiles -WhatIf

The full treatment, from an elevated prompt.

.\Invoke-DiskCleanup.ps1 -AllProfiles -RunCleanMgr

Only really old temp files, and just your own.

.\Invoke-DiskCleanup.ps1 -OlderThanDays 30

Just Disk Cleanup's update leftovers. The huge age limit keeps the temp-file step from doing anything.

.\Invoke-DiskCleanup.ps1 -RunCleanMgr -CleanMgrHandlers 'Update Cleanup' -OlderThanDays 3650

What you'll see

Example outputvalues are illustrative
Location                              FilesFound FilesRemoved SkippedInUse MBFreed MBFound
--------                              ---------- ------------ ------------ ------- -------
C:\Windows\Temp                              412          398           14  1843.2  1851.0
C:\Users\adele.v\AppData\Local\Temp         2871         2866            5   922.7   923.1
C:\Users\megan.b\AppData\Local\Temp          119          119            0    64.3    64.3

Free space on C: 3.12 GB before, 8.71 GB now.

How it works

  1. Measure first. It records free space on the system drive before doing anything, so the last line can tell you what you got back.
  2. Build the list of temp folders. C:\Windows\Temp always, plus either your own temp folder or, with -AllProfiles, AppData\Local\Temp in every profile that Win32_UserProfile knows about.
  3. Find old files. In each folder, it collects files whose last write is older than the cutoff and adds up their size. That total is what you see under -WhatIf.
  4. Delete one file at a time. A locked file is counted and skipped; everything else goes. One bad file doesn't stop the folder, and one bad folder doesn't stop the run.
  5. Drive Disk Cleanup. With -RunCleanMgr, it sets StateFlags4242 (or whichever sageset you pick) to 2 on each chosen handler under VolumeCaches, which is exactly what cleanmgr /sageset does when you tick boxes in the UI, then runs cleanmgr /sagerun:4242 and waits.

Take it further

  • Go deeper on the component store. DISM /Online /Cleanup-Image /StartComponentCleanup cleans up superseded Windows components. It's slow, but on an old machine it can free several gigabytes.
  • Let Storage Sense handle the daily stuff. On Windows 10 and 11, Storage Sense can clear temp files on a schedule, and you can configure it with Intune. Keep the script for the "disk is full right now" moments.
  • Find out what's really using the space. If a cleanup barely moves the needle, it's usually something like a giant OST, a forgotten VM, or a Downloads folder full of installers. The OST cleanup and user profile scripts cover two of those.

Things that'll trip you up

  • Update Cleanup can take a long time. The Windows Update Cleanup handler can run for 20 minutes or more on a machine that's never had it, and cleanmgr gives no progress when it's run silently. Don't assume it's hung.
  • Handler names change between versions. Categories come and go with Windows releases. If a name in -CleanMgrHandlers doesn't exist on that machine, the script warns and carries on with the rest. Look under the VolumeCaches key to see what's available.
  • Locked files are normal. A running app's temp files are in use, and that's fine. The script counts them as SkippedInUse instead of throwing errors. If the same huge file is always locked, find out what's holding it.
  • Your own temp folder when running as SYSTEM. Run from a deployment tool without -AllProfiles and "your" temp folder is SYSTEM's, which is C:\Windows\Temp or C:\Windows\SystemTemp on newer builds. Use -AllProfiles to reach the users' folders.