SCRIPT LIBRARY · POWERSHELL
List Installed Software on Windows with PowerShell
A quick, safe software inventory from the registry, local or remote, that returns objects you can filter and export instead of a wall of colored text.
- What it does
- Reads the 64-bit, 32-bit and (optionally) per-user Uninstall registry keys and returns each installed application's name, version, publisher, install date and architecture, for one computer or many.
- Requires
- Windows PowerShell 5.1 or PowerShell 7+
- No modules
- PowerShell remoting (WinRM) enabled on remote computers
- Permissions
- Any user can read the local Uninstall keys. For remote computers you need rights to connect with PowerShell remoting, which usually means local admin.
- Runs on
- Windows 10/11, Windows Server 2016+
- Tested
- Parse-checked and dry-run with mocked cmdlets in PowerShell 7.4
Somebody asks "which machines still have the old Java on them?" and you realize there's no quick answer. Maybe there's an inventory tool, but its last scan was Tuesday and the machine in question was off on Tuesday.
Windows keeps its own list. Every well-behaved installer writes an entry under the Uninstall registry keys, and that's exactly what Programs and Features (and Settings > Apps) reads to build its list. Read those keys yourself and you've got the same inventory in a second or two, with no agent and nothing to install.
The old version of this script printed a colorful list to the screen, which looked nice and was impossible to do anything with. This one returns objects, works against remote machines, hides the same system components Windows hides, and has a name filter so you can go straight to the thing you're hunting for.
<#
.SYNOPSIS
Lists installed software on one or more Windows computers, straight from the registry.
.DESCRIPTION
Reads the Uninstall keys (64-bit, 32-bit, and optionally the current user's) and returns one
object per application with name, version, publisher, install date and architecture. Works
locally, or remotely over PowerShell remoting. Doesn't touch Win32_Product.
.PARAMETER ComputerName
One or more computers. Defaults to this one. Remote computers need PowerShell remoting enabled.
.PARAMETER Name
Wildcard filter on the application name, for example '*Chrome*'. Default: everything.
.PARAMETER IncludeUser
Also read HKEY_CURRENT_USER, for per-user installs. Only covers the account running the query.
.PARAMETER IncludeSystemComponents
Include entries flagged as system components or updates, which Programs and Features hides.
.PARAMETER Credential
Credentials for the remote connection.
.EXAMPLE
.\Get-InstalledSoftware.ps1 -Name '*Java*'
.EXAMPLE
.\Get-InstalledSoftware.ps1 -ComputerName PC-0142, PC-0187 | Export-Csv .\software.csv -NoTypeInformation
#>
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('CN', 'DNSHostName')]
[string[]]$ComputerName = $env:COMPUTERNAME,
[string]$Name = '*',
[switch]$IncludeUser,
[switch]$IncludeSystemComponents,
[pscredential]$Credential
)
begin {
# This block runs on the target machine, so it can't use anything from outside it except its arguments.
$collector = {
param([string]$NameFilter, [bool]$User, [bool]$System)
$keys = @(
@{ Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'; Arch = '64-bit' }
@{ Path = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'; Arch = '32-bit' }
)
if (-not [Environment]::Is64BitOperatingSystem) { $keys[0].Arch = '32-bit'; $keys = @($keys[0]) }
if ($User) { $keys += @{ Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'; Arch = 'User' } }
foreach ($key in $keys) {
Get-ItemProperty -Path $key.Path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and $_.DisplayName -like $NameFilter } |
Where-Object { $System -or (-not $_.SystemComponent -and -not $_.ParentKeyName) } |
ForEach-Object {
$installed = $null
if ($_.InstallDate -match '^\d{8}$') {
$installed = [datetime]::ParseExact($_.InstallDate, 'yyyyMMdd', $null)
}
[pscustomobject]@{
ComputerName = $env:COMPUTERNAME
Name = $_.DisplayName.Trim()
Version = $_.DisplayVersion
Publisher = $_.Publisher
InstallDate = $installed
Architecture = $key.Arch
UninstallString = $_.UninstallString
}
}
}
}
}
process {
foreach ($computer in $ComputerName) {
$isLocal = $computer -in @('.', 'localhost', $env:COMPUTERNAME)
try {
$argsList = @($Name, $IncludeUser.IsPresent, $IncludeSystemComponents.IsPresent)
$apps = if ($isLocal) {
& $collector @argsList
}
else {
$remote = @{ ComputerName = $computer; ScriptBlock = $collector; ArgumentList = $argsList; ErrorAction = 'Stop' }
if ($Credential) { $remote.Credential = $Credential }
Invoke-Command @remote | Select-Object * -ExcludeProperty PSComputerName, RunspaceId, PSShowComputerName
}
Write-Verbose "$computer : $(@($apps).Count) application(s)"
$apps | Sort-Object Name, Architecture
}
catch {
Write-Warning "$computer : $($_.Exception.Message)"
}
}
}
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-ComputerName | string[] | $env:COMPUTERNAME | One or more computers. Takes pipeline input, including objects from Get-ADComputer. |
-Name | string | * | Wildcard filter on the application name, like '*Chrome*'. |
-IncludeUser | switch | — | Also read HKEY_CURRENT_USER for per-user installs. Only covers the account doing the reading. |
-IncludeSystemComponents | switch | — | Show entries that Programs and Features hides, like runtimes flagged as system components and child updates. |
-Credential | pscredential | — | Credentials for remote computers. |
Run it
Everything on this machine.
.\Get-InstalledSoftware.ps1Is Java anywhere on these three machines, and which version?
.\Get-InstalledSoftware.ps1 -ComputerName PC-0142, PC-0187, PC-0203 -Name '*Java*' | Select-Object ComputerName, Name, VersionA software list for every computer in an OU, saved to CSV.
Get-ADComputer -Filter * -SearchBase 'OU=Workstations,DC=contoso,DC=com' | .\Get-InstalledSoftware.ps1 | Export-Csv .\software.csv -NoTypeInformationWhat got installed in the last 30 days?
.\Get-InstalledSoftware.ps1 | Where-Object InstallDate -gt (Get-Date).AddDays(-30)What you'll see
ComputerName Name Version Publisher InstallDate Architecture
------------ ---- ------- --------- ----------- ------------
PC-0142 7-Zip 24.08 (x64) 24.08 Igor Pavlov 3/14/2026 64-bit
PC-0142 Google Chrome 129.0.6668.90 Google LLC 9/24/2026 64-bit
PC-0142 Microsoft Visual C++ 2015-2022 ... 14.40.33810.0 Microsoft Corporation 1/9/2026 32-bit
PC-0142 Notepad++ (64-bit x64) 8.7 Notepad++ Team 5/2/2026 64-bit
PC-0142 Zoom Workplace 6.2.3 Zoom Communications User
How it works
- Pick the right keys. On 64-bit Windows, 64-bit apps register under
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstalland 32-bit apps under theWOW6432Nodecopy of it. The script reads both and tags each result with its architecture.-IncludeUseradds theHKCUversion. - Filter like Programs and Features does. Entries without a display name are skipped. So are ones marked
SystemComponentor with aParentKeyName(usually child updates), unless you ask for them with-IncludeSystemComponents. - Run it where the data is. The collector is a single script block. Locally it just runs; for remote computers it goes through
Invoke-Command, so the registry is read on the target and only the results come back. - One bad machine doesn't stop the run. Each computer has its own
try. An offline machine gets a warning and the script moves on to the next one.
Take it further
- Fan out faster. The script visits one computer at a time, which keeps the error handling simple. For hundreds of machines, lift the script block out and call
Invoke-Commandonce with the whole list; it works through them 32 at a time by default. - Compare two machines.
Compare-Object (.\Get-InstalledSoftware.ps1 -ComputerName PC-0142) (.\Get-InstalledSoftware.ps1 -ComputerName PC-0187) -Property Nameshows what one has that the other doesn't. Great for "it works on her machine." - Pull the uninstall command. The
UninstallStringis in every object. It's what Windows runs when you click Uninstall, which makes it a good starting point for a cleanup script.
Things that'll trip you up
- Don't use Win32_Product for this. It's the first thing people reach for, and it's slow, only sees MSI installs, and makes Windows Installer run a consistency check on every MSI package, which can trigger repairs. The registry is faster and doesn't change anything.
- Per-user installs are hit and miss. Apps like Zoom or VS Code can install into a single user's profile and only register in that user's HKCU. -IncludeUser reads the hive of whoever's running the script, which remotely is you, not the person who uses the machine.
- Store apps aren't here. Microsoft Store and other MSIX/AppX packages don't use the Uninstall keys. Get-AppxPackage -AllUsers covers those.
- InstallDate is often missing. Plenty of installers never write it, and some write it in the wrong format. The script only converts proper yyyyMMdd values and leaves the rest blank rather than guessing.