~/2025/11/05/powershell-windows-11-strip-preinstalled-apps-from-a-deployment-image.md

PowerShell: Windows 11 – Strip Preinstalled Apps from an Image

---
author: 
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 Dism and Appx PowerShell modules, run from an elevated Windows PowerShell 5.1 session.
  • Offline mode: install.wim copied 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 -WhatIf first. Remove-AppxPackage -AllUsers removes the app for every account on the machine and needs administrator rights.
  • DISM writes its log to %WINDIR%\Logs\Dism\dism.log by default, which is where to look when a removal fails.

Parameters

NameTypeRequiredDescription
AppxNamePatternstringNoPackage name prefixes to match and remove. Defaults to a consumer-app list (see script).
ImagePathstringNoRoot 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:

powershell
Get-WindowsImage -ImagePath "C:\Images\install.wim" | Format-Table ImageIndex, ImageName

Mount it, preview the removals, strip the default list, then commit and unmount:

powershell
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:

text
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:

powershell
Dismount-WindowsImage -Path "C:\Mount\Win11" -Discard

Remove only a custom list of packages from the running system:

powershell
.\Remove-PreinstalledApps.ps1 -AppxNamePattern "Microsoft.ZuneMusic", "Microsoft.ZuneVideo", "Clipchamp.Clipchamp"

Script

powershell
<#
.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-AppxProvisionedPackage only affects accounts created after the removal. Microsoft's cmdlet reference is explicit: packages are not removed from existing user accounts; use Remove-AppxPackage for those. That's why the online path removes the installed copies first.
  • The Microsoft.Xbox prefix 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-AppxProvisionedPackage at all. Microsoft's app overview lists them with Get-AppxPackage -PackageTypeFilter Main filtered on SignatureKind -eq "System". Don't chase those with this script.
  • Always dismount with -Save to commit. -Discard throws away everything the script did. Dismount-WindowsImage requires 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 RemoveDefaultMicrosoftStorePackages setting in the ApplicationManagement area 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):

xml
<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:

powershell
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.

Source