IntuneでユーザーコンテキストのChrome・Firefox・Zoom・Braveを一括アンインストールする方法(HKCU対応PowerShell)

Intuneで「ユーザー権限(ユーザーコンテキスト)」で入ってしまったChrome / Firefox / Zoom / Braveなどを回収しようとすると、HKCU(ユーザーごとのレジストリ)やAppData配下にだけ痕跡が残り、通常の「システム向けアンインストール」と同じ感覚ではうまくいきません。本記事では、失敗しがちな理由を整理しつつ、Intuneからスクリプト配布で一括削除を成功させる現実的な設計と、すぐ使えるPowerShellの実装例をまとめます。

日程Fit。無料・登録不要。「いつ空いてる?」を、ひとつのリンクで。リンクを送って、○△×でかんたん日程調整。無料で日程を作る。
目次

ユーザーコンテキストで入ったアプリが「一括削除しづらい」理由

社内PCの運用では、本来はIntuneやソフト配布ツールで「システムコンテキスト(全体インストール)」としてアプリを入れるのが基本です。しかし現場では、利用者が管理者権限なしで、ユーザー領域(AppData)にインストールしてしまうケースが起こります。代表例が、Chrome / Firefox / Zoom / Braveなどです。

このタイプのアプリは、主に次の特徴を持ちます。

  • インストール先が C:\Users\<ユーザー名>\AppData\Local\… 配下になりやすい
  • アンインストール情報が HKCU(ユーザーのレジストリ) にのみ登録されることがある
  • MSIではなくEXEベースのアンインストーラー(setup.exe / uninstall.exe)が多い
  • アンインストールの「サイレント引数」が製品ごとに違う/そもそもQuietUninstallStringが登録されていない場合もある
観点システム(デバイス全体)インストールユーザーコンテキスト(ユーザー領域)インストール
主な配置先C:\Program Files / C:\Program Files (x86)C:\Users\<user>\AppData\Local など
レジストリHKLM\Software\…\UninstallHKCU\Software\…\Uninstall(ユーザーごと)
Intuneからの扱いやすさ比較的扱いやすい(Win32アプリ/標準アンインストール)実行コンテキストを誤ると検出すらできない
アンインストールMSI(msiexec)や統一された仕組みが多いUninstallStringの解釈が製品ごとに異なる

まず押さえる:Intuneの実行コンテキストとHKCUの関係

IntuneでPowerShellスクリプトを配布するとき、もっとも重要なのが「どのアカウントとして実行されるか」です。ここを取り違えると、スクリプトはエラーにならなくても“何も見つけられない”状態になります。

Intune設定/方式実行主体HKCUが指すものユーザー領域アプリの検出
PowerShellスクリプト(ログオンユーザー資格情報:Yes)ログオン中ユーザーそのユーザーのHKCU基本的に可能
PowerShellスクリプト(ログオンユーザー資格情報:No)SYSTEMSYSTEMのHKCU基本的に不可(ユーザーHKCUを見ないため)
Win32アプリ(ユーザーコンテキスト実行)ログオン中ユーザーそのユーザーのHKCU可能(検出ルール設計が重要)
Win32アプリ(SYSTEM実行)SYSTEMSYSTEMのHKCU基本的に不可(工夫すれば可能だが難易度高)

今回のように「HKCUにしかいないアプリ」を削除したいなら、基本方針は次のどちらかです。

  • ユーザーコンテキストで実行して、そのユーザーのHKCUからUninstallStringを取り、正しく実行する
  • (難易度が上がるが)SYSTEMで全ユーザーのハイブを読み込む設計にする

現実的で事故が少ないのは前者です。Intuneの割り当ても「デバイス」ではなく、できるだけ対象ユーザー(ユーザーグループ)に寄せて設計すると、意図したHKCUで動きやすくなります。

なぜ「UninstallStringをmsiexec /xで包む」と失敗するのか

よくある失敗が、レジストリから取得したUninstallStringを、無条件に msiexec /x で実行してしまうパターンです。これはUninstallStringがMSIとは限らないため、コマンドラインとして破綻しやすいです。

UninstallStringの中身よくある例正しい扱い誤り(やりがち)
MSI(GUID)msiexec.exe /I {GUID}/I を /X に置換し、/qn 等を付けるmsiexec /x <UninstallString全体>
MSI(既にアンインストール)msiexec.exe /X {GUID} /qnそのまま実行(必要なら /norestart 追加)さらに /x を重ねて壊す
EXE(setup.exe系)“…\setup.exe” –uninstall …FilePathと引数を分離して実行msiexec /x “…\setup.exe –uninstall”
EXE(uninstall.exe系)“…\uninstall.exe” /Sベンダー推奨のサイレント引数で実行/Sの付け忘れ、またはmsiexecで包む

