Report and clear temporary files, the Windows Update download cache and, optionally, recycle bins on a device
Reclaim space on a full Windows system drive by clearing temporary files and caches, and report free space before and after. For MSPs and IT teams handling "disk full" tickets and low-disk alerts.
| Runs in | On the device (PowerShell, as SYSTEM) |
| Uses | None |
| Suggested approval | None, so only Engineers and Admins can run it. Don't offer it to end users |
| Category | Devices |
Test it with SuperIT before you rely on it
This is an example, not a ready-made action, and it hasn't been tested end to end. Before you use it, work through it with SuperIT: ask SuperIT to review the script against your environment and requirements, and adjust it together. Then run it on a test device, as in Test it. Device actions can't use Test, so every run is real.
Why use an action instead of asking SuperIT
SuperIT can see free space from the SuperIT RMM agent's readings and can find what's using it, but it can't clean up for you in a chat. Recursive deletes under C:\Windows and C:\Users are blocked by a locked command policy, and clearing the Windows Update cache means stopping the Windows Update service, which the default command policy denies. This action runs as SYSTEM, isn't checked against command policies, and deletes only old files in the temp and cache folders you chose; its authorization policy decides who can run it. Keep it for Engineers and Admins: it deletes files as SYSTEM, so it isn't a self-service action.
To find out how full a drive is or what's filling it, just ask SuperIT in chat.
When to use it
- "My C drive is full and I can't save anything."
- "Outlook says there's not enough disk space."
- A ticket investigation finds the system drive almost full. An engineer then runs the action on that device from a chat.
- "Clean up the temp files on SRV-RDS01 every week."
Use Clean up mode to fix the problem. Report mode shows what a cleanup would free before you run it.
What the action does
- Reads
expected_hostname,mode(Report or Clean up),older_than_days(1 to 365, default 7) andempty_recycle_bins(default off). - Stops without changing anything if
expected_hostnameisn't this device's computer name. - Records free space on the system drive.
- Measures what can be removed from:
- the Windows temp folder (
C:\Windows\Temp), files older thanolder_than_days; - every user's temp folder (
C:\Users\<user>\AppData\Local\Temp), files older thanolder_than_days; - the Windows Update download cache (
C:\Windows\SoftwareDistribution\Download); - every user's recycle bin, if
empty_recycle_binsis on.
- the Windows temp folder (
- In Report mode, stops there and prints the sizes.
- In Clean up mode, stops the Windows Update service, deletes those files, starts the service again if it was running, and clears the Delivery Optimization cache where Windows supports it.
- Prints free space before and after, and returns the figures as a result.
The script never follows links. Standard users can create junctions and symbolic links in their own temp folder, in C:\Windows\Temp and in their own recycle bin. In Windows PowerShell 5.1, Get-ChildItem -Recurse follows them, so a planted link could point a SYSTEM cleanup at system files or another user's profile. The script walks each folder itself and skips anything marked as a reparse point (a junction, symbolic link or mount point): it neither enters nor deletes it. Just before deleting each file, it checks again that neither the file nor any folder on its path, up to the drive root, has become a link. A user who can write to a temp folder could still try to swap a folder for a link in the moment between that check and the delete, which is another reason to keep the action for engineers.
Files that are in use are skipped silently.
What it doesn't do:
- It doesn't delete user documents, downloads, old user profiles or
Windows.old. - It doesn't run Disk Cleanup or DISM component cleanup. They can take longer than a device action's timeout.
- It doesn't clean drives other than the system drive.
Before you begin
- A Windows device with the SuperIT RMM agent, online. Actions run on Windows devices only; the macOS and Linux agents are in Beta and can't run actions. See Requirements.
- Your role. Creating an action in a chat needs the Admin role. Engineers and Admins can build it under Workflows → Actions.
- No credential. See Credentials.
- No authorization policy. With None, only Engineers and Admins can run it, without approval. Don't add an Anyone rule. See Authorization policies and approvals.
Device actions and end users
A device action runs as SYSTEM on the device chosen for the run, or on the action's default device or your team's designated device if none is chosen. SuperIT doesn't check that the device belongs to the person who asked. Don't let end users run this action.
Build it with SuperIT
Create an action called "Free up disk space".
Runtime: PowerShell, running on the device as SYSTEM (execution target: device). Timeout 5 minutes (the default).
Inputs: expected_hostname (string, required), mode (select: Report, Clean up; required; default Report), older_than_days (string, optional, default 7, 1-365), empty_recycle_bins (boolean, optional).
Stop before changing anything unless expected_hostname matches $env:COMPUTERNAME.
Measure, then in Clean up mode delete: files older than N days in C:\Windows\Temp and each user's AppData\Local\Temp; everything in C:\Windows\SoftwareDistribution\Download (stop wuauserv first, restart it if it was running); recycle bins under C:\$Recycle.Bin only if empty_recycle_bins is true. Clear the Delivery Optimization cache if the cmdlet exists.
Don't use Get-ChildItem -Recurse: walk folders by hand and skip any item with the ReparsePoint attribute, never entering or deleting it. Skip a user's temp folder if any folder on its path is a reparse point.
Skip locked files. Print size per location and system drive free space before and after. Windows PowerShell 5.1 compatible.
No authorization policy.
SuperIT confirms the brief, drafts the script, validates it and creates the action as a draft. Review the script, then publish it under Workflows → Actions.
Or build it yourself
Create the action under Workflows → Actions with the PowerShell runtime and these inputs, then set it to run on a device.
| Name | Type | Required | Example |
|---|---|---|---|
expected_hostname |
string | Yes | SRV-RDS01 |
mode |
select (Report, Clean up) |
Yes | Report |
older_than_days |
string | No | 7 |
empty_recycle_bins |
boolean | No | false |
On a device, the script runs in Windows PowerShell as SYSTEM with $ErrorActionPreference = "Stop" already set. Inputs arrive as strings in $ctx.inputs; booleans are "true" or "false". Invoke-DiskCleanup at the top lists the steps in order; each step is a small function below it, and the last line runs it. $result is set as $script:result, because a plain $result = inside a function stays local and never reaches the Result tab.
# Measures old temp files, the Windows Update download cache and (optionally) recycle bins on the
# system drive, deletes them in Clean up mode without following links, and prints free space before and after.
function Invoke-DiskCleanup {
param($ExpectedHostname, $Mode, $OlderThanDays, $EmptyRecycleBins)
Assert-TargetDevice -ExpectedHostname $ExpectedHostname
$options = Assert-Inputs -Mode $Mode -OlderThanDays $OlderThanDays -EmptyRecycleBins $EmptyRecycleBins
$cleanUp = $options.mode -eq 'Clean up'
$targets = @(Get-CleanupTarget -Days $options.days -IncludeRecycleBins $options.empty_bins)
$before = Get-SystemFreeGB
Write-Output '=== DISK CLEANUP ==='
Write-Output "Mode: $($options.mode). Free space on $($env:SystemDrive): $before GB"
$report = @()
foreach ($t in $targets) {
$files = @(Get-OldFile -Roots $t.Roots -OlderThanDays $t.Days)
$mb = [math]::Round((($files | Measure-Object Length -Sum).Sum) / 1MB)
$report += @{ location = $t.Name; files = $files.Count; size_mb = $mb }
Write-Output ("{0,-22} {1,8} MB in {2} files" -f $t.Name, $mb, $files.Count)
if ($cleanUp -and $files.Count -gt 0) { Remove-CleanupFile -Target $t -Files $files }
}
if ($cleanUp) { Clear-DeliveryOptimizationStore }
$after = Get-SystemFreeGB
if ($cleanUp) { Write-Output ("Free space after: {0} GB (freed {1} GB)" -f $after, [math]::Round($after - $before, 2)) }
# $result is read after the script ends, so set it at script scope, not in this function.
$script:result = @{ mode = $options.mode; free_gb_before = $before; free_gb_after = $after; locations = $report }
}
function Assert-TargetDevice {
param($ExpectedHostname)
$expected = ("$ExpectedHostname".Trim() -split '\.')[0]
if (-not $expected) { throw "expected_hostname is required: enter this device's computer name." }
if ($expected -ne $env:COMPUTERNAME) { throw "This run reached '$env:COMPUTERNAME', not '$expected'. Nothing was changed." }
}
function Assert-Inputs {
param($Mode, $OlderThanDays, $EmptyRecycleBins)
if ("$Mode" -notin @('Report', 'Clean up')) { throw "mode must be 'Report' or 'Clean up'." }
$days = 7
if ($null -ne $OlderThanDays) {
if ($OlderThanDays -notmatch '^\d{1,3}$' -or [int]$OlderThanDays -lt 1 -or [int]$OlderThanDays -gt 365) {
throw "older_than_days must be a whole number from 1 to 365."
}
$days = [int]$OlderThanDays
}
@{ mode = "$Mode"; days = $days; empty_bins = ($EmptyRecycleBins -eq 'true') }
}
function Get-CleanupTarget {
param([int]$Days, [bool]$IncludeRecycleBins)
@{ Name = 'Windows temp'; Roots = @("$env:SystemRoot\Temp"); Days = $Days }
@{ Name = 'User temp folders'; Roots = @(Get-UserTempRoot); Days = $Days }
@{ Name = 'Windows Update cache'; Roots = @("$env:SystemRoot\SoftwareDistribution\Download"); Days = 0; StopWindowsUpdate = $true }
if ($IncludeRecycleBins) { @{ Name = 'Recycle bins'; Roots = @("$env:SystemDrive\`$Recycle.Bin"); Days = 0 } }
}
function Get-UserTempRoot {
# Users own their profile folders, so check every folder on the path, not just the last one.
foreach ($userProfile in @(Get-ChildItem -LiteralPath "$env:SystemDrive\Users" -Directory -Force -ErrorAction SilentlyContinue)) {
if (Test-ReparsePoint -Item $userProfile) { continue }
$dir = $userProfile.FullName
$safe = $true
foreach ($part in 'AppData', 'Local', 'Temp') {
$dir = Join-Path $dir $part
$item = Get-Item -LiteralPath $dir -Force -ErrorAction SilentlyContinue
if (-not $item -or (Test-ReparsePoint -Item $item)) { $safe = $false; break }
}
if ($safe) { $dir }
}
}
function Get-SystemFreeGB {
$disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$env:SystemDrive'"
[math]::Round($disk.FreeSpace / 1GB, 2)
}
function Get-OldFile {
param([string[]]$Roots, [int]$OlderThanDays)
# Not Get-ChildItem -Recurse: in Windows PowerShell 5.1 it follows junctions and symbolic links,
# which a standard user can plant in a temp folder to point SYSTEM at files outside it.
$cutoff = (Get-Date).AddDays(-$OlderThanDays)
$pending = New-Object System.Collections.Stack
foreach ($root in $Roots) {
$item = Get-Item -LiteralPath $root -Force -ErrorAction SilentlyContinue
if ($item -and $item.PSIsContainer -and -not (Test-ReparsePoint -Item $item)) { $pending.Push($item.FullName) }
}
while ($pending.Count -gt 0) {
$dir = $pending.Pop()
foreach ($child in @(Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue)) {
if (Test-ReparsePoint -Item $child) { continue }
if ($child.PSIsContainer) { $pending.Push($child.FullName) }
elseif ($child.LastWriteTime -lt $cutoff) { $child }
}
}
}
function Test-ReparsePoint {
param($Item)
[bool]($Item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)
}
function Test-PathHasReparsePoint {
param([string]$Path)
# Check every folder up to the drive root, not just the parent: a user can swap any folder they own on the path
# for a junction after the scan, and the parent would still look like an ordinary folder.
$item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
if (-not $item -or $item.PSIsContainer -or (Test-ReparsePoint -Item $item)) { return $true }
$dir = Split-Path -Path $Path -Parent
while ($dir) {
$item = Get-Item -LiteralPath $dir -Force -ErrorAction SilentlyContinue
if (-not $item -or (Test-ReparsePoint -Item $item)) { return $true }
$dir = Split-Path -Path $dir -Parent
}
$false
}
function Remove-CleanupFile {
param($Target, $Files)
$wuWasRunning = $false
if ($Target.StopWindowsUpdate) {
$wuWasRunning = (Get-Service -Name wuauserv).Status -eq 'Running'
if ($wuWasRunning) { Stop-Service -Name wuauserv -Force }
}
try {
foreach ($file in $Files) {
if (Test-PathHasReparsePoint -Path $file.FullName) { continue }
Remove-Item -LiteralPath $file.FullName -Force -ErrorAction SilentlyContinue
}
} finally {
# Restart Windows Update even if the delete fails, so the device isn't left without it.
if ($wuWasRunning) { Start-Service -Name wuauserv }
}
}
function Clear-DeliveryOptimizationStore {
if (-not (Get-Command Delete-DeliveryOptimizationCache -ErrorAction SilentlyContinue)) { return }
Delete-DeliveryOptimizationCache -Force
Write-Output "Cleared the Delivery Optimization cache."
}
Invoke-DiskCleanup -ExpectedHostname $ctx.inputs.expected_hostname -Mode $ctx.inputs.mode `
-OlderThanDays $ctx.inputs.older_than_days -EmptyRecycleBins $ctx.inputs.empty_recycle_bins
Remove-CleanupFile notes whether Windows Update was running before stopping it, and starts it again in a finally block, so a failed delete never leaves the service stopped. Assert-TargetDevice compares the first part of expected_hostname with $env:COMPUTERNAME, so SRV-RDS01 and SRV-RDS01.contoso.local both match.
Test it
You can't use Test on an action that runs on a device. (On a cloud-sandbox action, Test isn't a dry run either: it runs the script for real and skips approvals.) Every device run is real, so start with Report mode on a test device.
- Publish the action under Workflows → Actions.
- Select Run, choose Report, enter the test device's computer name in
expected_hostname, and under Run target pick that device. Select Run action. - Check the sizes on Stdout.
- Run it again with a different name in
expected_hostnameand confirm it fails with "Nothing was changed". - On the test device, as a standard user, create a junction in your temp folder that points at a folder of test files:
mklink /J %TEMP%\linktest C:\LinkTarget. Make the test files older thanolder_than_days. - Run it with Clean up and confirm free space went up and the files in
C:\LinkTargetare still there.
A device with many user profiles can take minutes to scan, and device actions time out after 5 minutes by default. If runs time out, raise Script timeout (minutes) or run it more often so there is less to clear each time. See Timeouts.
Use it
- In a chat. Select the device and ask "How much space can we free up on this device?" or "Clean up temp files on this device". SuperIT asks whether to run on the device, then fills in the inputs from your request. Check
expected_hostnameis the device you selected. - From the app. Open the action, select Run, choose the mode, enter the device's name in
expected_hostname, pick the same device under Run target, and select Run action. - On a schedule. Ask SuperIT "Run Free up disk space in Clean up mode on SRV-RDS01 every 7 days, with expected_hostname SRV-RDS01". Each schedule is tied to one device.
- Not from tickets automatically. Don't switch on Run automatically for this action. A run started from a ticket's Recommended actions panel goes to the action's default device or your team's designated device, not the computer of the person who raised the ticket. To act on a ticket, open a chat, select the device and run it there.
- For clients. MSPs can publish it to managed teams.
Always pick the device. If no device is chosen, SuperIT runs a device action on the action's default device or your team's designated device, and expected_hostname stops the run if that's the wrong one.
Variations
- Remove user profiles unused for 90 days with
Win32_UserProfile(Remove-CimInstance). Give that its own action with Require approval from an admin group. - Add the browser caches under each user's
AppData\Localfor Chrome and Edge. Find them with the same reparse-point checks asGet-UserTempRoot. - Run
Dism.exe /Online /Cleanup-Image /StartComponentCleanupon servers, with a longer timeout.