Home / Writing / Windows

Cleaning Up Stale AppX Registrations Without Removing the Current App

2026.10.08 · Windows
Cleaning Up Stale AppX Registrations Without Removing the Current App

A vulnerability scanner finds an old codec or image extension under C:\Program Files\WindowsApps. The Microsoft Store says the app is current. The active user has the latest version. The finding returns after the next scan.

That combination is possible because a packaged app is not represented by one device-wide installation record. Windows can have a current registration for one user, an older registration for another user who no longer signs in, a provisioned package for future profiles and several versioned payload folders on disk. This coexistence is by design: AppX registration and servicing are per user. The states are related, but they are not interchangeable evidence of what is installed or vulnerable for every user.

Microsoft documents this scenario in KB5011324. I wrote Remove-StaleAppxPackage for the case where an administrator has already identified the affected package and minimum acceptable version, and needs to remove only the outdated per-user registrations.

Four states that look like one application

Before removing anything, separate the evidence:

  • Per-user registration: the package registered for a particular user profile. Get-AppxPackage -AllUsers exposes these registrations and their user information.
  • Provisioned package: the package offered to new user profiles. An old provisioned version can introduce the same old registration again when another user signs in.
  • Payload files: versioned directories under WindowsApps. Removing a registration does not guarantee that its folder disappears immediately.
  • Scanner finding: the scanner's interpretation of a package name, version or file on disk. Its detection rule is evidence to investigate, not automatically the correct removal boundary.

This is why deleting a folder from WindowsApps is the wrong starting point. It bypasses the package manager and the registration model. It can also turn a scanner finding into a broken application or dependency.

Microsoft's guidance begins with identifying the profiles that still hold the old version. Letting those users sign in so the app updates is the least disruptive path. Deleting genuinely stale profiles is another option. Package removal is useful when those approaches are not practical and the scope is understood.

Define stale instead of guessing

For this script, stale has a deliberately narrow definition: a per-user registration whose package version is below the configured minimum for an exact package name. It does not infer vulnerability from the folder name, query a vulnerability feed or decide that an application is unused.

The built-in package table contains example thresholds for several inbox image and video extensions and Paint. It is not a complete or current vulnerability list. Align the names and minimum versions with the scanner finding and your validated remediation target before deployment.

The comparison uses the .NET [version] type. That small detail matters: comparing the strings 1.0.9.0 and 1.0.10.0 lexically can produce the wrong order.

Inspect first

The script requires elevated 64-bit Windows PowerShell 5.1. Start with a dry run and inspect the per-user detail:

$result = .\Remove-StaleAppxPackage.ps1 -WhatIf
$result.Details |
  Format-Table Name, Version, MinimumVersion, User, InstallState, Outcome

-WhatIf still performs preflight checks, reads provisioned packages, enumerates user registrations and writes the log. It does not remove registrations. It also skips the post-removal verification pass, so the summary's Remaining value is zero by design during a dry run. Use Details and the WhatIf count to review proposed work; do not interpret Remaining = 0 as verification.

To check a controlled list, pass a hashtable:

$minimum = [ordered]@{
  'Microsoft.VP9VideoExtensions' = '1.0.52781.0'
  'Microsoft.WebMediaExtensions' = '1.0.62192.0'
  'Microsoft.HEVCVideoExtension' = '2.1.1803.0'
}

.\Remove-StaleAppxPackage.ps1 `
  -MinimumVersion $minimum `
  -WhatIf

A non-empty supplied hashtable replaces the built-in table; it is not merged with it. An empty table restores the defaults, so do not use an empty override as a no-op. Use exact package names without wildcards: the script does not validate names or reject wildcard input. The version values must parse as versions. The script validates every threshold before attempting a removal, so one invalid version stops the run during preflight.

Remove one registration at a time

