Updated the article to improve clarity and fix minor grammatical issues throughout the text, including prerequisites, policy creation, and script instructions. <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves # # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [ ] Timeouts are implemented and retries are limited to avoid infinite loops - [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [ ] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [ ] QA'd all new/changed functionality manually For unreleased bug fixes in a release candidate, one of: - [ ] Confirmed that the fix is not expected to adversely impact load test results - [ ] Alerted the release DRI if additional load testing is needed ## Database migrations - [ ] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [ ] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [ ] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). ## New Fleet configuration settings - [ ] Setting(s) is/are explicitly excluded from GitOps If you didn't check the box above, follow this checklist for GitOps-enabled settings: - [ ] Verified that the setting is exported via `fleetctl generate-gitops` - [ ] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - [ ] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [ ] Verified that any relevant UI is disabled when GitOps mode is enabled ## fleetd/orbit/Fleet Desktop - [ ] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [ ] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [ ] Verified that fleetd runs on macOS, Linux and Windows - [ ] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))
21 KiB
Detect and remove unwanted software installed by peripherals in Fleet
When you plug in a monitor, docking station, or printer, Windows can quietly install companion software, from monitor utilities that push trialware ads to docking station managers nobody asked for. The recent LG Monitor App story (July 2026) made headlines, but the broader problem is older: peripherals bundling junkware that lands on corporate workstations.
This guide walks through detecting and automatically removing unwanted software using Fleet policies and scripts. It covers traditional Windows installers (MSI/EXE) and Store-installed MSIX apps like the LG Monitor App.
Prerequisites
Check these before you start:
- Fleet Premium for policy automation scripts. Script automations (triggering remediation when a policy fails) require Fleet Premium. Fleet Free users can create detection policies and run scripts manually from the Fleet UI.
- Windows hosts enrolled in Fleet. Detection uses the
programstable for both MSI/EXE software and Store (MSIX) apps. - Fleet's agent updated for full Store app visibility. Store (MSIX) apps appear in the
programstable once the agent is recent enough. Support for MSIX packages arrived in osquery 5.17.0, and osquery 5.22.1 fixed a gap where provisioned or never-launched Store apps were invisible. Check the version on your hosts with a live query:SELECT version FROM osquery_info;. Update Fleet's agent (fleetd) if a host reports a version below 5.22.1. - Scripts enabled. If you use Fleet's MDM features, scripts are enabled by default. If you deploy
fleetdwithout MDM, pass the--enable-scriptsflag during installation.
Create a policy to detect unwanted software (MSI/EXE)
In Fleet, a policy passes when its query returns at least one row, and fails when it returns zero rows. To detect unwanted software, invert the logic: return a row when the software is NOT present.
- Navigate to Policies and click Add policy.
- In the Name field, enter "McAfee trial software detected."
- In the Query field, paste the following SQL:
SELECT 1 WHERE NOT EXISTS (
SELECT 1 FROM programs WHERE name LIKE '%McAfee%'
);
- In the Resolution field, add instructions for your help desk: "McAfee trial software was detected and has been automatically removed. Contact IT if you were expecting to use McAfee products on this machine."
This query returns a row (pass) when no McAfee software exists. When McAfee is found, the subquery returns results, NOT EXISTS evaluates to false, and the outer query returns zero rows, so the policy fails and triggers any attached automation.
Note: The
programstable reads the Windows Uninstall registry keys (both MSI and EXE installers that register in Add/Remove Programs). Recent versions of Fleet's agent also include Store (MSIX) apps in this table. See the Store apps section below for how to target those precisely.
Create a script to remove unwanted software
- Navigate to Controls > Scripts and click Add script.
- Name the script "Remove McAfee trial software" and set Platform to Windows.
- In the Script field, paste the following PowerShell:
# Find McAfee entries in the Windows Uninstall registry and remove them
$uninstallPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
)
$found = $false
$failed = $false
# Exit codes that indicate a successful uninstall:
# 0 = success, 3010 = success but reboot required (common with /norestart),
# 1641 = success, reboot initiated by the installer.
$successCodes = @(0, 3010, 1641)
# Parse an uninstall string into executable + arguments and run it.
# Handles quoted paths with spaces (e.g. "C:\Program Files\...\uninstall.exe" /S)
# without laundering the command through cmd.exe.
function Invoke-Uninstaller {
param(
[string]$CommandLine,
[string]$ExtraArgs = ""
)
if ($CommandLine -match '^"([^"]+)"\s*(.*)$') {
$exe = $matches[1]
$argString = $matches[2]
} else {
$parts = $CommandLine -split '\s+', 2
$exe = $parts[0]
$argString = if ($parts.Count -gt 1) { $parts[1] } else { "" }
}
if ($ExtraArgs) {
$argString = ("$argString $ExtraArgs").Trim()
}
if ([string]::IsNullOrWhiteSpace($argString)) {
$proc = Start-Process -FilePath $exe -NoNewWindow -Wait -PassThru
} else {
$proc = Start-Process -FilePath $exe -ArgumentList $argString -NoNewWindow -Wait -PassThru
}
return $proc.ExitCode
}
foreach ($basePath in $uninstallPaths) {
if (-not (Test-Path $basePath)) { continue }
Get-ChildItem $basePath | ForEach-Object {
$entry = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
if (-not $entry) { return }
if ($entry.DisplayName -notlike '*McAfee*') { return }
$found = $true
$name = $entry.DisplayName
Write-Output "Found: $name"
# MSI uninstall: rebuild the command from the product GUID.
# Matches both /I{GUID} (modify) and /X{GUID} (uninstall) registrations,
# including quoted msiexec paths like "C:\Windows\system32\msiexec.exe" /I{GUID}.
if ($entry.UninstallString -match 'MsiExec\.exe"?\s*/(I|X)\s*\{(.+?)\}') {
$guid = $matches[2]
Write-Output "Uninstalling MSI: $name"
$proc = Start-Process "MsiExec.exe" -ArgumentList "/X{$guid} /qn /norestart" -NoNewWindow -Wait -PassThru
if ($proc.ExitCode -notin $successCodes) {
Write-Output "Failed: exit code $($proc.ExitCode)"
$failed = $true
} elseif ($proc.ExitCode -eq 3010) {
Write-Output "Uninstalled: $name (reboot required)"
}
return
}
# Quiet uninstall string (preferred, already includes silent flags)
if ($entry.QuietUninstallString) {
Write-Output "Uninstalling $name (quiet)"
$code = Invoke-Uninstaller -CommandLine $entry.QuietUninstallString
if ($code -notin $successCodes) {
Write-Output "Failed: exit code $code"
$failed = $true
}
return
}
# Standard uninstall string. Caveat: may not support silent mode
if ($entry.UninstallString) {
Write-Output "Uninstalling $name (standard, may not be silent)"
$code = Invoke-Uninstaller -CommandLine $entry.UninstallString -ExtraArgs "/quiet /norestart"
if ($code -notin $successCodes) {
Write-Output "Failed: exit code $code"
$failed = $true
}
return
}
}
}
if (-not $found) {
Write-Output "No McAfee software found to remove."
exit 0
}
if ($failed) {
Write-Output "One or more uninstallations failed. Check output above."
exit 1
}
Write-Output "McAfee removal complete."
exit 0
This script reads both the 64-bit and 32-bit Uninstall registry hives, rebuilds MSI uninstall commands from the product GUID (handling both /I and /X registrations, quoted or unquoted msiexec paths), and parses EXE uninstall strings into executable-plus-arguments so paths with spaces, like anything under Program Files, launch correctly. It prefers QuietUninstallString when available and falls back to the standard UninstallString with appended quiet flags. Exit codes 3010 and 1641 (success, reboot required) are treated as success so a completed uninstall doesn't trigger a spurious retry. The script returns non-zero only on real failures, so Fleet's 3-retry mechanism triggers correctly.
- Click Save to create the script.
Warning: Test this on a single host first. Run the script manually against one machine and check the script output in that host's activity feed to confirm it uninstalls the right software before automating fleet-wide.
Note: If your organization legitimately uses McAfee/Trellix Endpoint Security on some machines, narrow the scope. Replace
*McAfee*with*McAfee Trial*or*McAfee Safe Search*to avoid removing production security software.
Note: Appending
/quiet /norestartto arbitrary EXE uninstallers doesn't always work. NSIS installers want/S, Inno Setup wants/VERYSILENT. If an uninstaller lacks quiet support, it will prompt for UI and hang in Fleet's non-interactive SYSTEM context until the script timeout. For stubborn software, use vendor-specific removal tools (for example, McAfee's MCPR tool) deployed as a Fleet software package.
Connect the policy and script with automation
- Navigate to Policies and click Manage automations.
- Find your "McAfee trial software detected" policy and select it.
- In the automation panel, choose Run script and select "Remove McAfee trial software."
- Click Save.
When any Windows host fails the McAfee policy, Fleet runs the uninstall script. The script runs up to 3 times total, retriggering each time it exits with a non-zero code. After removal, the next policy evaluation passes.
Note: Policy automations attach to policies scoped to a specific fleet (team), not global policies. If you organize hosts by fleet, create the policy at that level and attach the script there.
Detect Store apps (MSIX) like the LG Monitor App
Companion apps like the LG Monitor App and Alienware Command Center install as MSIX packages from the Microsoft Store, with no user action required. Once Fleet's agent is recent enough, MSIX packages appear in the programs table with a populated package_family_name column. Support arrived in osquery 5.17.0, and osquery 5.22.1 closed the remaining gap where apps that no user had launched were missing from inventory. The same policy pattern works here: match on the package family name rather than the display name, since it's the stable identifier.
Policy
- Navigate to Policies and click Add policy.
- In the Name field, enter "LG Monitor App (Store) detected."
- In the Query field, paste:
SELECT 1 WHERE NOT EXISTS (
SELECT 1 FROM programs
WHERE package_family_name LIKE 'LGElectronics.LGMonitorApp%'
);
This returns zero rows (fail) when the LG Monitor App package is present on the host.
Note: To find the package family name for any Store app, run
Get-AppxPackage -AllUsers | Select Name, PackageFamilyNameon an affected host, or querySELECT name, package_family_name FROM programs WHERE package_family_name != ''via Fleet live query. Vendors sometimes ship a separate installer stub package alongside the app itself. Check for related packages (for example, names containing "Installer") and widen theLIKEpattern if you find one.
Note: Querying MSIX data in
programsinvolves enumerating installed packages through the Windows Appx APIs, which is slower than the registry reads used for MSI/EXE entries. Policy evaluations run on a schedule (default hourly), so this doesn't affect end users, but live queries can take longer on hosts with many Store apps.
Removal script
- Navigate to Controls > Scripts and click Add script.
- Name the script "Remove LG Monitor App and prevent reinstall" and set Platform to Windows.
- In the Script field, paste:
$failed = $false
# Remove the LG Monitor App (Store/MSIX package) for all users.
# Wildcard also catches related packages (e.g. installer stubs).
$packages = Get-AppxPackage -Name "LGElectronics.LGMonitorApp*" -AllUsers -ErrorAction SilentlyContinue
if ($packages) {
foreach ($package in $packages) {
Write-Output "Removing $($package.Name) version $($package.Version)"
try {
$package | Remove-AppxPackage -AllUsers -ErrorAction Stop
Write-Output "App removed."
} catch {
Write-Output "App removal failed: $($_.Exception.Message)"
$failed = $true
}
}
} else {
Write-Output "LG Monitor App not found (may already be removed)."
}
# Remove the provisioned package so it isn't installed for new users
try {
$provisioned = Get-AppxProvisionedPackage -Online -ErrorAction Stop |
Where-Object { $_.DisplayName -like "*LGMonitorApp*" }
} catch {
$provisioned = $null
}
if ($provisioned) {
try {
$provisioned | Remove-AppxProvisionedPackage -Online -ErrorAction Stop | Out-Null
Write-Output "Provisioned package removed."
} catch {
Write-Output "Provisioned package removal failed: $($_.Exception.Message)"
$failed = $true
}
}
# Remove LG's driver-store delivery packages. LG ships SoftwareComponent
# driver packages matched to monitor hardware IDs whose job is to re-trigger
# the Store install. They survive app removal and re-arm the install cycle,
# so removing the app alone is not enough. Removing them does not affect
# basic monitor functionality.
try {
$lgDrivers = Get-WindowsDriver -Online -ErrorAction Stop |
Where-Object {
$_.ProviderName -like "LG Electronics*" -and
$_.ClassName -in @("SoftwareComponent", "Extension")
}
} catch {
$lgDrivers = @()
Write-Output "Could not enumerate the driver store: $($_.Exception.Message)"
}
foreach ($drv in $lgDrivers) {
Write-Output "Removing driver package $($drv.Driver) ($($drv.OriginalFileName))"
$null = pnputil /delete-driver $drv.Driver /uninstall /force
if ($LASTEXITCODE -notin @(0, 3010)) {
Write-Output "Failed to remove $($drv.Driver): pnputil exit code $LASTEXITCODE"
$failed = $true
}
}
# Belt and suspenders: block device metadata retrieval, which is one of the
# channels Windows uses to deliver companion apps for connected hardware.
# Note: this does NOT block installs triggered by driver-store packages.
# That's what the pnputil cleanup above is for.
$policyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata"
if (-not (Test-Path $policyPath)) {
New-Item -Path $policyPath -Force | Out-Null
}
$existing = (Get-ItemProperty -Path $policyPath -Name "PreventDeviceMetadataFromNetwork" -ErrorAction SilentlyContinue).PreventDeviceMetadataFromNetwork
if ($existing -eq 1) {
Write-Output "Device metadata retrieval already disabled."
} else {
Set-ItemProperty -Path $policyPath -Name "PreventDeviceMetadataFromNetwork" -Value 1 -Type DWord
Write-Output "Device metadata retrieval disabled."
}
if ($failed) { exit 1 }
exit 0
- Save the script and attach it to the Store app policy via Manage automations.
Warning: Before deploying, run
pnputil /enum-driverson an affected host and confirm the LG delivery packages' provider name and class. Adjust theProviderNamefilter if your hosts report a different string. OnlySoftwareComponentandExtensionclass packages are targeted, so display drivers are untouched.
Warning: Disabling device metadata retrieval blocks companion app installation via device metadata for ALL hardware, including legitimate ones your users may want. Scope this to specific fleets rather than applying it globally.
Note:
PreventDeviceMetadataFromNetworkis also settable through Windows MDM as an ADMX-backed policy (./Device/Vendor/MSFT/Policy/Config/DeviceInstallation/PreventDeviceMetadataFromNetwork). If you manage Windows hosts with Fleet MDM, a custom configuration profile is the more durable option: profiles are re-enforced, while a script sets the value once. The script approach above works on hosts without MDM enrollment.
Get notified when unwanted software is detected
Fleet sends webhook notifications when a host transitions to a failing policy state. Webhooks fire once per day by default, not immediately. To send Slack notifications, you need a transform layer because Fleet's JSON payload doesn't match Slack's expected {"text": "..."} format:
- Set up an incoming webhook in Tines, Zapier, or a Lambda function that transforms Fleet's payload into Slack format.
- Navigate to Policies > Manage automations, enable the webhook workflow, select your policy, and enter your transform layer's URL.
Note: Webhook notifications are available on Fleet Free. Script automations require Fleet Premium.
Adapt this for other peripheral-installed software
The same pattern works for any unwanted software:
- Docking station utilities. DisplayLink Manager, Plugable utilities, and other dock software show up in
programs. Use the policy template and scope bypublisherplus a specific product name. - Printer bundles. Canon, Epson, and Brother utilities follow the same approach. Scope by publisher to avoid hitting unrelated software.
- Monitor companion apps (Store). Alienware Command Center auto-installs via the same mechanisms. Use the same
package_family_name LIKE '...'pattern. RunGet-AppxPackage -AllUsers | Select PackageFamilyNameon an affected host to get the exact prefix.
Note: Avoid broad substring matches like
%HP%in thenamefield, since they hit unrelated programs. Scope onpublisheror use specific product names.
Verify the cleanup worked
- Navigate to Software and search for "McAfee" in the software inventory.
- Confirm the number of affected hosts drops to zero as policies evaluate.
You can also run a live query from Queries:
SELECT name, version, publisher, install_date FROM programs WHERE name LIKE '%McAfee%';
If no results return, the software is fully removed from your fleet.
Troubleshoot
Policy automation didn't trigger for hosts that were already failing.
Automations fire on transition (newly failing: no-response-to-fail or pass-to-fail). Hosts that were already failing won't trigger. To force a recheck: deselect the policy in Manage automations, click Save, then reselect it. This resets the host counts and re-triggers the automation immediately.
Store app doesn't appear in the software inventory.
Check the version reported on the host (SELECT version FROM osquery_info;). MSIX support in the programs table requires osquery 5.17.0, and apps that no user has launched, the normal state for auto-installed companion apps, require 5.22.1. On older versions, update Fleet's agent (fleetd) to bring Store apps into inventory.
Script hangs or times out on some hosts.
If an uninstaller lacks quiet/silent flags, it may prompt for UI input, which fails in Fleet's non-interactive SYSTEM context and hangs until the script timeout. The timeout is an agent option (script_execution_timeout under agent_options, default 300 seconds, maximum 18000), settable through the Fleet UI or GitOps. For stubborn software, use vendor-specific removal tools deployed as Fleet software packages.
Store app keeps reinstalling after removal.
Windows has two delivery channels that can re-trigger the install when the user reconnects the peripheral. The first is device metadata: Windows matches the hardware to a companion app listing and installs it. The PreventDeviceMetadataFromNetwork policy blocks this channel. The second is driver-store delivery: the vendor ships a SoftwareComponent driver package (via Windows Update, matched to hardware IDs) whose only job is to install the Store app. The metadata policy does NOT block this channel. The driver package must be removed from the driver store with pnputil, which the removal script above does. If the app still returns, check pnputil /enum-drivers output for vendor packages the script's filter missed, and check whether Windows Update re-delivered the driver package (block it with a driver group policy or WSUS/WUfB deferral if so).
Automation retry limit reached.
Script automations attempt up to 3 times, retriggering on non-zero exit codes. If all 3 fail, Fleet stops retrying. Check the script output in the host's activity feed to see why it failed. In Fleet Premium, set continuous_automations_enabled: true on the policy to trigger on every evaluation, including fail-to-fail transitions.
Further reading
- Policy automations. Configure webhooks and script triggers for policies.
- Run scripts on policy failure. Step-by-step for connecting policies to remediation scripts.
- Provisioned MSIX apps in software inventory (fleetdm/fleet#39065). Background on the osquery 5.22.1 fix that makes never-launched Store apps visible in
programs.