<# .SYNOPSIS Clears the disk caches for Chrome, Edge, Brave, Opera and Firefox, profile by profile, and reports the space freed. .DESCRIPTION Finds every browser profile under a Windows user profile (Default, Profile 1, Profile 2 and so on for the Chromium browsers, every profile folder for Firefox) and empties the cache folders only: Chrome, Edge, Brave, Opera Cache, Code Cache, GPUCache, Service Worker\CacheStorage Firefox cache2 Nothing else is touched. Cookies, history, saved passwords, bookmarks, Local Storage, IndexedDB, extensions and settings all live in other files and folders, and the script never goes near them. A browser that's running is skipped, because it holds its cache files open. Add -CloseBrowser to close it first (you'll be asked to confirm unless you add -Force). Returns one object per browser profile with the size before and the space freed. Supports -WhatIf. .PARAMETER Browser Which browsers to clean. Default: all five. .PARAMETER UserProfile One or more Windows user profile folders to clean. Default: the current user. Pass the folders under C:\Users (from an elevated session) to do every user on the machine. .PARAMETER CloseBrowser Close a running browser before cleaning it, instead of skipping it. .PARAMETER Force With -CloseBrowser, don't ask before closing. .EXAMPLE .\Clear-BrowserCache.ps1 -WhatIf .EXAMPLE .\Clear-BrowserCache.ps1 -Browser Chrome, Edge -CloseBrowser .EXAMPLE Get-ChildItem C:\Users -Directory | .\Clear-BrowserCache.ps1 -Browser Firefox #> [CmdletBinding(SupportsShouldProcess)] param( [ValidateSet('Chrome', 'Edge', 'Brave', 'Opera', 'Firefox')] [string[]]$Browser = @('Chrome', 'Edge', 'Brave', 'Opera', 'Firefox'), [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName')] [string[]]$UserProfile = $env:USERPROFILE, [switch]$CloseBrowser, [switch]$Force ) begin { # Where each browser keeps its profiles (relative to the user profile), and the process name to check for. $browsers = [ordered]@{ Chrome = @{ Process = 'chrome'; Kind = 'Chromium'; Roots = @('AppData\Local\Google\Chrome\User Data') } Edge = @{ Process = 'msedge'; Kind = 'Chromium'; Roots = @('AppData\Local\Microsoft\Edge\User Data') } Brave = @{ Process = 'brave'; Kind = 'Chromium'; Roots = @('AppData\Local\BraveSoftware\Brave-Browser\User Data') } # Opera keeps the profile itself in Roaming and the cache in Local, with no Default folder. Opera = @{ Process = 'opera'; Kind = 'Opera'; Roots = @('AppData\Local\Opera Software\Opera Stable', 'AppData\Roaming\Opera Software\Opera Stable', 'AppData\Local\Opera Software\Opera GX Stable', 'AppData\Roaming\Opera Software\Opera GX Stable') } # Firefox's cache2 lives in Local, not in the Roaming profile folder next to your bookmarks. Firefox = @{ Process = 'firefox'; Kind = 'Firefox'; Roots = @('AppData\Local\Mozilla\Firefox\Profiles') } } $chromiumCaches = @('Cache', 'Code Cache', 'GPUCache', 'Service Worker\CacheStorage') function Get-FolderSize([string]$Folder) { if (-not (Test-Path -LiteralPath $Folder)) { return [long]0 } $sum = (Get-ChildItem -LiteralPath $Folder -File -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum [long]$(if ($sum) { $sum } else { 0 }) } # Returns @{ ProfileName = @(cache folders) } for one browser under one user profile. function Get-CacheTarget([string]$UserHome, $Info) { $targets = [ordered]@{} foreach ($rel in $Info.Roots) { $root = Join-Path $UserHome $rel if (-not (Test-Path -LiteralPath $root -PathType Container)) { continue } switch ($Info.Kind) { 'Chromium' { $profiles = Get-ChildItem -LiteralPath $root -Directory | Where-Object { $_.Name -eq 'Default' -or $_.Name -like 'Profile *' -or $_.Name -eq 'Guest Profile' } foreach ($p in $profiles) { $targets[$p.Name] = @($chromiumCaches | ForEach-Object { Join-Path $p.FullName $_ }) } } 'Opera' { $name = Split-Path $root -Leaf $list = @(if ($targets.Contains($name)) { $targets[$name] }) $targets[$name] = $list + @($chromiumCaches | ForEach-Object { Join-Path $root $_ }) } 'Firefox' { foreach ($p in Get-ChildItem -LiteralPath $root -Directory) { $targets[$p.Name] = @(Join-Path $p.FullName 'cache2') } } } } $targets } $userList = [System.Collections.Generic.List[string]]::new() } process { foreach ($u in $UserProfile) { if ($u) { $userList.Add($u) } } } end { foreach ($name in $Browser) { $info = $browsers[$name] # Is it running? A running browser holds its cache open, so either close it or skip it. $running = @(Get-Process -Name $info.Process -ErrorAction SilentlyContinue) $skipReason = $null if ($running.Count) { if (-not $CloseBrowser) { $skipReason = 'Skipped: browser is running (use -CloseBrowser)' } elseif ($WhatIfPreference) { Write-Host "What if: would close $name ($($running.Count) process(es)) before cleaning." } else { # Ask before closing. If the prompt can't be shown (a non-interactive session), treat it as a no. $ok = $Force if (-not $ok) { try { $ok = $PSCmdlet.ShouldContinue("Close every $name window? Tabs usually come back on restart, but anything unsaved in a page will be lost.", "Close $name") } catch { Write-Warning "Couldn't ask about closing $name ($($_.Exception.Message)). Use -Force to close it without asking."; $ok = $false } } if (-not $ok) { $skipReason = 'Skipped: you chose not to close it' } } if ($running.Count -and -not $skipReason -and -not $WhatIfPreference) { if ($PSCmdlet.ShouldProcess("$name ($($running.Count) process(es))", 'Close browser')) { $running | ForEach-Object { try { [void]$_.CloseMainWindow() } catch { } } $running | Wait-Process -Timeout 10 -ErrorAction SilentlyContinue # Anything still open after 10 seconds (a stuck tab, a background process) gets stopped. $running | Where-Object { -not $_.HasExited } | Stop-Process -Force -ErrorAction SilentlyContinue $running | Wait-Process -Timeout 5 -ErrorAction SilentlyContinue if ($running | Where-Object { -not $_.HasExited }) { $skipReason = 'Skipped: browser would not close' } } else { $skipReason = 'Skipped: browser is running' } } } foreach ($userHome in $userList) { $targets = Get-CacheTarget $userHome $info if (-not $targets.Count) { Write-Verbose "$name : no profiles under $userHome"; continue } foreach ($profileName in $targets.Keys) { $folders = @($targets[$profileName] | Where-Object { Test-Path -LiteralPath $_ -PathType Container }) $before = [long]0 foreach ($f in $folders) { $before += Get-FolderSize $f } $status = 'Cleared'; $failed = 0 if ($skipReason) { $status = $skipReason } elseif (-not $folders.Count) { $status = 'Nothing to clear' } else { foreach ($f in $folders) { if (-not $PSCmdlet.ShouldProcess($f, "Empty $name cache")) { $status = 'WhatIf'; continue } # Empty the folder but keep it, so the browser finds what it expects on the next start. foreach ($item in Get-ChildItem -LiteralPath $f -Force -ErrorAction SilentlyContinue) { try { Remove-Item -LiteralPath $item.FullName -Recurse -Force -ErrorAction Stop } catch { $failed++; Write-Verbose "$($item.FullName): $($_.Exception.Message)" } } } if ($failed) { $status = "Partial: $failed item(s) in use" } } $after = [long]0 if ($status -notlike 'Skipped*' -and $status -ne 'WhatIf') { foreach ($f in $folders) { $after += Get-FolderSize $f } } else { $after = $before } [pscustomobject]@{ Browser = $name Profile = $profileName User = Split-Path $userHome -Leaf Status = $status SizeMB = [math]::Round($before / 1MB, 1) FreedMB = [math]::Round(($before - $after) / 1MB, 1) FreedBytes = $before - $after } } } } }