For each configured package, the script:

  1. Checks the provisioned package inventory and warns when its version is also below the minimum.
  2. Runs Get-AppxPackage -AllUsers -Name for the exact package.
  3. Selects every per-user registration below the minimum.
  4. Skips SYSTEM, LOCAL SERVICE and NETWORK SERVICE registrations.
  5. Calls Remove-AppxPackage -Package <full name> -User <SID> for each remaining target.
  6. Re-queries the configured packages and reports outdated registrations that remain.

The -User parameter is the important boundary. It removes the package from the specified user account, not every user on the device. A user who already has a current version is outside the target set.

This is still a removal. There is no automatic rollback. Review dependencies and bundles on a pilot device, and use -Confirm when an interactive per-registration prompt is appropriate:

.\Remove-StaleAppxPackage.ps1 `
  -MinimumVersion @{
    'Microsoft.VP9VideoExtensions' = '1.0.52781.0'
  } `
  -Confirm

Without -WhatIf or -Confirm, the script is unattended and does not prompt. That is useful for management systems, but it also means the package table is the change approval. Do not deploy the example thresholds as though they were a live security catalog.

A registration warning is not a provisioning repair

Remove-StaleAppxPackage does not remove or update provisioned packages. If it finds an old provisioned copy, it logs a warning because a new profile can register that version later. Treat that as a separate servicing problem. A successful per-user cleanup does not correct the source offered to future users.

It also does not remove a package that has no user registration. That is intentional: the script's unit of work is an outdated user registration. A folder with no matching registration belongs to a different investigation.

Expect the folder to outlive the registration

Microsoft notes that AppX registration removal and payload cleanup are not one synchronous operation. Files can remain in WindowsApps because cleanup is asynchronous and best-effort. Those leftover files are described as inert and low risk, but a file-based scanner can still report them immediately after remediation.

The script verifies registration state by calling Get-AppxPackage again. It does not claim that every old folder has already disappeared. Preserve that distinction in remediation reporting:

  • Removed: the targeted per-user registration was removed.
  • Remaining: a non-service user still has a registration below the configured minimum.
  • Old folder still visible: payload cleanup may still be pending, or the folder may need separate correlation.
  • Scanner still reports vulnerable: rerun after cleanup has had time to complete and inspect what the scanner actually matched.

Deploy with the execution context in mind

For an Intune platform script, run as SYSTEM and select the 64-bit PowerShell host. Platform scripts cannot pass the -MinimumVersion hashtable, so edit the ordered table in the script before upload. Run the edited script with -WhatIf on a pilot group first.

ConfigMgr, RMM and scheduled-task wrappers can use -File when no hashtable argument is needed. To pass -MinimumVersion inline from cmd.exe or another deployment tool, use powershell.exe -Command; -File passes the hashtable as a string and parameter binding fails.

A completed run returns one summary object with counts for removed, failed, skipped, dry-run and remaining registrations, plus a Details array. Preflight failures exit before that summary is produced. Exit code 0 means no user-registration enumeration or removal failure and no outdated non-service registration remained after an execute run. Exit code 1 covers preflight, user-registration enumeration, removal or verification failure. After successful preflight, a dry run returns zero unless user-registration enumeration fails; again, that is not a successful removal result. Provisioned package inventory and log-write failures produce warnings and can still leave the exit code at zero. Read those warnings alongside the summary.

Logs append under the Intune Management Extension log directory by default, so they can be included in collected diagnostics. They contain package names, versions, SIDs, usernames and error details. Treat the log as internal device evidence and provide rotation through your normal log-management process.

Keep the remediation claim narrow

The useful claim is not "the vulnerable app was deleted." It is: the device was checked against an approved package/version table; registrations below that minimum were removed for the identified users; current users were left alone; provisioning warnings and remaining registrations were recorded.

That wording survives the awkward parts of the AppX model. It also gives the next scanner result somewhere precise to land: registration, provisioning, payload cleanup or detection logic.

The current script and deployment notes are in the StaleAppxPackage directory. Microsoft documents the underlying scenario in KB5011324 and the relevant cmdlets in Get-AppxPackage and Remove-AppxPackage.