~/2025/11/05/powershell-windows-11-strip-preinstalled-apps-from-a-deployment-image.md
PowerShell: Windows 11 – Strip Preinstalled Apps from an Image
--- author: Tom Lasswell date: read: 5 min in: [ps, scripts] tags: [powershell, windows-11, debloat, gpo, intune] ---
$ grep -n '^#' post.md
Every Windows 11 image ships with a set of provisioned apps that show up for every new user profile whether anyone asked for them or not: a game bar overlay, a "get started" tile, a weather app, a solitaire collection nobody in a business environment needs. Removing them one at a time through Settings only fixes the current profile; the provisioned package is still staged for the next person who signs in. This script strips a configurable list of apps either from an offline image mounted with Mount-WindowsImage or from a running system, so the packages never get provisioned in the first place.
Offline is the path I prefer. Microsoft's Sysprep troubleshooting article points out that the "installed for a user, but not provisioned for all users" failure doesn't occur when you service an offline image, because deprovisioning offline clears the package for every user, including the one running the command. On a running system you have to remove the per-user installs as well, which the script does. (If you're debloating a reference VM you're about to sysprep instead, use the golden image version.)
Requirements
- Windows 10 or 11 with the in-box
DismandAppxPowerShell modules, run from an elevated Windows PowerShell 5.1 session. - Offline mode:
install.wimcopied to a writable local disk (not mounted read-only from the ISO), an empty mount folder, and free space for the mounted image. - Online mode: test with
-WhatIffirst.Remove-AppxPackage -AllUsersremoves the app for every account on the machine and needs administrator rights. - DISM writes its log to
%WINDIR%\Logs\Dism\dism.logby default, which is where to look when a removal fails.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
AppxNamePattern | string | No | Package name prefixes to match and remove. Defaults to a consumer-app list (see script). |
ImagePath | string | No | Root folder of an offline Windows image already mounted with Mount-WindowsImage. Omit to operate on the running system. |
Usage
Find the index of the edition you deploy. An install.wim from Microsoft's media holds several editions:
Get-WindowsImage -ImagePath "C:\Images\install.wim" | Format-Table ImageIndex, ImageName
Mount it, preview the removals, strip the default list, then commit and unmount:
New-Item -Path "C:\Mount\Win11" -ItemType Directory -Force | Out-Null
Mount-WindowsImage -ImagePath "C:\Images\install.wim" -Index 3 -Path "C:\Mount\Win11"
.\Remove-PreinstalledApps.ps1 -ImagePath "C:\Mount\Win11" -WhatIf
.\Remove-PreinstalledApps.ps1 -ImagePath "C:\Mount\Win11"
Dismount-WindowsImage -Path "C:\Mount\Win11" -Save
Sample output from the offline run:
Found 17 matching provisioned package(s) in C:\Mount\Win11.
Removed provisioned package: Clipchamp.Clipchamp
Removed provisioned package: Microsoft.BingNews
Removed provisioned package: Microsoft.BingWeather
...
Removed provisioned package: Microsoft.ZuneMusic
Remaining matching provisioned packages: 0
If something goes wrong, throw the changes away instead of saving them:
Dismount-WindowsImage -Path "C:\Mount\Win11" -Discard
Remove only a custom list of packages from the running system:
.\Remove-PreinstalledApps.ps1 -AppxNamePattern "Microsoft.ZuneMusic", "Microsoft.ZuneVideo", "Clipchamp.Clipchamp"
Script
<#
.SYNOPSIS
Removes provisioned Windows 11 apps from a running system or an offline mounted image.
.DESCRIPTION
Matches provisioned AppX packages against a list of name prefixes and removes them,
either from an offline image already mounted with Mount-WindowsImage (which clears
the package for all users of that image) or from the running system. Online, it also
removes the installed package for every existing user profile, because
Remove-AppxProvisionedPackage does not touch existing accounts. Supports -WhatIf.
.PARAMETER AppxNamePattern
Package name prefixes to match and remove. Defaults to a consumer-app list.
.PARAMETER ImagePath
Root folder of an offline Windows image already mounted with Mount-WindowsImage.
Omit to operate on the running system.
.EXAMPLE
.\Remove-PreinstalledApps.ps1 -WhatIf
.EXAMPLE
.\Remove-PreinstalledApps.ps1 -ImagePath "C:\Mount\Win11"
.NOTES
Author : Thomas Lasswell (https://www.techcolumnist.com)
Version : 1.0 (2025-11-05)
Requires: Windows PowerShell 5.1 (elevated), in-box Dism and Appx modules
#>
#Requires -RunAsAdministrator
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[string[]]$AppxNamePattern = @(
"Clipchamp.Clipchamp",
"Microsoft.BingNews",
"Microsoft.BingWeather",
"Microsoft.GamingApp",
"Microsoft.GetHelp",
"Microsoft.Getstarted",
"Microsoft.MicrosoftOfficeHub",
"Microsoft.MicrosoftSolitaireCollection",
"Microsoft.PowerAutomateDesktop",
"Microsoft.Todos",
"Microsoft.WindowsFeedbackHub",
"Microsoft.WindowsMaps",
"Microsoft.Xbox",
"Microsoft.YourPhone",
"Microsoft.ZuneMusic",
"Microsoft.ZuneVideo"
),
[string]$ImagePath
)
# Return $true when a name starts with any of the prefixes.
function Test-PrefixMatch {
param (
[string]$Name,
[string[]]$Prefix
)
foreach ($p in $Prefix) {
if ($Name -like "$p*") {
return $true
}
}
return $false
}
# Return the provisioned packages that match one of the name prefixes.
function Get-MatchingProvisionedPackage {
param (
[string]$ImagePath,
[string[]]$NamePattern
)
if ($ImagePath) {
$provisioned = Get-AppxProvisionedPackage -Path $ImagePath
} else {
$provisioned = Get-AppxProvisionedPackage -Online
}
@($provisioned | Where-Object { Test-PrefixMatch -Name $_.DisplayName -Prefix $NamePattern })
}
if ($ImagePath -and -not (Test-Path -Path (Join-Path -Path $ImagePath -ChildPath "Windows"))) {
throw "No Windows folder under '$ImagePath'. Mount the WIM first with Mount-WindowsImage."
}
$target = if ($ImagePath) { $ImagePath } else { "the running system" }
# Online only: remove installed copies for every account first. Removing only the
# provisioned package leaves existing profiles untouched.
if (-not $ImagePath) {
$installed = @(Get-AppxPackage -AllUsers | Where-Object { Test-PrefixMatch -Name $_.Name -Prefix $AppxNamePattern })
foreach ($package in $installed) {
if ($PSCmdlet.ShouldProcess($package.PackageFullName, "Remove installed package for all users")) {
try {
Remove-AppxPackage -Package $package.PackageFullName -AllUsers -ErrorAction Stop
Write-Host "Removed installed package: $($package.PackageFullName)"
} catch {
Write-Warning "Could not remove $($package.PackageFullName): $($_.Exception.Message)"
}
}
}
}
$toRemove = Get-MatchingProvisionedPackage -ImagePath $ImagePath -NamePattern $AppxNamePattern
Write-Host "Found $($toRemove.Count) matching provisioned package(s) in $target."
foreach ($package in $toRemove) {
if ($PSCmdlet.ShouldProcess($package.DisplayName, "Remove provisioned package")) {
try {
if ($ImagePath) {
Remove-AppxProvisionedPackage -Path $ImagePath -PackageName $package.PackageName -ErrorAction Stop | Out-Null
} else {
Remove-AppxProvisionedPackage -Online -PackageName $package.PackageName -ErrorAction Stop | Out-Null
}
Write-Host "Removed provisioned package: $($package.DisplayName)"
} catch {
Write-Warning "Could not remove $($package.DisplayName): $($_.Exception.Message) (see %WINDIR%\Logs\Dism\dism.log)"
}
}
}
$remaining = Get-MatchingProvisionedPackage -ImagePath $ImagePath -NamePattern $AppxNamePattern
Write-Host "Remaining matching provisioned packages: $($remaining.Count)"
$remaining | Select-Object DisplayName, PackageName
Notes
Remove-AppxProvisionedPackageonly affects accounts created after the removal. Microsoft's cmdlet reference is explicit: packages are not removed from existing user accounts; useRemove-AppxPackagefor those. That's why the online path removes the installed copies first.- The
Microsoft.Xboxprefix intentionally covers several packages (Microsoft.XboxGamingOverlay,Microsoft.XboxIdentityProvider,Microsoft.XboxSpeechToTextOverlay,Microsoft.Xbox.TCUI). Narrow it if any line-of-business or training app depends on Xbox sign-in. - Some packages are part of the OS rather than provisioned apps and won't appear in
Get-AppxProvisionedPackageat all. Microsoft's app overview lists them withGet-AppxPackage -PackageTypeFilter Mainfiltered onSignatureKind -eq "System". Don't chase those with this script. - Always dismount with
-Saveto commit.-Discardthrows away everything the script did.Dismount-WindowsImagerequires one or the other: called with neither, it fails parameter binding and leaves the image mounted. - A Windows feature update that rebuilds the OS can bring inbox apps back on devices that were imaged from a stripped WIM. That's the main argument for the policy below.
The policy alternative on Windows 11 24H2 and later
On Windows 11, version 24H2 or newer, Enterprise and Education editions support policy-based in-box app removal. It isn't available on Pro or Home, and Microsoft lists multi-session environments as unsupported. Removal runs at OOBE, at the first sign-in after an OS upgrade, and at the first sign-in after the policy changes, and a removed app stays blocked from reinstalling while it's selected. It also survives feature updates, which an image edit doesn't.
- Group Policy: Computer Configuration > Administrative Templates > Windows Components > App Package Deployment > Remove Default Microsoft Store packages from the system (requires current Windows 11 ADMX templates). Additional MSIX/AppX apps go in by package family name under "Specify additional package family names to remove", one per line.
- Intune / MDM: the
RemoveDefaultMicrosoftStorePackagessetting in theApplicationManagementarea of the Policy CSP, OMA-URI./Device/Vendor/MSFT/Policy/Config/ApplicationManagement/RemoveDefaultMicrosoftStorePackages, assigned to device groups.
A custom OMA-URI payload that removes the consumer apps and keeps the tools users actually need looks like this (the IDs are the static list from Microsoft's documentation):
<enabled/>
<data id="BingNews" value="true"/>
<data id="BingWeather" value="true"/>
<data id="Clipchamp" value="true"/>
<data id="GamingApp" value="true"/>
<data id="MicrosoftSolitaireCollection" value="true"/>
<data id="XboxGamingOverlay" value="true"/>
<data id="XboxIdentityProvider" value="true"/>
<data id="XboxSpeechToTextOverlay" value="true"/>
<data id="XboxTCUI" value="true"/>
<data id="WindowsFeedbackHub" value="true"/>
<data id="MicrosoftOfficeHub" value="false"/>
<data id="Copilot" value="false"/>
<data id="Photos" value="false"/>
<data id="MicrosoftStickyNotes" value="false"/>
<data id="MSTeams" value="false"/>
<data id="Todo" value="false"/>
<data id="OutlookForWindows" value="false"/>
<data id="Paint" value="false"/>
<data id="QuickAssist" value="false"/>
<data id="ScreenSketch" value="false"/>
<data id="WindowsCalculator" value="false"/>
<data id="WindowsCamera" value="false"/>
<data id="MediaPlayer" value="false"/>
<data id="WindowsNotepad" value="false"/>
<data id="WindowsSoundRecorder" value="false"/>
<data id="WindowsTerminal" value="false"/>
<data id="DynamicRemovalList" value=""/>
To confirm a device got the policy and see what it did, check the registry key it writes and the AppX deployment log. Event 606 records packages removed at first sign-in after OOBE, 614 records failed removals, and 762 records a blocked reinstall attempt:
Get-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Appx\RemoveDefaultMicrosoftStorePackages"
# Resolve the AppxDeployment-Server Operational channel, then pull the policy events.
$log = Get-WinEvent -ListLog "*AppXDeployment*" | Where-Object { $_.LogName -like "*Operational" } | Select-Object -First 1
Get-WinEvent -FilterHashtable @{ LogName = $log.LogName; Id = 606, 614, 762 } -ErrorAction SilentlyContinue |
Select-Object TimeCreated, Id, Message
Two caveats from the documentation. Don't configure it through both Intune and GPO on the same device, because whichever arrives last wins. And unselecting an app doesn't reinstall it: you have to reprovision it from the Store, media, or your deployment tool.