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.
- 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
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.
<#
.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
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-OlderThanDays | int | 7 | Only delete temp files that haven't been written to in this many days. |
-AllProfiles | switch | — | Clean the temp folder in every local user profile, not just yours. |
-RunCleanMgr | switch | — | Also run Disk Cleanup silently with the handlers in -CleanMgrHandlers. |
-CleanMgrHandlers | string[] | Temporary Files, Thumbnail Cache, Update Cleanup, Delivery Optimization Files, Windows Error Reporting Files, Temporary Setup Files | Which Disk Cleanup categories to run. These are the key names under HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches. |
-SageSet | int | 4242 | The 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 -WhatIfThe full treatment, from an elevated prompt.
.\Invoke-DiskCleanup.ps1 -AllProfiles -RunCleanMgrOnly really old temp files, and just your own.
.\Invoke-DiskCleanup.ps1 -OlderThanDays 30Just 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 3650What you'll see
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
- Measure first. It records free space on the system drive before doing anything, so the last line can tell you what you got back.
- Build the list of temp folders.
C:\Windows\Tempalways, plus either your own temp folder or, with-AllProfiles,AppData\Local\Tempin every profile thatWin32_UserProfileknows about. - 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. - 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.
- Drive Disk Cleanup. With
-RunCleanMgr, it setsStateFlags4242(or whichever sageset you pick) to 2 on each chosen handler underVolumeCaches, which is exactly whatcleanmgr /sagesetdoes when you tick boxes in the UI, then runscleanmgr /sagerun:4242and waits.
Take it further
- Go deeper on the component store.
DISM /Online /Cleanup-Image /StartComponentCleanupcleans 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.