SCRIPT LIBRARY · POWERSHELL
Take Control of Explorer's Preview Pane with PowerShell
List the preview handlers installed on a PC, see which one each file type uses, and point extensions like .log, .ps1 or .json at the handler you want.
- What it does
- Lists registered Explorer preview handlers, shows which handler a file extension is using, and maps extensions to a handler (or removes the mapping) for the current user or the whole machine.
- Requires
- PowerShell 7+ or Windows PowerShell 5.1
- No modules
- Permissions
- None for -Scope CurrentUser. Local admin for -Scope LocalMachine.
- Runs on
- Windows 10/11
- Tested
- Parse-checked and dry-run with a mocked registry in PowerShell 7.4 (listing, name and CLSID lookup, existing-mapping protection, -Force, -Remove and -WhatIf)
Part 5 of the thread File wrangling
The preview pane (Alt+P in Explorer) is one of those features people either love or forget exists. Click a file, see what's in it, no app launch. Except half the time you click a .log or .ps1 and get "No preview available," even though it's plain text and Windows ships a perfectly good text previewer.
That's because preview handlers are wired up per file type. A handler is a COM object, registered in HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\PreviewHandlers, and Explorer finds the right one for a file through a shellex key named {8895b1c6-b41f-4c1c-a562-0d564250836f} under the file's extension (or its ProgID). If that key isn't there, you get nothing. This script lists what's installed, shows what each extension is using, and adds or removes that mapping.
The earlier version of this post installed a few apps with Chocolatey and changed their file associations with assoc and ftype. That changes which app opens a file, which isn't the same thing as a preview handler, so it didn't really do what the title said. Third-party handlers (for PDFs, 3D models, Markdown and so on) come with their own installers, including Microsoft's PowerToys. Install whichever you like, then use this to see and control where they apply.
<#
.SYNOPSIS
Lists Explorer preview handlers and maps file extensions to them.
.DESCRIPTION
Preview handlers are COM objects registered under
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\PreviewHandlers. Explorer finds the one for a
file type through the shellex key {8895b1c6-b41f-4c1c-a562-0d564250836f} under the extension
(or its ProgID). This script shows what's installed, shows which handler an extension uses,
and points an extension at a handler you choose, or removes that mapping. Supports -WhatIf.
.PARAMETER ListHandlers
List every registered preview handler with its CLSID.
.PARAMETER Extension
One or more extensions, like .log or .ps1. On its own, shows the current mapping.
.PARAMETER Handler
The handler to use: a CLSID, or part of its name as shown by -ListHandlers.
.PARAMETER Remove
Delete the mapping for the extension at the chosen scope.
.PARAMETER Scope
CurrentUser (HKCU, no admin needed) or LocalMachine (HKLM, needs admin). Default CurrentUser.
.PARAMETER Force
Replace an existing mapping that points at a different handler.
.EXAMPLE
.\Set-PreviewHandler.ps1 -ListHandlers
.EXAMPLE
.\Set-PreviewHandler.ps1 -Extension .log, .ps1, .json -Handler '{1531d583-8375-4d3f-b5fb-d23bbd169f22}' -WhatIf
#>
[CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Show')]
param(
[Parameter(Mandatory, ParameterSetName = 'List')][switch]$ListHandlers,
[Parameter(Mandatory, ParameterSetName = 'Show')]
[Parameter(Mandatory, ParameterSetName = 'Set')]
[Parameter(Mandatory, ParameterSetName = 'Remove')]
[ValidatePattern('^\.[\w\-\.]+$')][string[]]$Extension,
[Parameter(Mandatory, ParameterSetName = 'Set')][string]$Handler,
[Parameter(Mandatory, ParameterSetName = 'Remove')][switch]$Remove,
[Parameter(ParameterSetName = 'Set')]
[Parameter(ParameterSetName = 'Remove')]
[ValidateSet('CurrentUser', 'LocalMachine')][string]$Scope = 'CurrentUser',
[Parameter(ParameterSetName = 'Set')][switch]$Force
)
$previewGuid = '{8895b1c6-b41f-4c1c-a562-0d564250836f}'
$hkcr = 'Registry::HKEY_CLASSES_ROOT'
function Get-DefaultValue([string]$Key) {
if (Test-Path -LiteralPath $Key) { (Get-ItemProperty -LiteralPath $Key -ErrorAction SilentlyContinue).'(default)' }
}
# Every registered handler: value name is the CLSID, value data is the friendly name.
$handlers = foreach ($root in 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\PreviewHandlers', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\PreviewHandlers') {
if (-not (Test-Path -LiteralPath $root)) { continue }
$props = Get-ItemProperty -LiteralPath $root
foreach ($p in $props.PSObject.Properties | Where-Object Name -match '^\{[0-9a-fA-F\-]{36}\}$') {
[pscustomobject]@{ Name = $p.Value; Clsid = $p.Name.ToLower() }
}
}
$handlers = @($handlers | Sort-Object Clsid -Unique | Sort-Object Name)
if ($ListHandlers) { return $handlers }
foreach ($ext in $Extension.ToLower()) {
$userKey = "HKCU:\Software\Classes\$ext\shellex\$previewGuid"
$key = if ($Scope -eq 'LocalMachine') { "HKLM:\Software\Classes\$ext\shellex\$previewGuid" } else { $userKey }
if ($PSCmdlet.ParameterSetName -eq 'Show') {
# Look where handlers usually get registered: the extension, its ProgID, and SystemFileAssociations.
$progId = Get-DefaultValue "$hkcr\$ext"
$found = $null
foreach ($where in @("$hkcr\$ext", $(if ($progId) { "$hkcr\$progId" }), "$hkcr\SystemFileAssociations\$ext")) {
if (-not $where) { continue }
$clsid = Get-DefaultValue "$where\shellex\$previewGuid"
if ($clsid) { $found = [pscustomobject]@{ Extension = $ext; Handler = ($handlers | Where-Object Clsid -eq $clsid.ToLower()).Name; Clsid = $clsid; FoundAt = $where -replace '^Registry::', '' }; break }
}
if (-not $found) { $found = [pscustomobject]@{ Extension = $ext; Handler = '(none)'; Clsid = $null; FoundAt = $null } }
$found
continue
}
try {
if ($Remove) {
if (-not (Test-Path -LiteralPath $key)) { Write-Verbose "$ext has no mapping at $Scope scope."; continue }
if ($PSCmdlet.ShouldProcess("$ext ($Scope)", 'Remove preview handler mapping')) {
Remove-Item -LiteralPath $key -Recurse -ErrorAction Stop
[pscustomobject]@{ Extension = $ext; Scope = $Scope; Status = 'Removed'; Clsid = $null }
}
continue
}
$target = if ($Handler -match '^\{?[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}\}?$') {
'{' + $Handler.Trim('{}').ToLower() + '}'
}
else {
$hits = @($handlers | Where-Object Name -like "*$Handler*")
if ($hits.Count -ne 1) { throw "'$Handler' matches $($hits.Count) handlers. Use -ListHandlers and pass the CLSID." }
$hits[0].Clsid
}
if ($handlers.Clsid -notcontains $target -and -not $Force) { throw "$target isn't a registered preview handler. Install the handler first, or use -Force to map it anyway." }
$current = Get-DefaultValue $key
if ($current -and $current -ne $target -and -not $Force) {
[pscustomobject]@{ Extension = $ext; Scope = $Scope; Status = "SkippedExisting ($current), use -Force"; Clsid = $current }
continue
}
if ($PSCmdlet.ShouldProcess("$ext ($Scope)", "Set preview handler to $target")) {
if (-not (Test-Path -LiteralPath $key)) { $null = New-Item -Path $key -Force -ErrorAction Stop }
Set-ItemProperty -LiteralPath $key -Name '(default)' -Value $target -ErrorAction Stop
[pscustomobject]@{ Extension = $ext; Scope = $Scope; Status = 'Set'; Clsid = $target }
}
}
catch {
Write-Warning "${ext}: $($_.Exception.Message)"
}
}
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-ListHandlers | switch | — | List every registered preview handler, with its name and CLSID. |
-Extension | string[] | — | One or more extensions, with the dot. On its own, shows which handler each one uses and where the mapping lives. |
-Handler | string | — | The handler to map to. A CLSID, or part of the handler's name as -ListHandlers shows it. A name has to match exactly one handler. |
-Remove | switch | — | Delete the mapping for the extension at the chosen scope. Honors -WhatIf. |
-Scope | string | CurrentUser | CurrentUser writes to HKCU and needs no admin rights. LocalMachine writes to HKLM for everyone on the PC. |
-Force | switch | — | Replace a mapping that already points at a different handler, or map a CLSID that isn't in the registered list. |
Run it
What's installed on this PC?
.\Set-PreviewHandler.ps1 -ListHandlersWhich handler do these file types use right now?
.\Set-PreviewHandler.ps1 -Extension .txt, .log, .ps1, .mdPreview scripts and logs as plain text, with Windows' built-in text previewer.
.\Set-PreviewHandler.ps1 -Extension .log, .ps1, .psm1, .json, .yml -Handler '{1531d583-8375-4d3f-b5fb-d23bbd169f22}' -WhatIfUndo it for one type.
.\Set-PreviewHandler.ps1 -Extension .json -RemoveWhat you'll see
Extension Scope Status Clsid
--------- ----- ------ -----
.log CurrentUser Set {1531d583-8375-4d3f-b5fb-d23bbd169f22}
.ps1 CurrentUser Set {1531d583-8375-4d3f-b5fb-d23bbd169f22}
.psm1 CurrentUser Set {1531d583-8375-4d3f-b5fb-d23bbd169f22}
.json CurrentUser SkippedExisting ({11111111-2222-3333-4444-555555555555}), use -Force {11111111-2222-3333-4444-555555555555}
.yml CurrentUser Set {1531d583-8375-4d3f-b5fb-d23bbd169f22}
How it works
- Read the handler list. Every registered handler is a value under
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\PreviewHandlers(and itsWOW6432Nodetwin), where the value name is the CLSID and the data is the friendly name.-ListHandlersjust returns that. - Show a mapping. With only
-Extension, the script looks in the mergedHKEY_CLASSES_ROOTview under the extension, its ProgID, andSystemFileAssociations, for ashellex\{8895b1c6-...}key, and reports the first one it finds and where it lives. - Resolve the handler.
-Handlercan be a CLSID or part of a name. A name has to match exactly one registered handler, and a CLSID that isn't registered is refused unless you pass-Force, because mapping to a handler that doesn't exist just gets you a blank pane. - Write it. The mapping goes in
Software\Classes\<ext>\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}under HKCU or HKLM, with the handler's CLSID as the default value. An existing mapping to a different handler is left alone unless you pass-Force. - Everything that writes goes through ShouldProcess. Setting and removing both honor
-WhatIfand-Confirm.
Take it further
- Roll it out with Intune or Group Policy. A per-user mapping is just a registry value under HKCU, so a Group Policy Preferences registry item or a remediation script can push the same thing to every machine.
- Map a whole family at once. Keep a list of text-ish extensions (
.log,.ini,.cfg,.ps1,.psm1,.psd1,.json,.yml,.md,.csv) in a file and pass it straight to-Extension. - Audit before you change. Run the show mode across a list of extensions on a few PCs first. You'll learn which apps have already claimed which types.
Things that'll trip you up
- Explorer caches. A new mapping usually shows up the next time you click a file, but sometimes Explorer hangs on to the old answer. Close and reopen the window, or restart Explorer from Task Manager, before deciding it didn't work.
- Downloaded files may never preview. Recent Windows security updates block the preview pane for files carrying the Mark of the Web (anything downloaded from the internet). If a downloaded file won't preview but a local copy will, that's why. Unblock it in the file's Properties if you trust it.
- The ProgID can have its own handler. The script writes the mapping under the extension. If an app registered a different handler under the file type's ProgID, that one can win. Run the script with just -Extension to see what's found and where.
- Check the CLSID on your build. {1531d583-8375-4d3f-b5fb-d23bbd169f22} is the text previewer on Windows 10 and 11, but run -ListHandlers and check before you roll a mapping out to a fleet.
- Handlers run code on every click. A preview handler parses the file the moment you select it. Only install handlers from sources you trust, and don't map risky types (like .hta or .lnk) to anything.