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

SCRIPT LIBRARY · POWERSHELL

Emptying the Recycle Bin from PowerShell, Quietly

Empty the Recycle Bin with no prompt, for one user or everyone on the machine, and optionally keep anything deleted recently.

AT A GLANCEClear-RecycleBinItem.ps1
What it does
Empties the Recycle Bin without a confirmation dialog, either through the Windows API for the current user or by cleaning the $Recycle.Bin folders directly for every user. Can keep items deleted in the last N days.
Requires
  • Windows PowerShell 5.1 or PowerShell 7+
  • No modules
Permissions
None for your own Recycle Bin. Local admin (elevated) for -AllUsers.
Runs on
Windows 10/11, Windows Server 2016+
Tested
Parse-checked and dry-run with mocked cmdlets in PowerShell 7.4

The Recycle Bin is where disk space goes to hide. Somebody drags a 40 GB folder of old ISOs into it, feels productive, and never empties it. Six months later the C: drive is red and they swear they deleted everything.

Windows PowerShell 5.1 has Clear-RecycleBin, but it only ever deals with the bin of the person running it, and it's all or nothing. The original version of this post called the Windows SHEmptyRecycleBin API directly, which is still the cleanest way to empty your own bin with no dialog. This version keeps that, fixes the broken code block from the old post, and adds the two things I actually wanted: clean every user's bin on a shared machine, and only remove things that have been sitting there a while.

Run it with -WhatIf first. There's no undo on this one. That's kind of the point of the Recycle Bin.

Clear-RecycleBinItem.ps1Download
<#
.SYNOPSIS
    Empties the Recycle Bin without a prompt, for the current user or everyone, optionally
    only for items deleted more than N days ago.
.DESCRIPTION
    With no parameters it calls the Windows SHEmptyRecycleBin API for the current user,
    which is exactly what "Empty Recycle Bin" in Explorer does, minus the dialog and the sound.

    With -OlderThanDays or -AllUsers it works on the $Recycle.Bin folders directly. Every
    deleted item is a pair of files: $I<id> (the metadata) and $R<id> (the actual content).
    The script removes both halves together so Explorer never ends up with ghost entries.
.PARAMETER OlderThanDays
    Only remove items that have been in the bin longer than this. 0 (the default) means everything.
.PARAMETER AllUsers
    Clean every user's Recycle Bin, not just yours. Needs to run elevated.
.PARAMETER DriveLetter
    Which drives to clean. Defaults to every local fixed drive.
.EXAMPLE
    .\Clear-RecycleBinItem.ps1
.EXAMPLE
    .\Clear-RecycleBinItem.ps1 -AllUsers -OlderThanDays 30 -WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [ValidateRange(0, 3650)]
    [int]$OlderThanDays = 0,

    [switch]$AllUsers,

    [ValidatePattern('^[A-Za-z]$')]
    [string[]]$DriveLetter
)

if (-not $DriveLetter) {
    $DriveLetter = Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' |
        ForEach-Object { $_.DeviceID.TrimEnd(':') }
}

$mySid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value

# Fast path: current user, everything. Let Windows do it the official way.
if (-not $AllUsers -and $OlderThanDays -eq 0) {
    if (-not ('RecycleBin.NativeMethods' -as [type])) {
        Add-Type -Namespace RecycleBin -Name NativeMethods -MemberDefinition @'
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
public static extern int SHEmptyRecycleBin(IntPtr hwnd, string pszRootPath, uint dwFlags);
'@
    }
    # SHERB_NOCONFIRMATION | SHERB_NOPROGRESSUI | SHERB_NOSOUND
    $flags = 0x1 -bor 0x2 -bor 0x4

    foreach ($letter in $DriveLetter) {
        $root = "${letter}:\"
        if (-not $PSCmdlet.ShouldProcess("Recycle Bin on $root for $env:USERNAME", 'Empty')) { continue }
        $hr = [RecycleBin.NativeMethods]::SHEmptyRecycleBin([IntPtr]::Zero, $root, $flags)
        [pscustomobject]@{
            Drive        = $root
            Sid          = $mySid
            ItemsRemoved = $null
            MBFreed      = $null
            # A non-zero result on an already-empty bin is normal, not an error.
            Status       = if ($hr -eq 0) { 'Emptied' } else { 'Nothing to empty (0x{0:X8})' -f $hr }
        }
    }
    return
}

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

