Remove Zoom User Install (Package)
A Tanium package that removes the per-user copy of Zoom from Windows endpoints, workstations and servers alike. It cleans every user profile on the endpoint, whether the user is signed in or not.
The per-user copy is the one a user gets by clicking a Zoom meeting link and running the installer that downloads. It installs into the user's profile instead of Program Files, so it does not need administrator rights, and tools that patch software as SYSTEM cannot update it. Old per-user copies tend to sit in profiles for years.
The system-wide copy in C:\Program Files\Zoom or C:\Program Files (x86)\Zoom is never changed, and the package does not install anything. The script only checks whether it is there, to decide which folders in the profile are safe to delete. See Per-user and system-wide on the same endpoint.
Vulnerability findings
A common reason to run this is a vulnerability finding on an outdated Zoom. Scanners, Tanium Comply included, check for Zoom.exe in each user profile separately from the system-wide copy. Removing the per-user copy clears that finding. If users still need Zoom, they need another copy that is kept up to date, because this package takes theirs away.
Test status
The package was run through Tanium on two endpoints: one with only a per-user copy, open at the time (removed, exit 0), and one with only the system-wide copy (nothing touched, exit 0). An endpoint with both copies, and a signed-out profile, have not been tested yet. Test those on one endpoint before deploying widely. See Test results.
Which copy of Zoom is which
Both copies create AppData\Roaming\Zoom and AppData\Local\Zoom in each user's profile. The difference is what is inside them.
| Copy | Program files | What it puts in the profile | Removed by this package |
|---|---|---|---|
| Per-user (installed from a meeting link or the download page without admin rights) | C:\Users\<user>\AppData\Roaming\Zoom\bin\Zoom.exe (the folder can be bin_00 after an update) |
Program folders (bin or bin_00, uninstall) and user data (data, logs, reports, plugin) |
Yes |
| System-wide, 64-bit MSI | C:\Program Files\Zoom\bin\Zoom.exe |
User data only (data, logs, reports, plugin), no .exe files |
No |
| System-wide, 32-bit MSI | C:\Program Files (x86)\Zoom\bin\Zoom.exe |
User data only, no .exe files |
No |
The user data folders hold the user's Zoom settings, sign-in, and local chat history. A Zoom.exe anywhere under the two Zoom folders in a profile means a per-user copy. The system-wide copy never puts one there.
The per-user copy also writes to the user's own registry:
- An uninstall entry at
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall\ZoomUMX, with an uninstall command of"C:\Users\<user>\AppData\Roaming\Zoom\uninstall\Installer.exe" /uninstall. Inventory, including Installed Applications, reads the installed version from here. - Sometimes a
Zoomvalue underHKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run, if the user turned on starting Zoom at sign-in. A fresh per-user install in testing did not create one.
Deleting the Zoom folder by hand removes the files but leaves both registry entries, and inventory keeps reporting Zoom as installed. The package removes both.
Find the endpoints
In Interact, ask:
Get Computer Name and Installed Applications from all machines with Installed Applications contains "Zoom"
Installed Applications shows both the per-user and the system-wide copy, and their names can look the same. To find only per-user copies, use the Find File by Name sensor:
Any path under C:\Users\<user>\AppData\Roaming\Zoom is a per-user copy.
To check whether Zoom is open on those endpoints, ask:
Get Computer Name and Running Processes from all machines with Running Processes contains "Zoom.exe"
Zoom open during a meeting is the case that matters. See Close Zoom.
Per-user and system-wide on the same endpoint
Both copies on one endpoint usually means the per-user copy was installed first. When a system-wide copy is already there, the per-user installer (ZoomInstallerFull.exe) does not install anything. It opens the system-wide Zoom instead. This was confirmed in testing.
Because both copies use the same two folders in the profile, deleting those folders whole would also wipe the data of users on the system-wide copy. The script decides what to delete like this:
| Endpoint | Profile | What is deleted from the profile |
|---|---|---|
| Has a system-wide Zoom | Has a per-user Zoom.exe |
Only the per-user program parts: bin, bin_*, uninstall, any other subfolder that contains an .exe, and .exe files at the top of either Zoom folder. The data folders stay, because the system-wide copy uses them. |
| No system-wide Zoom | Has a per-user Zoom.exe |
Both AppData\Roaming\Zoom and AppData\Local\Zoom. Nothing else on the endpoint uses them. |
| Either | No per-user Zoom.exe (data folders only) |
Nothing. This is a system-wide user, or a profile left over from a Zoom that was uninstalled. |
The check for a system-wide copy looks for Zoom.exe in C:\Program Files\Zoom\bin and C:\Program Files (x86)\Zoom\bin. It is read only. The first line of the action log after the parameter line says which case the endpoint is in.
A Zoom that is open from Program Files is not closed and does not block anything, because nothing it uses is deleted.
What the script does
- Finds every user profile on the endpoint: profiles Windows knows about (local, domain, and work or school accounts), plus folders left in
C:\Usersby accounts that were deleted. - Checks whether a system-wide Zoom is installed, and for each profile, whether it has a per-user
Zoom.exe. That decides what is deleted, as in the table above. - Checks whether Zoom is running from anything that will be deleted. If it is and Close Zoom is
No, the profile is skipped. If it isYes, Zoom is closed. -
Cleans the user's registry:
- Signed in: works on the user's loaded registry directly.
- Signed out: loads the user's
NTUSER.DATasHKEY_USERS\ZOOM_CLEANUP, cleans it, and unloads it.
In both cases it deletes the
ZoomUMXuninstall entry, any other uninstall entry whose uninstall command points intoAppData\Roaming\Zoom, and any Run value that points there. Every signed-out profile's registry is checked, not only profiles that still have the folder, so entries left behind by a manual delete are caught too. 5. Deletes the shortcuts in the user's Start menu and on the desktop that point into what is being deleted, and the Start menu folder if that leaves it empty. Shortcuts that point anywhere else, including to the system-wide copy, are left alone. 6. Deletes the folders from step 2. 7. Checks that noZoom.exeis left in the profile, and prints a summary.
What it leaves behind
- Zoom's link handlers (what opens
zoommtg:links) can stay registered for the user. They point to a file that no longer exists, so nothing runs. - Shortcuts outside the user's Start menu and desktop, for example a desktop redirected to OneDrive, are not checked.
- On endpoints with a system-wide Zoom, the user data folders of users who had a per-user copy. The system-wide copy keeps using them.
- The system-wide copy, the Zoom Outlook plug-in, and anything else installed for all users.
Parameter
| Parameter | Passed as | Required | What to enter |
|---|---|---|---|
| Close Zoom | $1 |
No | No (or blank) skips users who have Zoom open. Yes closes Zoom for them first. |
Close Zoom = Yes
The script ends every Zoom process running from a user's per-user copy, without asking. Anyone in a meeting is dropped from it. On a Remote Desktop session host, this applies to every signed-in user at once. Use Yes on servers, or on workstations outside working hours.
Exit codes
When more than one applies, 6 is returned first, then 1, then 3.
| Exit code | Meaning |
|---|---|
0 |
Removed from every profile that had it, or no profile has it. |
1 |
At least one profile could not be fully cleaned: a folder could not be deleted, a registry entry could not be deleted, or a hive could not be loaded. The action log shows which. |
2 |
Close Zoom has a value other than No, Yes, or blank. Nothing changed. |
3 |
At least one user was skipped because Zoom was open. Deploy again later, or with Close Zoom set to Yes. |
5 |
The script is not running as SYSTEM or an administrator. Nothing changed. |
6 |
A registry hive could not be unloaded. Restart the endpoint before that user signs in. |
Registry hive unload
The script loads each signed-out user's registry file while that user is away. If a hive does not unload (for example, another program opened it at the same moment), the script stops and returns exit code 6. If that user signs in before the endpoint restarts, Windows can give them a temporary profile. When you see exit code 6, restart the endpoint before the user signs in.
Create the package
Menu labels can differ slightly between Tanium versions.
- Save the script at the bottom of this page as
Remove-ZoomUserInstall.ps1. - In the Tanium Console, go to Administration > Content > Packages.
- Click Create Package.
- Fill in the fields:
- Package Display Name:
Remove Zoom User Install(add your team's prefix if you use one). - Description:
Removes the per-user copy of Zoom (AppData\Roaming\Zoom\bin) from every profile, signed in or not, plus its ZoomUMX uninstall entry and shortcuts. If a system-wide Zoom is installed, user data folders are kept. The system-wide copy is never changed. Close Zoom = Yes drops live meetings. Exit 0 = removed or nothing to do. Exit 1 = something remains. Exit 3 = skipped, Zoom open. See the action log. - Content Set: the content set your team uses for custom content.
- Command:
- Command Timeout:
10minutes. Each signed-out profile takes a few seconds for the registry load and unload, so endpoints with many profiles take longer.
- Package Display Name:
- Under Files, click Add, choose Local File, and upload
Remove-ZoomUserInstall.ps1. - Expand Parameters. Under Parameter Inputs, add a Drop-Down List parameter. It shows in the list as
$1.- Label:
Close Zoom - Provide Help Text: select it and enter
No skips users who have Zoom open. Yes closes Zoom first and drops any meeting in progress. - Values: add two entries, in this order:
No, thenYes.
- Label:
- In the Preview panel on the right, confirm you see Close Zoom with
Noselected. - Click Save.
Deploy the package
- In Interact, ask the Find File by Name question from Find the endpoints.
- Select the computers in the results grid.
- Click Deploy Action.
- In Deployment Package, search for and select
Remove Zoom User Install. - Set Close Zoom:
- Servers:
Yes, unless the server is a Remote Desktop session host in use. - Workstations during working hours:
No.
- Servers:
- Under the schedule, leave it as a one-time action for the first run.
- Click Show preview to continue, review the targets, then click Deploy Action.
For endpoints that return exit code 3, deploy again later, or deploy again after hours with Close Zoom set to Yes.
View the results
One endpoint
- Open the action from Administration > Actions > Action History.
- Select an endpoint.
- Open its action log. The script output appears between the
Command Lineline and theCompletedline. The last lines are the summary:
Summary:
Removed: <profile paths>
Failed: <profile paths, or none>
Skipped, Zoom open: <profile paths, or none>
Each profile also gets its own lines, such as deleted, removed uninstall entry 'ZoomUMX', Zoom is open, skipped, or still present, so a failure shows which file or registry entry was the problem.
Example, endpoint with only a per-user copy, Zoom open, Close Zoom set to Yes:
2026-10-20 13:43:08 [INFO] Close Zoom parameter: 'Yes'
2026-10-20 13:43:09 [INFO] No system-wide Zoom. Profiles with a per-user copy have both Zoom folders deleted.
2026-10-20 13:43:12 [INFO] C:\Users\jdoe : found C:\Users\jdoe\AppData\Roaming\Zoom\bin\Zoom.exe version 7.2.1.48556
2026-10-20 13:43:12 [INFO] C:\Users\jdoe : closed Zoom.exe (PID 9984)
2026-10-20 13:43:12 [INFO] C:\Users\jdoe : closed Zoom.exe (PID 7340)
2026-10-20 13:43:15 [INFO] C:\Users\jdoe : removed uninstall entry 'ZoomUMX'
2026-10-20 13:43:15 [INFO] C:\Users\jdoe : deleted C:\Users\jdoe\AppData\Roaming\Zoom
2026-10-20 13:43:16 [INFO] C:\Users\jdoe : deleted C:\Users\jdoe\AppData\Local\Zoom
2026-10-20 13:43:16 [INFO] C:\Users\jdoe : removed
2026-10-20 13:43:18 [INFO] Summary:
2026-10-20 13:43:18 [INFO] Removed: C:\Users\jdoe
2026-10-20 13:43:18 [INFO] Failed: none
2026-10-20 13:43:18 [INFO] Skipped, Zoom open: none
Example, endpoint with only the system-wide copy. Nothing is a per-user copy, so nothing is touched:
2026-10-20 13:43:13 [INFO] Close Zoom parameter: 'Yes'
2026-10-20 13:43:14 [INFO] System-wide Zoom found (C:\Program Files\Zoom\bin\Zoom.exe). User data folders are kept; only per-user program folders are deleted.
2026-10-20 13:43:17 [INFO] Summary:
2026-10-20 13:43:17 [INFO] Removed: none
2026-10-20 13:43:17 [INFO] Failed: none
2026-10-20 13:43:17 [INFO] Skipped, Zoom open: none
Many endpoints
Ask a question with the built-in Tanium Action Log sensor, using the action ID from the top of the log:
Confirm the copy is gone
Ask the Find File by Name question from Find the endpoints again. The endpoint should return NOT FOUND. The Installed Applications sensor can return a cached result for a while after the change, so an endpoint that still shows Zoom right after the action may clear on its own. If the removal was for a vulnerability finding, run the Comply assessment again afterward.
Requirements
- Windows 10, Windows 11, or Windows Server endpoints with Windows PowerShell 5.1.
- The action must run as SYSTEM, which is the Tanium default. Without administrator rights, the script stops with exit code
5. - A Tanium account with permission to create packages in a content set and deploy actions to the target computers.
Things to know
- Zoom is closed by path, not by name. Only processes running from what is about to be deleted are closed. A system-wide Zoom that is open is not touched.
- A per-user copy is found by
Zoom.exe, not by the folders. The folders alone do not mean a per-user copy, since the system-wide copy creates them too. - It runs as 64-bit. If the Tanium client starts 32-bit PowerShell, the script restarts itself as 64-bit.
- Parameters arrive URL-encoded. The script decodes Close Zoom before checking it.
- Users can install it again. Any user can click a meeting link and get a new per-user copy. To stop that, block
%OSDRIVE%\Users\*\AppData\Roaming\Zoom\bin\*with Tanium Enforce or AppLocker. - Deleting the folder by hand is not enough. It leaves the uninstall entry behind, and inventory keeps reporting Zoom. Use this package instead.
Test results
| Case | Expected | Result |
|---|---|---|
| Parser check, Windows PowerShell 5.1 | No errors | Pass |
| Endpoint with system-wide Zoom 7.2, profile with data folders only, detection run read only | System-wide copy found, no per-user Zoom.exe, nothing targeted |
Pass |
Simulated profiles: per-user bin or bin_00 with system-wide present |
Only bin or bin_00 and uninstall deleted, data, logs, reports, plugin kept |
Pass |
| Simulated profile: per-user copy, no system-wide Zoom | Both Zoom folders deleted | Pass |
| Simulated profile: data folders only, with or without system-wide Zoom | Nothing deleted | Pass |
| Real per-user install (7.2.1), layout and registry checked by hand | ZoomUMX entry pointing to Roaming\Zoom\uninstall, program folders bin and uninstall, same data folders as the system-wide copy |
Confirmed |
Close Zoom = Maybe |
Exit 2, nothing changed | Pass |
Close Zoom = Yes or blank, not elevated |
Value accepted, exit 5, nothing changed | Pass |
Close Zoom URL-encoded (%59es, %4eo), from 32-bit PowerShell |
Restarts as 64-bit, decoded to Yes and No, exit 5 |
Pass |
Per-user copy only (7.2.1), signed-in user, Zoom open, Close Zoom = Yes, through Tanium |
Zoom closed, ZoomUMX removed, both Zoom folders deleted, exit 0 |
Pass |
System-wide copy only (7.2), Close Zoom = Yes, through Tanium |
System-wide copy found, nothing touched, exit 0 | Pass |
| Per-user and system-wide copy on the same endpoint | Only bin and uninstall deleted, data folders kept, system-wide Zoom still opens with the user's settings |
Not tested yet |
Signed-in user, Zoom open, Close Zoom = No |
Skipped, exit 3 | Not tested yet |
| Signed-out profile, hive load, entry and folder removal | Removed, hive unloaded | Not tested yet |
| Folder deleted by hand earlier, entry still in a signed-out hive | Entry removed | Not tested yet |
Script
######################################################################
# Remove Zoom User Install (Tanium package)
#
# Tanium package command:
# cmd.exe /d /c powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File Remove-ZoomUserInstall.ps1 "$1"
#
# $1 Close Zoom No (or blank) or Yes. Yes closes Zoom for the user before
# removing it. Any meeting in progress is dropped.
#
# Runs as SYSTEM. Removes the per-user copy of Zoom (installed under
# %APPDATA%\Zoom\bin) from every user profile, signed in or not.
#
# Both the per-user copy and the system-wide (MSI) copy keep user data in
# AppData\Roaming\Zoom and AppData\Local\Zoom. Only the per-user copy puts
# program files there. So:
# - A profile counts as having the per-user copy only when a Zoom.exe is
# found under those folders. Profiles with data folders only are left alone.
# - If a system-wide Zoom is installed (Program Files\Zoom\bin\Zoom.exe),
# only the per-user program folders are deleted (bin, bin_*, uninstall, and
# any other subfolder that contains an .exe). The data folders stay, because
# the system-wide copy uses them.
# - If no system-wide Zoom is installed, both Zoom folders are deleted.
#
# For each profile with the per-user copy:
# 1. Deletes the folders above.
# 2. Deletes Start menu and desktop shortcuts that point into what was deleted.
# 3. Deletes the per-user uninstall entry (ZoomUMX) and the Zoom Run value
# from the user's registry. Signed-out users' registry hives are loaded
# offline for this, then unloaded. Every signed-out hive is checked, so
# entries left by a folder deleted by hand are cleaned too.
#
# The system-wide copy is only checked for, never changed, and nothing is
# installed.
#
# Exit codes (when more than one applies, the highest-priority code wins:
# 6, then 1, then 3):
# 0 Removed from every profile that had it, or no profile has it
# 1 At least one profile could not be fully cleaned
# 2 Invalid parameter. Nothing changed.
# 3 At least one user was skipped because Zoom was open (Close Zoom = No)
# 5 Not running as SYSTEM or an administrator. Nothing changed.
# 6 A registry hive could not be unloaded. Restart the endpoint before
# that user signs in.
######################################################################
param(
[string]$CloseZoom = ""
)
# Restart as 64-bit if the Tanium client started 32-bit PowerShell.
# The raw (still URL-encoded) value is passed on, so it is decoded only once.
if ([Environment]::Is64BitOperatingSystem -and -not [Environment]::Is64BitProcess) {
$ps64 = Join-Path $env:SystemRoot 'Sysnative\WindowsPowerShell\v1.0\powershell.exe'
$argList = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $PSCommandPath)
if ($CloseZoom) { $argList += @('-CloseZoom', $CloseZoom) }
& $ps64 @argList
exit $LASTEXITCODE
}
$ErrorActionPreference = 'Stop'
$RelZoomDirs = @('AppData\Roaming\Zoom', 'AppData\Local\Zoom')
$RelStartMenu = 'AppData\Roaming\Microsoft\Windows\Start Menu\Programs'
$RelDesktop = 'Desktop'
$RunKeyRel = 'Software\Microsoft\Windows\CurrentVersion\Run'
$UninstallKeyRel = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'
$ZoomPathMatch = '*\AppData\Roaming\Zoom\*'
$MountName = 'ZOOM_CLEANUP'
$MountPs = "Registry::HKEY_USERS\$MountName"
function Write-Log {
param ([string]$Message, [string]$Level = 'INFO')
Write-Output ("{0} [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message)
}
# Tanium URL-encodes parameter values. Decode, then strip stray whitespace and quotes.
function ConvertFrom-TaniumParam {
param ([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) { return "" }
return ([Uri]::UnescapeDataString($Value)).Trim().Trim('"').Trim()
}
function Join-OrNone {
param ([string[]]$Items)
if ($Items -and $Items.Count -gt 0) { return ($Items -join ', ') }
return 'none'
}
function Test-UnderPath {
param ([string]$Path, [string[]]$Roots)
if (-not $Path) { return $false }
foreach ($r in $Roots) {
if ($Path.StartsWith($r.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) { return $true }
}
return $false
}
# Windows PowerShell 5.1 turns native stderr into a terminating error when
# ErrorActionPreference is Stop, so reg.exe runs with Continue.
function Invoke-Reg {
param ([string[]]$Arguments)
$previous = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$out = (& reg.exe @Arguments 2>&1 | Out-String).Trim()
$code = $LASTEXITCODE
} finally {
$ErrorActionPreference = $previous
}
return [pscustomobject]@{ ExitCode = $code; Output = $out }
}
# Open registry handles keep a hive from unloading, so release them and retry.
function Dismount-UserHive {
for ($i = 1; $i -le 3; $i++) {
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
Start-Sleep -Seconds 2
$r = Invoke-Reg @('unload', "HKU\$MountName")
if ($r.ExitCode -eq 0) { return $true }
}
return $false
}
# Removes the Zoom Run value and the per-user uninstall entry under one user's
# registry root. Sets $script:StepFailed on any error. Log lines are written
# straight to output, so the function returns nothing to test.
function Remove-ZoomRegistry {
param ([string]$Root, [string]$Label)
$runKey = "$Root\$RunKeyRel"
if (Test-Path -LiteralPath $runKey) {
$skip = @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider')
$item = Get-ItemProperty -LiteralPath $runKey -ErrorAction SilentlyContinue
if ($item) {
foreach ($prop in @($item.PSObject.Properties | Where-Object { $_.Name -notin $skip -and "$($_.Value)" -like $ZoomPathMatch })) {
try {
Remove-ItemProperty -LiteralPath $runKey -Name $prop.Name
Write-Log ("{0} : removed Run value '{1}'" -f $Label, $prop.Name)
} catch {
Write-Log ("{0} : could not remove Run value '{1}'. {2}" -f $Label, $prop.Name, $_.Exception.Message) 'ERROR'
$script:StepFailed = $true
}
}
}
}
$uninstRoot = "$Root\$UninstallKeyRel"
if (Test-Path -LiteralPath $uninstRoot) {
$entries = @(Get-ChildItem -LiteralPath $uninstRoot -ErrorAction SilentlyContinue | Where-Object {
$_.PSChildName -eq 'ZoomUMX' -or "$($_.GetValue('UninstallString'))" -like $ZoomPathMatch
})
foreach ($e in $entries) {
$name = $e.PSChildName
try {
Remove-Item -LiteralPath $e.PSPath -Recurse -Force
if (Test-Path -LiteralPath "$uninstRoot\$name") { throw 'The entry is still present after the delete.' }
Write-Log ("{0} : removed uninstall entry '{1}'" -f $Label, $name)
} catch {
Write-Log ("{0} : could not remove uninstall entry '{1}'. {2}" -f $Label, $name, $_.Exception.Message) 'ERROR'
$script:StepFailed = $true
}
}
}
}
# Every Zoom.exe in the profile's Zoom folders. The system-wide copy never puts
# one there, so any hit is a per-user copy.
function Get-ZoomExe {
param ([string]$ProfilePath)
foreach ($rel in $RelZoomDirs) {
$root = Join-Path $ProfilePath $rel
if (-not (Test-Path -LiteralPath $root)) { continue }
Get-ChildItem -LiteralPath $root -Filter 'Zoom.exe' -File -Recurse -Force -ErrorAction SilentlyContinue
}
}
# Program parts of a per-user copy: bin, bin_* (Zoom alternates between bin and
# bin_00 across updates), uninstall, any other subfolder holding an .exe, and
# .exe files at the top of the Zoom folder. Data folders are not returned.
function Get-ZoomProgramItems {
param ([string]$ProfilePath)
foreach ($rel in $RelZoomDirs) {
$root = Join-Path $ProfilePath $rel
if (-not (Test-Path -LiteralPath $root)) { continue }
foreach ($item in @(Get-ChildItem -LiteralPath $root -Force -ErrorAction SilentlyContinue)) {
if (-not $item.PSIsContainer) {
if ($item.Extension -eq '.exe') { $item.FullName }
continue
}
if ($item.Name -eq 'bin' -or $item.Name -like 'bin_*' -or $item.Name -eq 'uninstall') { $item.FullName; continue }
$exe = Get-ChildItem -LiteralPath $item.FullName -Filter '*.exe' -File -Recurse -Force -ErrorAction SilentlyContinue | Select-Object -First 1
if ($exe) { $item.FullName }
}
}
}
# Deletes the given folders and files, and the shortcuts that point into them.
# Sets $script:StepFailed when a target is left in place.
function Remove-ZoomFiles {
param ([string]$ProfilePath, [string]$Label, [string[]]$Targets)
$links = @()
foreach ($rel in @($RelStartMenu, $RelDesktop)) {
$dir = Join-Path $ProfilePath $rel
if (-not (Test-Path -LiteralPath $dir)) { continue }
$recurse = ($rel -eq $RelStartMenu)
$links += @(Get-ChildItem -LiteralPath $dir -Filter '*.lnk' -File -Recurse:$recurse -Force -ErrorAction SilentlyContinue)
}
if ($links.Count -gt 0) {
$shell = New-Object -ComObject WScript.Shell
foreach ($lnk in $links) {
$target = ''
try { $target = $shell.CreateShortcut($lnk.FullName).TargetPath } catch { }
if (-not (Test-UnderPath -Path $target -Roots $Targets)) { continue }
try {
Remove-Item -LiteralPath $lnk.FullName -Force
Write-Log ("{0} : deleted {1}" -f $Label, $lnk.FullName)
$parent = Split-Path $lnk.FullName -Parent
if ((Split-Path $parent -Leaf) -like 'Zoom*' -and -not (Get-ChildItem -LiteralPath $parent -Force)) {
Remove-Item -LiteralPath $parent -Force
Write-Log ("{0} : deleted empty folder {1}" -f $Label, $parent)
}
} catch {
Write-Log ("{0} : could not delete {1}. {2}" -f $Label, $lnk.FullName, $_.Exception.Message) 'WARN'
}
}
}
foreach ($item in $Targets) {
if (-not (Test-Path -LiteralPath $item)) { continue }
try {
Remove-Item -LiteralPath $item -Recurse -Force
Write-Log ("{0} : deleted {1}" -f $Label, $item)
} catch {
Write-Log ("{0} : could not delete {1}. {2}" -f $Label, $item, $_.Exception.Message) 'ERROR'
}
if (Test-Path -LiteralPath $item) { $script:StepFailed = $true }
}
}
# --- Validate parameter -------------------------------------------------------
$closeValue = ConvertFrom-TaniumParam $CloseZoom
Write-Log "Close Zoom parameter: '$closeValue'"
if (-not $closeValue -or $closeValue -eq 'No') {
$forceClose = $false
} elseif ($closeValue -eq 'Yes') {
$forceClose = $true
} else {
Write-Log "Unknown Close Zoom value '$closeValue'. Use No (or leave it blank), or Yes." 'ERROR'
exit 2
}
# --- Elevation check ----------------------------------------------------------
$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Log "Not running as SYSTEM or an administrator. Other users' profiles cannot be changed. Nothing changed." 'ERROR'
exit 5
}
# A hive left mounted by an earlier run would block every load below.
if (Test-Path -LiteralPath $MountPs) {
Write-Log "A hive from an earlier run is still loaded as HKU\$MountName. Unloading it." 'WARN'
if (-not (Dismount-UserHive)) {
Write-Log "Could not unload HKU\$MountName. Restart the endpoint, then run again. Nothing changed." 'ERROR'
exit 6
}
}
# --- Profiles -----------------------------------------------------------------
$profiles = @(Get-CimInstance -ClassName Win32_UserProfile -Filter "Special=False" | Where-Object { $_.LocalPath } | ForEach-Object {
[pscustomobject]@{ Path = $_.LocalPath; Sid = $_.SID; Loaded = [bool]$_.Loaded }
})
# Folders left behind by deleted accounts have no registry hive, but can still hold Zoom.exe.
$profilesDir = Join-Path $env:SystemDrive 'Users'
try {
$profilesDir = [Environment]::ExpandEnvironmentVariables((Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -Name ProfilesDirectory).ProfilesDirectory)
} catch { }
$knownPaths = @($profiles | ForEach-Object { $_.Path })
$excluded = @('Public', 'Default', 'Default User', 'All Users')
foreach ($d in @(Get-ChildItem -LiteralPath $profilesDir -Directory -Force -ErrorAction SilentlyContinue)) {
if ($d.Name -in $excluded -or ($d.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $d.FullName -in $knownPaths) { continue }
$profiles += [pscustomobject]@{ Path = $d.FullName; Sid = $null; Loaded = $false }
}
# --- System-wide copy (checked only, never changed) ---------------------------
$machineExe = @(@($env:ProgramW6432, $env:ProgramFiles, ${env:ProgramFiles(x86)}) | Where-Object { $_ } |
ForEach-Object { Join-Path $_ 'Zoom\bin\Zoom.exe' } | Select-Object -Unique | Where-Object { Test-Path -LiteralPath $_ })
$keepData = $machineExe.Count -gt 0
if ($keepData) {
Write-Log ("System-wide Zoom found ({0}). User data folders are kept; only per-user program folders are deleted." -f ($machineExe -join ', '))
} else {
Write-Log "No system-wide Zoom. Profiles with a per-user copy have both Zoom folders deleted."
}
$zoomProcs = @(Get-CimInstance -ClassName Win32_Process | Where-Object { $_.ExecutablePath -like '*\AppData\*\Zoom\*' })
$removed = @()
$failed = @()
$skippedOpen = @()
$unloadFailed = $false
foreach ($prof in $profiles) {
$path = $prof.Path
$copies = @(Get-ZoomExe -ProfilePath $path)
foreach ($c in $copies) {
Write-Log ("{0} : found {1} version {2}" -f $path, $c.FullName, $c.VersionInfo.FileVersion)
}
# What to delete: nothing for data-only profiles, program folders only when
# the system-wide copy uses the data, both Zoom folders otherwise.
$targets = @()
if ($copies.Count -gt 0) {
if ($keepData) {
$targets = @(Get-ZoomProgramItems -ProfilePath $path)
} else {
$targets = @($RelZoomDirs | ForEach-Object { Join-Path $path $_ } | Where-Object { Test-Path -LiteralPath $_ })
}
}
$hasFiles = $targets.Count -gt 0
# --- Zoom open from what will be deleted ---------------------------------
$running = @($zoomProcs | Where-Object { Test-UnderPath -Path $_.ExecutablePath -Roots $targets })
if ($running.Count -gt 0) {
if (-not $forceClose) {
Write-Log ("{0} : Zoom is open, skipped. Run again later, or with Close Zoom = Yes." -f $path) 'WARN'
$skippedOpen += $path
continue
}
foreach ($p in $running) {
try {
Stop-Process -Id $p.ProcessId -Force
Write-Log ("{0} : closed {1} (PID {2})" -f $path, $p.Name, $p.ProcessId)
} catch {
Write-Log ("{0} : could not close {1} (PID {2}). {3}" -f $path, $p.Name, $p.ProcessId, $_.Exception.Message) 'WARN'
}
}
Start-Sleep -Seconds 3
}
$profileOk = $true
# --- Registry -------------------------------------------------------------
if ($prof.Sid -and $prof.Loaded) {
$root = "Registry::HKEY_USERS\$($prof.Sid)"
if (Test-Path -LiteralPath $root) {
$script:StepFailed = $false
Remove-ZoomRegistry -Root $root -Label $path
if ($script:StepFailed) { $profileOk = $false }
}
} elseif ($prof.Sid) {
# Every signed-out hive is checked, because a folder deleted by hand
# leaves the uninstall entry behind and inventory keeps reporting it.
$hive = Join-Path $path 'NTUSER.DAT'
if (Test-Path -LiteralPath $hive) {
$load = Invoke-Reg @('load', "HKU\$MountName", $hive)
if ($load.ExitCode -ne 0) {
Write-Log ("{0} : could not load the registry hive. {1}" -f $path, $load.Output) 'ERROR'
$profileOk = $false
} else {
try {
$script:StepFailed = $false
Remove-ZoomRegistry -Root $MountPs -Label $path
if ($script:StepFailed) { $profileOk = $false }
} finally {
if (-not (Dismount-UserHive)) {
Write-Log ("{0} : could not unload the registry hive. Restart the endpoint before this user signs in." -f $path) 'ERROR'
$unloadFailed = $true
}
}
}
}
}
if ($unloadFailed) {
$failed += $path
Write-Log "Stopping, so no other hive is loaded while one is stuck." 'ERROR'
break
}
if (-not $hasFiles) {
if (-not $profileOk) { $failed += $path }
continue
}
# --- Files and shortcuts --------------------------------------------------
$script:StepFailed = $false
Remove-ZoomFiles -ProfilePath $path -Label $path -Targets $targets
if ($script:StepFailed) { $profileOk = $false }
foreach ($exe in @(Get-ZoomExe -ProfilePath $path)) {
Write-Log ("{0} : still present: {1}" -f $path, $exe.FullName) 'ERROR'
$profileOk = $false
}
if ($profileOk) {
Write-Log ("{0} : removed" -f $path)
$removed += $path
} else {
Write-Log ("{0} : not fully removed" -f $path) 'ERROR'
$failed += $path
}
}
# --- Summary ------------------------------------------------------------------
Write-Log "Summary:"
Write-Log (" Removed: {0}" -f (Join-OrNone $removed))
Write-Log (" Failed: {0}" -f (Join-OrNone $failed))
Write-Log (" Skipped, Zoom open: {0}" -f (Join-OrNone $skippedOpen))
if ($unloadFailed) { exit 6 }
if ($failed.Count -gt 0) { exit 1 }
if ($skippedOpen.Count -gt 0) { exit 3 }
exit 0