さらに厄介なのが、UninstallStringの形式が製品・バージョン・インストール方法で揺れることです。つまり「汎用の1行で全部消す」よりも、実務上は“UninstallStringを尊重しつつ、製品ごとのクセだけ吸収する”実装が安定します。

解決の設計:ユーザーHKCUから拾って「そのまま正しく」実行する

実際にうまくいく設計は、考え方としてはシンプルです。

  • 対象アプリを HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall(必要ならWow6432Nodeも)から検索
  • QuietUninstallStringがあれば優先、なければUninstallStringを使う
  • コマンドラインを 実行ファイル(FilePath)引数(ArgumentList) に正しく分離
  • 実行前にプロセスを停止(Chrome/Firefox/Zoomが起動中だと失敗しやすい)
  • 実行後に「まだレジストリに残っていないか」を再チェックして成功/失敗をログ化

そしてIntune側は、ユーザーコンテキストで実行させるのが基本です。これにより、HKCUが「ログオン中ユーザーのHKCU」を正しく指し、ユーザー領域インストールのアンインストールが成立しやすくなります。

実装例:Chrome / Firefox / Zoom / Braveをユーザーコンテキストで一括削除するPowerShell

以下は「ユーザーコンテキストで動く」前提のサンプルです。UninstallStringの解釈ミスを避けるため、まずはQuietUninstallStringの優先、次にUninstallStringをコマンドとして破綻しない形で実行します。必要に応じて、残骸(AppData配下)も削除できます。

運用に載せる場合は、まずテスト端末(検証用ユーザー)で「検出→削除→再ログオン→再検出」の流れを確認し、対象範囲を段階的に広げるのが安全です。

# ============================================
# User-context uninstall script for Intune
# Target: Chrome / Firefox / Zoom / Brave (per-user installs)
# ============================================