foreach ($letter in $DriveLetter) {
    $binRoot = "${letter}:\`$Recycle.Bin"
    if (-not (Test-Path -LiteralPath $binRoot)) { Write-Verbose "No Recycle Bin on ${letter}:"; continue }

    $sidFolders = Get-ChildItem -LiteralPath $binRoot -Directory -Force -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -like 'S-1-*' -and ($AllUsers -or $_.Name -eq $mySid) }

    foreach ($folder in $sidFolders) {
        try {
            # $I files are created at the moment of deletion, so their age is the item's age in the bin.
            $metaFiles = @(Get-ChildItem -LiteralPath $folder.FullName -Filter '$I*' -File -Force -ErrorAction Stop |
                Where-Object { $OlderThanDays -eq 0 -or $_.CreationTime -lt $cutoff })
        }
        catch {
            Write-Warning "Can't read $($folder.FullName): $($_.Exception.Message)"
            continue
        }
        if ($metaFiles.Count -eq 0) { continue }

        $pairs = foreach ($meta in $metaFiles) {
            $dataPath = Join-Path $folder.FullName ('$R' + $meta.Name.Substring(2))
            $bytes = 0
            if (Test-Path -LiteralPath $dataPath) {
                $bytes = (Get-ChildItem -LiteralPath $dataPath -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum
                if (-not $bytes) { $bytes = (Get-Item -LiteralPath $dataPath -Force).Length }
            }
            [pscustomobject]@{ Meta = $meta.FullName; Data = $dataPath; Bytes = [long]$bytes }
        }

        $totalMB = [math]::Round((($pairs | Measure-Object Bytes -Sum).Sum) / 1MB, 1)
        $target = "$($pairs.Count) item(s), $totalMB MB in $($folder.FullName)"
        if (-not $PSCmdlet.ShouldProcess($target, 'Permanently delete')) { continue }

        $removed = 0; $freed = 0L
        foreach ($p in $pairs) {
            try {
                # Content first, metadata second. If the content won't go, the entry stays visible.
                if (Test-Path -LiteralPath $p.Data) { Remove-Item -LiteralPath $p.Data -Recurse -Force -ErrorAction Stop }
                Remove-Item -LiteralPath $p.Meta -Force -ErrorAction Stop
                $removed++; $freed += $p.Bytes
            }
            catch {
                Write-Warning "Skipped $($p.Data): $($_.Exception.Message)"
            }
        }

        [pscustomobject]@{
            Drive        = "${letter}:\"
            Sid          = $folder.Name
            ItemsRemoved = $removed
            MBFreed      = [math]::Round($freed / 1MB, 1)
            Status       = if ($removed -eq $pairs.Count) { 'Emptied' } else { "Partial ($($pairs.Count - $removed) skipped)" }
        }
    }
}

Parameters

ParameterTypeDefaultWhat it's for
-OlderThanDaysint0Only remove items that have been in the bin longer than this. 0 means everything.
-AllUsersswitch—Clean every user's Recycle Bin on the machine. Needs an elevated session.
-DriveLetterstring[]all fixed drivesLimit the cleanup to specific drives, as bare letters like C or D.

Run it

Empty your own Recycle Bin on every drive, no dialog, no sound.

.\Clear-RecycleBinItem.ps1

See what a shared-PC cleanup would remove, keeping anything deleted in the last 30 days.

.\Clear-RecycleBinItem.ps1 -AllUsers -OlderThanDays 30 -WhatIf

Do it for real, just on D.

.\Clear-RecycleBinItem.ps1 -AllUsers -OlderThanDays 30 -DriveLetter D

How much did that free up?

.\Clear-RecycleBinItem.ps1 -AllUsers -OlderThanDays 14 | Measure-Object MBFreed -Sum

What you'll see

Example outputvalues are illustrative
Drive Sid                                            ItemsRemoved MBFreed Status
----- ---                                            ------------ ------- ------
C:\   S-1-5-21-1004336348-1177238915-682003330-1104            37  4812.6 Emptied
C:\   S-1-5-21-1004336348-1177238915-682003330-1117             3    12.4 Partial (1 skipped)
D:\   S-1-5-21-1004336348-1177238915-682003330-1104             2 38210.0 Emptied

How it works

  1. Fast path for the simple case. No -AllUsers, no age limit? The script loads SHEmptyRecycleBin from shell32 and calls it once per drive with the no-confirmation, no-progress and no-sound flags. That's exactly what Explorer does, minus the dialog.
  2. Folder path for everything else. Every NTFS drive has a hidden $Recycle.Bin folder with one subfolder per user SID. The script picks your SID, or every SID with -AllUsers.
  3. Pair up the files. Each deleted item is two files: $I<id> holds the original name, path and deletion time, and $R<id> is the actual content (a file or a whole folder). The $I file is created at the moment of deletion, so its creation time tells us how long the item has been in the bin.
  4. Delete content first, then metadata. If the $R half won't delete, the $I half is left alone, so you never end up with ghost entries in Explorer that point at nothing.
  5. Report per user, per drive. You get item counts and megabytes freed back as objects.

Take it further

  • Storage Sense might already do this. On Windows 10 and 11, Storage Sense can empty items older than 1, 14, 30 or 60 days per user, and Intune can configure it. The script is for the machines and cases it doesn't cover, like shared PCs and servers.
  • Map SIDs to names. Pipe the results through [System.Security.Principal.SecurityIdentifier] and .Translate([System.Security.Principal.NTAccount]) if you want to know whose 38 GB that was.
  • Schedule it on shared machines. Lab and kiosk PCs are where abandoned Recycle Bins pile up. A weekly task running as SYSTEM with -AllUsers -OlderThanDays 14 keeps them honest.

Things that'll trip you up

  • SHEmptyRecycleBin only knows about you. The API path empties the bin of whoever runs the script. Run it as SYSTEM from a scheduled task or a deployment tool and it cheerfully empties SYSTEM's bin, which is almost always empty. Use -AllUsers for anything that isn't running as the user.
  • An empty bin reports an odd status. When there's nothing to empty, the API returns a non-zero code instead of success. The script shows that as "Nothing to empty" rather than an error, because it isn't one.
  • Age is time in the bin, not file age. The age check uses when the item was deleted, not when the file was last modified. A ten-year-old file deleted this morning is kept by -OlderThanDays 30.
  • Some items won't go. A file that's open (yes, people open things from the Recycle Bin) or has odd permissions is skipped with a warning. Both halves of the item stay put, so Explorer still shows it correctly.