<# .SYNOPSIS Lists Explorer's icon overlay handlers, shows which ones are past the 15-slot limit, and disables or re-enables the ones you pick. .DESCRIPTION Explorer only loads the first 15 icon overlay handlers under HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers, in the order the key names sort. Sync apps register several each and pad their names with leading spaces to win the race, so the ones you care about can silently lose their icons, and a misbehaving handler can make Explorer hang or crash. With no parameters it lists every handler in load order with its CLSID, name and DLL, marks the ones past -Limit, and includes any you've disabled with this script. -Disable exports the whole key to a .reg backup, then moves the chosen handlers to HKLM\SOFTWARE\DisabledShellIconOverlays, where Explorer doesn't look. -Enable moves them back. Nothing is deleted. Changes take effect when Explorer restarts (-RestartExplorer, or sign out). .PARAMETER Disable Handler names to disable. Wildcards work, and leading spaces are ignored (DropboxExt0* matches ' DropboxExt01'). .PARAMETER Enable Previously disabled handler names to put back. .PARAMETER Limit How many handlers Explorer loads. Default: 15. .PARAMETER BackupFolder Where the .reg backups go. Default: OverlayHandlerBackups in your Documents folder. .PARAMETER RestartExplorer Restart Explorer afterwards so the change shows up. Open Explorer windows will close. .EXAMPLE .\Disable-ShellOverlayHandler.ps1 | Format-Table Position, Name, Handler, State .EXAMPLE .\Disable-ShellOverlayHandler.ps1 -Disable 'DropboxExt1*', 'IDM Shell Extension' -RestartExplorer -WhatIf #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'List')] param( [Parameter(Mandatory, ParameterSetName = 'Disable')] [ValidateNotNullOrEmpty()] [string[]]$Disable, [Parameter(Mandatory, ParameterSetName = 'Enable')] [ValidateNotNullOrEmpty()] [string[]]$Enable, [ValidateRange(1, 64)] [int]$Limit = 15, [Parameter(ParameterSetName = 'Disable')] [Parameter(ParameterSetName = 'Enable')] [string]$BackupFolder = (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'OverlayHandlerBackups'), [Parameter(ParameterSetName = 'Disable')] [Parameter(ParameterSetName = 'Enable')] [switch]$RestartExplorer ) $activeKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers' $parkedKey = 'HKLM:\SOFTWARE\DisabledShellIconOverlays' function Get-OverlayHandler([string]$KeyPath, [string]$State) { if (-not (Test-Path -LiteralPath $KeyPath)) { return } $keys = [System.Collections.Generic.List[object]]::new() foreach ($k in Get-ChildItem -LiteralPath $KeyPath) { $keys.Add($k) } # The registry sorts names ordinally after upper-casing them, which is why ' Name' (spaces) beats 'Name'. Match that exactly. $keys.Sort([Comparison[object]] { param($a, $b) [string]::CompareOrdinal($a.PSChildName.ToUpperInvariant(), $b.PSChildName.ToUpperInvariant()) }) $position = 0 foreach ($k in $keys) { $position++ $clsid = [string]$k.GetValue('') $handler = $null; $dll = $null if ($clsid -match '^\{[0-9A-Fa-f-]{36}\}$') { $clsidKey = "Registry::HKEY_CLASSES_ROOT\CLSID\$clsid" $handler = (Get-ItemProperty -LiteralPath $clsidKey -ErrorAction SilentlyContinue).'(default)' $dll = (Get-ItemProperty -LiteralPath "$clsidKey\InprocServer32" -ErrorAction SilentlyContinue).'(default)' } [pscustomobject]@{ Position = if ($State -eq 'Active') { $position } else { $null } Name = $k.PSChildName.Trim() RawName = $k.PSChildName State = if ($State -ne 'Active') { 'Disabled' } elseif ($position -le $Limit) { 'Loaded' } else { 'OverLimit' } Handler = $handler Clsid = $clsid Dll = $dll } } } function Select-Handler($List, [string[]]$Pattern) { foreach ($p in $Pattern) { $hit = @($List | Where-Object { $_.Name -like $p.Trim() -or $_.RawName -eq $p }) if (-not $hit) { Write-Warning "No handler matches '$p'." } $hit } } if ($PSCmdlet.ParameterSetName -eq 'List') { Get-OverlayHandler $activeKey 'Active' Get-OverlayHandler $parkedKey 'Disabled' return } $identity = [Security.Principal.WindowsIdentity]::GetCurrent() if (-not ([Security.Principal.WindowsPrincipal]$identity).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Changing overlay handlers writes to HKLM, so run this from an elevated PowerShell.' } if ($PSCmdlet.ParameterSetName -eq 'Disable') { $targets = @(Select-Handler (Get-OverlayHandler $activeKey 'Active') $Disable | Sort-Object RawName -Unique) $from = $activeKey; $to = $parkedKey; $verb = 'Disable overlay handler'; $done = 'Disabled' } else { $targets = @(Select-Handler (Get-OverlayHandler $parkedKey 'Disabled') $Enable | Sort-Object RawName -Unique) $from = $parkedKey; $to = $activeKey; $verb = 'Re-enable overlay handler'; $done = 'Enabled' } if (-not $targets) { return } # Back up both keys before touching anything. No backup, no changes. $backupFile = $null if (-not $WhatIfPreference) { try { $null = New-Item -ItemType Directory -Path $BackupFolder -Force -ErrorAction Stop $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' foreach ($pair in @(@($activeKey, 'active'), @($parkedKey, 'disabled'))) { if (-not (Test-Path -LiteralPath $pair[0])) { continue } $file = Join-Path $BackupFolder "ShellIconOverlayIdentifiers-$($pair[1])-$stamp.reg" $regPath = $pair[0] -replace '^HKLM:', 'HKEY_LOCAL_MACHINE' $null = & "$env:SystemRoot\System32\reg.exe" export $regPath $file /y 2>&1 if ($LASTEXITCODE -ne 0) { throw "reg.exe export of $regPath failed with exit code $LASTEXITCODE" } if ($pair[1] -eq 'active') { $backupFile = $file } } } catch { throw "Backup failed, so nothing was changed: $($_.Exception.Message)" } } foreach ($t in $targets) { if (-not $PSCmdlet.ShouldProcess("'$($t.Name)' ($($t.Handler))", $verb)) { continue } $result = [pscustomobject]@{ Name = $t.Name; Handler = $t.Handler; Action = $null; Backup = $backupFile } try { if (-not (Test-Path -LiteralPath $to)) { $null = New-Item -Path $to -Force -ErrorAction Stop } $existing = Join-Path $to $t.RawName if (Test-Path -LiteralPath $existing) { if ($done -eq 'Enabled') { throw 'The app has already re-created this handler, so it is active again. Nothing to move.' } Remove-Item -LiteralPath $existing -Recurse -Force -ErrorAction Stop # an older parked copy } Move-Item -LiteralPath (Join-Path $from $t.RawName) -Destination $to -ErrorAction Stop $result.Action = $done } catch { $result.Action = 'Failed' Write-Warning "$($t.Name): $($_.Exception.Message)" } $result } if ($RestartExplorer -and $PSCmdlet.ShouldProcess('explorer.exe', 'Restart Explorer')) { Get-Process -Name explorer -ErrorAction SilentlyContinue | Stop-Process -Force # Windows restarts the shell by itself, as the signed-in user. Starting it from here would run it elevated. Start-Sleep -Seconds 4 if (-not (Get-Process -Name explorer -ErrorAction SilentlyContinue)) { Write-Warning 'Explorer did not come back on its own. Press Ctrl+Shift+Esc, choose Run new task, and type explorer.' } } elseif (-not $RestartExplorer -and -not $WhatIfPreference) { Write-Warning 'Restart Explorer (or sign out and back in) for the change to show.' }