[CmdletBinding()]
param(
    [switch]$RemoveUserData
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

function Write-Log {
    param(
        [Parameter(Mandatory)][string]$Message,
        [ValidateSet("INFO","WARN","ERROR")][string]$Level = "INFO"
    )
    $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
    Write-Output "[$ts][$Level] $Message"
}

function Get-UserUninstallEntries {
    $paths = @(
        "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKCU:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )

    $items = foreach ($p in $paths) {
        if (Test-Path $p) {
            Get-ItemProperty $p -ErrorAction SilentlyContinue
        }
    }

    # DisplayNameが無いものを除外
    $items | Where-Object { $_.DisplayName -and $_.DisplayName.Trim() -ne "" }
}

function Find-UninstallEntryByDisplayName {
    param(
        [Parameter(Mandatory)][string[]]$NamePatterns
    )

    $entries = Get-UserUninstallEntries
    foreach ($pat in $NamePatterns) {
        $hit = $entries | Where-Object { $_.DisplayName -like $pat } | Select-Object -First 1
        if ($hit) { return $hit }
    }
    return $null
}

function Split-CommandLine {
    param(
        [Parameter(Mandatory)][string]$CommandLine
    )

    $cmd = $CommandLine.Trim()

    # 例: "C:\Path To\App.exe" /S /quiet
    if ($cmd -match '^\s*"([^"]+)"\s*(.*)\s*$') {
        return [pscustomobject]@{
            FilePath = $Matches[1]
            Arguments = $Matches[2]
        }
    }

    # 例: C:\Path\App.exe /S
    if ($cmd -match '^\s*([^\s]+)\s*(.*)\s*$') {
        return [pscustomobject]@{
            FilePath = $Matches[1]
            Arguments = $Matches[2]
        }
    }

    throw "Failed to parse command line: $CommandLine"
}

function Normalize-MsiExecArguments {
    param(
        [Parameter(Mandatory)][string]$Arguments
    )

    $a = $Arguments

    # /I を /X に(大文字小文字を吸収)
    $a = [regex]::Replace($a, '(?i)\s/([Ii])\s', ' /X ')
    $a = [regex]::Replace($a, '(?i)\s/([Ii])(?=\{)', ' /X')  # /I{GUID} のような形

    # 既に quiet/qn が無ければ追加
    if ($a -notmatch '(?i)\s/(q|quiet|qn)\b') {
        $a = "$a /qn"
    }
    if ($a -notmatch '(?i)\s/norestart\b') {
        $a = "$a /norestart"
    }
    return $a.Trim()
}

function Stop-TargetProcesses {
    param(
        [string[]]$ProcessNames
    )
    foreach ($pn in $ProcessNames) {
        try {
            Get-Process -Name $pn -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
        } catch {
            # 握りつぶし(停止できないことが致命的とは限らない)
        }
    }
}

function Invoke-UninstallEntry {
    param(
        [Parameter(Mandatory)]$Entry,
        [Parameter(Mandatory)][string]$AppLabel,
        [string[]]$StopProcesses = @(),
        [string]$OverrideArguments
    )

    $display = $Entry.DisplayName
    Write-Log "Detected: $AppLabel / DisplayName='$display'"

    Stop-TargetProcesses -ProcessNames $StopProcesses

    $cmd = $null
    if ($Entry.PSObject.Properties.Name -contains "QuietUninstallString" -and $Entry.QuietUninstallString) {
        $cmd = [string]$Entry.QuietUninstallString
        Write-Log "Using QuietUninstallString"
    } elseif ($Entry.UninstallString) {
        $cmd = [string]$Entry.UninstallString
        Write-Log "Using UninstallString"
    } else {
        Write-Log "No uninstall command found in registry entry." "WARN"
        return $false
    }

    $parts = Split-CommandLine -CommandLine $cmd
    $filePath = $parts.FilePath
    $args = $parts.Arguments

    if ($OverrideArguments) {
        $args = $OverrideArguments
        Write-Log "OverrideArguments applied."
    }

    # msiexec の場合だけ調整
    if ($filePath -match '(?i)msiexec(\.exe)?$') {
        $args = Normalize-MsiExecArguments -Arguments $args
    }

    if (-not (Test-Path $filePath)) {
        # msiexec.exe はパスが通っていることがあるため例外扱い
        if ($filePath -notmatch '(?i)msiexec(\.exe)?$') {
            Write-Log "Uninstaller not found: $filePath" "WARN"
            return $false
        }
    }

    Write-Log "Start uninstall: $filePath $args"
    try {
        $p = Start-Process -FilePath $filePath -ArgumentList $args -Wait -PassThru -WindowStyle Hidden
        Write-Log "ExitCode: $($p.ExitCode)"
    } catch {
        Write-Log "Failed to start uninstall process: $($_.Exception.Message)" "ERROR"
        return $false
    }

    return $true
}

function Confirm-Removed {
    param(
        [Parameter(Mandatory)][string[]]$NamePatterns
    )
    $hit = Find-UninstallEntryByDisplayName -NamePatterns $NamePatterns
    return ($null -eq $hit)
}

function Remove-PathsIfExist {
    param(
        [string[]]$Paths
    )
    foreach ($p in $Paths) {
        if ($p -and (Test-Path $p)) {
            try {
                Remove-Item -Path $p -Recurse -Force -ErrorAction Stop
                Write-Log "Removed path: $p"
            } catch {
                Write-Log "Failed to remove path: $p / $($_.Exception.Message)" "WARN"
            }
        }
    }
}

# --- Define targets ---
$targets = @(
    @{
        Label = "Google Chrome (per-user)"
        Patterns = @("Google Chrome*")
        Stop = @("chrome")
        Cleanup = @(
            Join-Path $env:LOCALAPPDATA "Google\Chrome\Application",
            Join-Path $env:LOCALAPPDATA "Google\Chrome"
        )
        # UninstallStringを基本的に尊重(必要ならOverrideArgumentsに切り替える)
        OverrideArgs = $null
    },
    @{
        Label = "Mozilla Firefox (per-user)"
        Patterns = @("Mozilla Firefox*","Firefox*")
        Stop = @("firefox")
        Cleanup = @(
            Join-Path $env:LOCALAPPDATA "Mozilla Firefox",
            Join-Path $env:APPDATA "Mozilla\Firefox"
        )
        OverrideArgs = $null
    },
    @{
        Label = "Brave Browser (per-user)"
        Patterns = @("Brave*")
        Stop = @("brave")
        Cleanup = @(
            Join-Path $env:LOCALAPPDATA "BraveSoftware\Brave-Browser\Application",
            Join-Path $env:LOCALAPPDATA "BraveSoftware\Brave-Browser"
        )
        OverrideArgs = $null
    },
    @{
        Label = "Zoom (per-user)"
        Patterns = @("Zoom*","Zoom Workplace*","Zoom Cloud Meetings*")
        Stop = @("Zoom","Zoom.exe","ZoomApp","CptHost","zTscoder","zBrowser","ZoomSharingHost")
        Cleanup = @(
            Join-Path $env:APPDATA "Zoom",
            Join-Path $env:LOCALAPPDATA "Zoom"
        )
        OverrideArgs = $null
    }
)

Write-Log "=== Start per-user app uninstall ==="
Write-Log "User: $env:USERNAME"
Write-Log "RemoveUserData: $RemoveUserData"

foreach ($t in $targets) {
    $label = $t.Label
    $patterns = [string[]]$t.Patterns
    $stop = [string[]]$t.Stop
    $cleanup = [string[]]$t.Cleanup
    $override = $t.OverrideArgs

    $entry = Find-UninstallEntryByDisplayName -NamePatterns $patterns
    if (-not $entry) {
        Write-Log "Not installed: $label"
        continue
    }

    $ok = Invoke-UninstallEntry -Entry $entry -AppLabel $label -StopProcesses $stop -OverrideArguments $override

    if (-not $ok) {
        Write-Log "Uninstall attempt failed: $label" "WARN"
        continue
    }

    # 再確認(レジストリ上の表示名が残っていないか)
    if (Confirm-Removed -NamePatterns $patterns) {
        Write-Log "Removed (registry not found): $label"
    } else {
        Write-Log "Still detected after uninstall (may require logoff/reboot): $label" "WARN"
    }

    if ($RemoveUserData) {
        Remove-PathsIfExist -Paths $cleanup
    }
}

Write-Log "=== Completed ==="

このスクリプトのポイント

  • HKCUのUninstallを検索し、ユーザー領域で入ったアプリを検出します(Wow6432Nodeも併用)。
  • QuietUninstallStringがあれば優先し、なければUninstallStringを使います。
  • UninstallStringを無理にmsiexecに渡さず、実行ファイルと引数を分離して起動します。
  • msiexecのときだけ /I → /X の正規化と、/qn /norestartの付与を行います。
  • Chrome/Firefox/Zoom/Braveは起動中だと失敗しやすいため、事前にプロセス停止します。
  • 必要なら -RemoveUserData でAppData配下の残骸を削除します(運用ポリシーに合わせてON/OFF)。

注意:ユーザーデータ削除は影響が大きい施策です。業務上「アプリだけ消したい」のか「個人プロファイルも消したい」のかを切り分け、段階的に実施してください。例えばFirefoxのプロファイル(APPDATA配下)を消すとブックマーク等が消えます。

「複数ユーザーをまとめて削除」したいときの現実的な考え方

端末に複数ユーザーがサインインする運用(共有PC、キッティング後に複数アカウントが利用など)では、「C:\Usersを列挙して全員分消したい」と考えがちです。ただし、ユーザーコンテキストで動くスクリプトは“そのログオンユーザー”のHKCUしか自然には触れません。

そこで実務では、次のどちらかに寄せると安定します。

  • ユーザー単位で回収する:Intuneの割り当てをユーザーグループにして、各ユーザーのログオン時(または定期実行)に回収する
  • Proactive Remediations(検出/修復)を使う:「検出スクリプトで存在確認 → 修復スクリプトで削除」を繰り返し可能にする

どうしてもSYSTEMで“全ユーザー分”をやりたい場合は、各ユーザーのNTUSER.DATを一時的にロードしてHKU配下から参照する設計になります。ただし、ハイブのロード/アンロード、ファイルロック、SIDの解決、失敗時の後処理など運用負荷が上がるため、まずはユーザーコンテキスト運用を優先するのが無難です。

Intune配布手順:PowerShellスクリプト機能で実行する

最短で回収したい場合は、Intuneの「PowerShell スクリプト」配布が手軽です。重要なのはユーザーコンテキストで実行させることです。

  • スクリプトの実行主体:ログオンしているユーザーの資格情報を使用(Yes)
  • 64bit PowerShell:端末環境に合わせて(迷う場合は64bitを推奨しがちですが、実行対象で差が出るなら検証優先)
  • 署名要件:社内ポリシーに従う(未署名許可が難しいならWin32アプリ方式も検討)
項目推奨の考え方理由
ログオンユーザー資格情報YesHKCUが対象ユーザーのハイブになるため
割り当てユーザーグループユーザー単位の回収と相性が良い
ログ出力Write-Output+必要ならTranscript失敗時の切り分けが速い
再実行性要件次第でRemediationへ“一度だけ”では取りこぼしが起きやすい

Intune配布手順:Win32アプリとして「回収ツール化」する

運用としては、削除スクリプトをWin32アプリ(.intunewin)としてラップし、検出ルールを付けて配布する方法もあります。利点は、検出/再試行/依存関係など、アプリ配布として管理しやすい点です。

基本設計は次の通りです。

  • パッケージ:PowerShellスクリプト(必要なら補助exe/設定ファイル)
  • インストールコマンド:実態は「アンインストール(回収)実行」
  • 検出ルール:HKCUのDisplayName存在、またはAppDataのパス存在で判定
  • 実行コンテキスト:ユーザー(HKCUを見るため)

検出ルールの例としては、次のような考え方が使えます。

  • レジストリ検出:HKCU\Software\…\Uninstall配下にDisplayNameが存在するか
  • ファイル検出:%LOCALAPPDATA%\Google\Chrome\Application が存在するか

ユーザーコンテキストでHKCU検出を行う場合、対象ユーザーが一度もログオンしていない端末では検出できないことがあります。共有端末では「どのタイミングで回収するか」も含めて設計してください。

Zoomが「完全サイレントにならない」など、製品ごとのクセへの対処

Zoomのように、ユーザーコンテキスト版のアンインストールが環境によってはポップアップが出たり、完全サイレントになりにくいケースがあります。この場合、次のような迂回策が実務では有効になることがあります。

  • まずIntuneでシステムコンテキスト版(全体インストール)を配布し、ユーザー版の状態を吸収させる
  • その後、Intune標準の手順(Win32アプリのアンインストールや検出)で回収する

ただし、Zoomは配布形態(MSI/EXE、企業向け設定、バージョン)で挙動が変わるため、必ず検証端末で「ユーザー版がどう置き換わるか」「設定やサインイン状態がどうなるか」を確認してから適用してください。

MicroSIPなど、AppData配下に入る別アプリへの応用

ユーザー領域インストールの回収は、ChromeやFirefoxに限りません。MicroSIPのように %LOCALAPPDATA% 配下にフォルダごと展開されるタイプでも、考え方は同じです。

  • まずはアンインストーラーの実体(例:%LOCALAPPDATA%\MicroSIP\Uninstall.exe)を探す
  • ベンダーが用意しているサイレント引数(/S や /silent など)を確認する
  • Intuneからユーザーコンテキストで実行し、完了後にフォルダが残っていないか確認する

「レジストリに情報が残らない」タイプは、検出と削除の両方でファイルパス基準が有効です。逆に「レジストリにだけ残る」タイプはHKCU基準が効きます。アプリごとに、どこに痕跡が残るかを先に把握しておくと設計がぶれません。

うまく消えないときのチェックポイント

一括削除の現場では、失敗理由が混ざりがちです。切り分けの観点を先に持っておくと復旧が早くなります。

症状よくある原因対処の方向性
スクリプト上は「見つからない」SYSTEMで実行していてHKCUがSYSTEMになっているユーザーコンテキスト(ログオンユーザー資格情報:Yes)に変更
UninstallStringは取得できるが実行に失敗FilePathと引数が分離できていない/引用符の扱いコマンドライン解析を修正(Split-CommandLine)
msiexecが動くが削除されない/Iのままになっている、/Xにできていない/I→/Xの正規化、/qn追加
アンインストール後も検出される再起動/ログオフが必要、または残骸が検出条件に残っている検出条件の見直し、追加のクリーンアップ
アンインストール中にアプリが再起動する自動更新タスク/常駐プロセスプロセス停止の強化、タスク/サービスの確認

事故を減らすための運用チェックリスト

「一括削除」は便利な反面、やり方を誤ると業務停止に直結します。配布前に次をチェックしておくと、炎上を避けやすくなります。

チェック項目見るべき点おすすめ
対象の明確化“ユーザー版”だけ消すのか、システム版も含むのかまずユーザー版に限定
影響範囲ブラウザ削除で業務SaaSに影響が出ないか代替(Edge等)を先に整備
ユーザーデータプロファイル(ブックマーク等)を消す必要があるか原則は残す(必要時のみRemoveUserData)
配布方式一回実行で足りるか、継続検出が必要か継続が必要ならRemediation
ログ失敗端末を特定できるかExitCodeと検出結果を必ず出す

まとめ:Intuneでユーザー領域アプリを回収する最短ルート

  • ユーザー領域で入ったアプリは、HKCUとAppDataが中心になり、通常の“マシン向け”アンインストール設計とは別物です。
  • Intuneでのつまずきポイントは、ほぼ実行コンテキストUninstallStringの扱いに集約されます。
  • 成功率を上げるには、ユーザーコンテキストでHKCUからUninstallString/QuietUninstallStringを取得し、FilePathと引数を正しく分離して実行するのが定石です。
  • Chrome/Firefox/Zoom/Braveなどは、プロセス停止→アンインストール→再検出(残っていないか確認)の流れを入れることで、回収の品質が上がります。
  • 「一度きり」で取りこぼしが出る運用なら、検出/修復を繰り返せる仕組み(Remediation/Win32化)に寄せるのが現場向きです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次