List Installed Software with PowerShell
Query the Windows registry for a complete, accurate inventory of installed programs — locally or across a network — without triggering Windows Installer side effects.
The fastest way
Every Windows installer that follows the Microsoft convention writes an uninstall entry under HKLM. Reading those keys is instant, read-only and needs no elevation:
Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*, HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | Sort-Object DisplayName | Format-Table -AutoSize
That returns a table of display name, version, publisher and install date for machine-wide installs. If you see blank rows, those are system components that ship no DisplayName — pipe through Where-Object DisplayName to drop them.
Why not Win32_Product?
Get-WmiObject Win32_Product (or its Get-CimInstance equivalent) for inventory. Enumerating this class makes Windows Installer validate every MSI-managed package on the machine, which can trigger silent reconfigurations and self-repairs of applications — the classic cause of "apps started fixing themselves after someone ran a software audit." It is also dramatically slower than a registry read. The WMI class is fine for querying one specific product by exact identity; it is the wrong tool for listing everything.
Remote computers
The same registry query runs remotely over WinRM with Invoke-Command, so nothing has to be installed on the target:
Invoke-Command -ComputerName PC01 -ScriptBlock {
Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*,
HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* |
Where-Object DisplayName |
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate |
Sort-Object DisplayName
} -Credential (Get-Credential)
You need the WinRM service enabled on the target (it is on by default for domain-joined servers), permission as a local administrator there, and typically an elevated session. Drop the -Credential parameter when your signed-in account already has rights.
Export to CSV
Swap Format-Table for Export-Csv to hand the inventory to Excel or a ticketing system. Adding UninstallString captures each package's uninstall command line at the same time:
$paths = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
Get-ItemProperty $paths |
Where-Object DisplayName |
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, UninstallString |
Sort-Object DisplayName |
Export-Csv -Path .\installed-software.csv -NoTypeInformation
-NoTypeInformation keeps the file clean in Windows PowerShell 5.1; PowerShell 7 omits that header row by default either way.
Filtering by name
Filter early with Where-Object instead of eyeballing a long table:
Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*,
HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* |
Where-Object { $_.DisplayName -like '*Visual Studio*' } |
Select-Object DisplayName, DisplayVersion, UninstallString
Use -eq 'Exact Name' for exact matches or -match 'pattern' for regex. Filtering before Select-Object keeps the pipeline cheap on machines with hundreds of entries.
The winget alternative
winget list
On Windows 10/11 with App Installer installed, winget list is a friendly alternative: it resolves human-readable names and shows packages winget can upgrade. Scope differs though — it reports what is visible to your user account, including per-user installs, while the HKLM keys above enumerate machine-wide installs regardless of which user queries them or which package manager put them there. For audits and compliance snapshots, prefer the registry method; use winget list interactively.
64-bit vs 32-bit: why both paths?
On 64-bit Windows there are two parallel registry views. Native 64-bit installers write under HKLM:\SOFTWARE\...\Uninstall, while 32-bit installers get silently redirected into HKLM:\SOFTWARE\WOW6432Node\.... Querying only one tree misses half your inventory, so the examples above always include both paths.
HKLM:\Software\Microsoft\... quietly resolves to the WOW6432Node view and you will see duplicates or miss entries. Use the default 64-bit shell (C:\Program Files\PowerShell\7\pwsh.exe or %SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe) and query both trees explicitly.FAQ
Do I need administrator rights to list installed software?
Not locally. The HKLM uninstall keys are readable by regular users. Administrator rights are required only for the remote scenario via Invoke-Command, where you must be authorized on the target machine.
Why does the output include entries with no DisplayName?
Many uninstall keys belong to MSI components, updates and runtime libraries that do not expose a display name. Filter them out with Where-Object DisplayName right after Get-ItemProperty.
How do I get the uninstall command line for each program?
Add UninstallString (and optionally QuietUninstallString) to the Select-Object list. The value contains the exact command Windows runs when a user clicks Uninstall.
Do these commands work in both Windows PowerShell 5.1 and PowerShell 7?
Yes. Every command on this page uses built-in cmdlets and the registry provider, which behave identically in 5.1 and 7 on Windows. Export-Csv's -NoTypeInformation flag is simply redundant in 7, where it is already the default.
More free IT tools
ITInsightHub also builds fast, ad-free networking references:
- IPv4 subnet calculator — network, broadcast and host range for any CIDR block.
- Subnet cheat sheet — printable mask table from /0 to /32.
- Wildcard mask calculator — CIDR ⇔ inverse masks for ACLs.
- About ITInsightHub — what we build and why.