> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.usepassport.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.usepassport.ai/_mcp/server.

# Deploy the Passport CLI with mobile device management

Deploy the [Passport CLI](/passport/reference/passport-cli/overview/) to managed machines with a mobile device management (MDM) tool, such as [Jamf](https://www.jamf.com/) or [Intune](https://learn.microsoft.com/mem/intune/). Developers who manage their own machine should use the self-serve [Install the Passport CLI](/passport/reference/passport-cli/install/) steps instead.

The Passport CLI is installed machine-wide so consumers don't install it by hand, and a post-install script then provisions the per-user client certificate in each signed-in user's session by running `passport setup`.

Each user must sign in with [`passport login`](/passport/reference/passport-cli/login/) before the post-install script can provision their certificate. The [`passport setup`](/passport/reference/passport-cli/setup/) command reads the signed-in user's login profile, so it can't issue a certificate until the user has authenticated.

## Install on macOS

To deploy the Passport CLI through your MDM tool, do the following:

1. Inject a [configuration profile](#configuration-profile) containing the Passport Proxy endpoints.
2. (Optional) Run the uninstall script to clean up any existing installation.
3. Download the Passport CLI `.pkg` ([Apple silicon](https://dl-passport.pstmn.io/download/version/0.6.0-canary-260921-212749/osx_arm64?channel=canary) or [Intel](https://dl-passport.pstmn.io/download/version/0.6.0-canary-260921-212749/osx_64?channel=canary)) and deploy it.
4. Run the post-install script to provision each signed-in user.

The post-install script runs `passport setup` in the signed-in user's session:

**`Post-install script`**

```bash title="Post-install script" maxLines=15 wordWrap
#!/bin/bash
set -eu

user=$(/usr/bin/stat -f '%Su' /dev/console)

case "$user" in
  ""|root|loginwindow|_mbsetupuser)
    echo "No GUI user; Passport setup must run after user login."
    exit 0
    ;;
esac

uid=$(/usr/bin/id -u "$user")

if ! /bin/launchctl print "gui/$uid" >/dev/null 2>&1; then
  echo "GUI session gui/$uid is unavailable; retry after login."
  exit 0
fi

exec /bin/launchctl asuser "$uid" \
  /usr/bin/sudo -H -u "$user" \
  /usr/local/bin/passport setup
```

### Configuration profile

The configuration profile sets the Passport Proxy endpoints and, optionally, a forced routing policy. Supply these settings to your MDM tool as a managed preference for the `com.postman.passport` domain. The tool forces them in a system-scoped Apple configuration profile and deploys it.

**`Passport configuration profile example`**

```xml title="Passport configuration profile example" wordWrap
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>ManagementURL</key>
  <string>https://passport-management.example.com</string>

  <key>ProxyURL</key>
  <string>https://passport-proxy.example.com:8443</string>

  <key>RoutingPolicy</key>
  <dict>
    <key>Mode</key>
    <string>allowlist</string>

    <key>Strict</key>
    <true/>

    <key>AllowedHosts</key>
    <array>
      <string>api.example.com</string>
      <string>*.internal.example.com</string>
    </array>
  </dict>
</dict>
</plist>
```

The Passport settings:

* **`ManagementURL`** — (Optional) The certificate management origin that issues each client certificate. Defaults to `ProxyURL` when omitted. Equivalent to `passport setup --management-url`.
* **`ProxyURL`** — The data-plane proxy origin that requests route through, including the port. Required when the profile manages endpoints. Equivalent to `passport setup --proxy-url`.
* **`RoutingPolicy`** — (Optional) Forces the daemon's routing policy on managed machines, overriding the user's local `daemon.yml`. When present, it must contain all three of the following keys:
  * **`Mode`** — Set to `forward-all` to route every host through the proxy, or `allowlist` to route only the hosts in `AllowedHosts`.
  * **`Strict`** — In `allowlist` mode, `true` denies non-listed hosts with a 502 and `false` sends them direct to the origin. It has no effect in `forward-all` mode.
  * **`AllowedHosts`** — The hosts to route through the proxy in `allowlist` mode, as DNS names, IP addresses, or simple `*` globs (up to 256 entries). Use an empty array in `forward-all` mode.

For a routing-only profile, omit `ManagementURL` and `ProxyURL`.

## Install on Windows

Deploy the Passport CLI through Intune, then run the post-install script, configured in Intune as follows:

* Don't run the script using the signed-in user's credentials, so it runs as SYSTEM.
* Run the script in the 64-bit PowerShell host.
* Don't enforce the script signature check.

The script registers a scheduled task that runs `passport setup` in each user's session, at sign-in and immediately for the current user, because Intune user-context scripts don't always run. It logs to `C:\ProgramData\Postman\Passport\Deploy.log` (SYSTEM) and `%LOCALAPPDATA%\Postman\Passport\IntuneSetup.log` (per user).

**`Post-install script`**

```powershell title="Post-install script" maxLines=20 wordWrap
$ErrorActionPreference = 'Stop'

if ($env:PROCESSOR_ARCHITEW6432 -eq 'AMD64' -and -not [Environment]::Is64BitProcess) {
    & (Join-Path $env:WINDIR 'SysNative\WindowsPowerShell\v1.0\powershell.exe') -NoProfile -ExecutionPolicy Bypass -File $PSCommandPath
    exit $LASTEXITCODE
}

$dir      = Join-Path $env:ProgramData 'Postman\Passport'
$log      = Join-Path $dir 'Deploy.log'
$userPs1  = Join-Path $dir 'Passport-User-Setup.ps1'
$taskName = 'Postman Passport User Setup'
New-Item -ItemType Directory -Path $dir -Force | Out-Null
function Write-Log($m) { $l = '{0}  {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $m; Add-Content $log $l; Write-Output $l }

try {
    Write-Log "Starting as $([Security.Principal.WindowsIdentity]::GetCurrent().Name)"

    # 1. Stop any daemon left running as SYSTEM by earlier attempts
    Get-CimInstance Win32_Process -Filter "Name like 'passport%'" | Where-Object {
        $_.CommandLine -match 'daemon' -and (Invoke-CimMethod -InputObject $_ -MethodName GetOwner).User -eq 'SYSTEM'
    } | ForEach-Object { Write-Log "Stopping SYSTEM daemon PID $($_.ProcessId)"; Stop-Process -Id $_.ProcessId -Force }

    # 2. Drop the per-user setup script where users can read/execute it
    $userScript = @'
# Passport-User-Setup.ps1
# Runs "passport.exe setup" as the SIGNED-IN USER (not SYSTEM).
# Intune: Run using logged on credentials = Yes, 64-bit = Yes, Signature check = No
# Log: %LOCALAPPDATA%\Postman\Passport\IntuneSetup.log

$ErrorActionPreference = 'Stop'

# --- Re-launch in 64-bit PowerShell if started in 32-bit -------------------
if ($env:PROCESSOR_ARCHITEW6432 -eq 'AMD64' -and -not [Environment]::Is64BitProcess) {
    $ps64 = Join-Path $env:WINDIR 'SysNative\WindowsPowerShell\v1.0\powershell.exe'
    & $ps64 -NoProfile -ExecutionPolicy Bypass -File $PSCommandPath
    exit $LASTEXITCODE
}

# --- Logging -----------------------------------------------------------------
$logDir  = Join-Path $env:LOCALAPPDATA 'Postman\Passport'
$logFile = Join-Path $logDir 'IntuneSetup.log'
New-Item -ItemType Directory -Path $logDir -Force | Out-Null

function Write-Log {
    param([string]$Message)
    $line = '{0}  {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
    Add-Content -Path $logFile -Value $line
    Write-Output $line
}

try {
    $user = [Security.Principal.WindowsIdentity]::GetCurrent().Name
    Write-Log "Starting. User: $user, 64-bit: $([Environment]::Is64BitProcess)"

    # Refuse to run as SYSTEM - setup is per-user
    if ($user -eq 'NT AUTHORITY\SYSTEM') {
        Write-Log 'ERROR: Running as SYSTEM. Set "Run this script using the logged on credentials" to Yes in Intune.'
        exit 1
    }

    # --- Find install location (64-bit and 32-bit registry views) ------------
    $installLocation = $null
    foreach ($view in [Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32) {
        $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $view)
        $key  = $base.OpenSubKey('SOFTWARE\Postman\Passport CLI')
        if ($key) { $installLocation = $key.GetValue('InstallLocation'); $key.Close() }
        $base.Close()
        if ($installLocation) { break }
    }

    if (-not $installLocation) {
        Write-Log 'ERROR: Passport installation location not found in registry.'
        exit 1
    }

    $passport = Join-Path $installLocation 'passport.exe'
    if (-not (Test-Path -LiteralPath $passport -PathType Leaf)) {
        Write-Log "ERROR: Passport not found: $passport"
        exit 1
    }

    # --- Run setup ------------------------------------------------------------
    Write-Log "Running: `"$passport`" setup"
    $output   = & $passport --color off --no-animation setup 2>&1 | Out-String
    $exitCode = $LASTEXITCODE
    if ($null -eq $exitCode) { $exitCode = 0 }
    if ($output) { Write-Log "Output:`r`n$output" }

    switch ($exitCode) {
        0       { Write-Log 'Setup succeeded: credentials ready and daemon running.' }
        2       { Write-Log 'ERROR: Partial success - credentials ready but daemon failed to start.' }
        default { Write-Log "ERROR: Setup failed with exit code $exitCode." }
    }

    # --- Confirm daemon state -------------------------------------------------
    $status = & $passport daemon status 2>&1 | Out-String
    Write-Log "Daemon status:`r`n$status"

    exit $exitCode
}
catch {
    Write-Log "ERROR: $($_.Exception.Message)"
    exit 1
}
'@
    Set-Content -Path $userPs1 -Value $userScript -Encoding UTF8 -Force
    Write-Log "Wrote $userPs1"

    # 3. Scheduled task: runs as whoever is logged on (Users group), at logon
    $action    = New-ScheduledTaskAction -Execute 'powershell.exe' `
                   -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$userPs1`""
    $trigger   = New-ScheduledTaskTrigger -AtLogOn
    $principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Limited   # BUILTIN\Users
    $settings  = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
                   -ExecutionTimeLimit (New-TimeSpan -Minutes 10) -StartWhenAvailable
    Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
        -Principal $principal -Settings $settings -Force | Out-Null
    Write-Log "Registered task '$taskName'"

    # 4. Run it now for the currently signed-in user (if any)
    $loggedOn = (Get-CimInstance Win32_ComputerSystem).UserName
    if ($loggedOn) {
        Start-ScheduledTask -TaskName $taskName
        Write-Log "Started task for signed-in user $loggedOn"
    } else {
        Write-Log 'No user signed in; task will run at next logon.'
    }
    exit 0
}
catch {
    Write-Log "ERROR: $($_.Exception.Message)"
    exit 1
}
```

## Uninstall

Run the uninstall script for your operating system.

### macOS

Run the uninstall script to remove the Passport CLI and per-user state. Remove the `com.postman.passport` configuration profile separately through your MDM tool.

**`Uninstall script`**

```bash title="Uninstall script" maxLines=15 wordWrap
#!/bin/bash
set -eu

CE_PACKAGE="@postman/postman-passport"

user=${4:-$(/usr/bin/stat -f '%Su' /dev/console)}

case "$user" in
  ""|root|loginwindow|_mbsetupuser|*/*)
    echo "No valid GUI user; skipping per-user cleanup."
    user=""
    ;;
esac

if [ -n "$user" ]; then
  uid=$(/usr/bin/id -u "$user")
  home=$(
    /usr/bin/dscl /Search -read "/Users/$user" NFSHomeDirectory |
      /usr/bin/sed -n 's/^NFSHomeDirectory: //p'
  )

  case "$home" in
    /*) ;;
    *)
      echo "Cannot resolve home for $user." >&2
      exit 1
      ;;
  esac

  # Run a command in the logged-in user's shell/environment.
  as_user() {
    /bin/launchctl asuser "$uid" \
      /usr/bin/sudo -u "$user" -H \
      /bin/zsh -lic "$1" >/dev/null 2>&1 || true
  }

  #
  # Stop Lens before removing Passport.
  #
  echo "Stopping Passport Lens..."
  as_user 'command -v passport >/dev/null && passport lens stop'
  as_user 'command -v passport >/dev/null && passport lens clean'

  #
  # Remove Community Edition, regardless of which supported
  # package manager was used to install it.
  #
  echo "Removing Passport Community Edition..."

  as_user "command -v npm  >/dev/null && npm uninstall -g '$CE_PACKAGE'"
  as_user "command -v pnpm >/dev/null && pnpm remove -g '$CE_PACKAGE'"
  as_user "command -v bun  >/dev/null && bun remove -g '$CE_PACKAGE'"

  #
  # Stop Passport launch services.
  #
  for label in \
    com.postman.passport-daemon \
    com.postman.passport-env
  do
    /bin/launchctl bootout "gui/$uid/$label" 2>/dev/null || true
    /bin/launchctl bootout "gui/0/$label" 2>/dev/null || true
    /bin/launchctl bootout "system/$label" 2>/dev/null || true
  done

  #
  # Clear environment injected by Passport.
  #
  for variable in \
    HTTP_PROXY \
    HTTPS_PROXY \
    NO_PROXY \
    http_proxy \
    https_proxy \
    no_proxy \
    NODE_EXTRA_CA_CERTS \
    REQUESTS_CA_BUNDLE \
    CURL_CA_BUNDLE \
    GIT_SSL_CAINFO \
    SSL_CERT_FILE
  do
    /bin/launchctl asuser "$uid" \
      /bin/launchctl unsetenv "$variable" 2>/dev/null || true
  done

  #
  # Remove per-user state.
  #
  /bin/rm -f \
    "$home/Library/LaunchAgents/com.postman.passport-daemon.plist" \
    "$home/Library/LaunchAgents/com.postman.passport-env.plist"

  # Passport owns these directories outright and they are channel-scoped.
  for dir in .passport .passport-beta .passport-stage; do
    if [ -d "$home/$dir" ]; then
      /bin/rm -rf "$home/$dir"
    fi
  done

  echo "Removed Passport state for $user."
fi

#
# Remove managed Passport installation.
#
/bin/rm -f \
  /usr/local/bin/passport \
  /usr/local/share/man/man1/passport.1

/bin/rm -rf /usr/local/libexec/postman-passport

/usr/sbin/pkgutil --forget com.postman.passport-cli >/dev/null 2>&1 || true

echo "Passport removed."
echo "Remove the com.postman.passport profile separately through Jamf."
echo "Restart open applications to clear inherited environment values."
```

### Windows

Run the uninstall script as SYSTEM, using the same Intune settings as the post-install script. Run it instead of uninstalling the installer package (MSI) on its own, because the per-user teardown needs `passport.exe`. The script does the following:

1. Removes the `Postman Passport User Setup` scheduled task registered at install time.
2. Runs the per-user teardown in the signed-in user's session.
3. Stops leftover Passport processes and removes the Passport certificate authority from `LocalMachine\Root`.
4. Uninstalls the MSI, located by its UpgradeCode.

Only the signed-in user is cleaned up; other profiles keep their `.passport` state. Remove the `HKLM\Software\Policies\Postman\Passport` policy separately through Intune or Group Policy.

**`Uninstall script`**

```powershell title="Uninstall script" maxLines=20 wordWrap
$ErrorActionPreference = 'Stop'

if ($env:PROCESSOR_ARCHITEW6432 -eq 'AMD64' -and -not [Environment]::Is64BitProcess) {
    & (Join-Path $env:WINDIR 'SysNative\WindowsPowerShell\v1.0\powershell.exe') -NoProfile -ExecutionPolicy Bypass -File $PSCommandPath
    exit $LASTEXITCODE
}

$dir           = Join-Path $env:ProgramData 'Postman\Passport'
$log           = Join-Path $dir 'Uninstall.log'
$msiLog        = Join-Path $dir 'Uninstall-msi.log'
$setupPs1      = Join-Path $dir 'Passport-User-Setup.ps1'
$cleanupPs1    = Join-Path $dir 'Passport-User-Cleanup.ps1'
$setupTask     = 'Postman Passport User Setup'
$cleanupTask   = 'Postman Passport User Cleanup'
$upgradeCode   = '{57DA6E9B-820A-4DE7-BB47-8EE9D7A36CA5}'
$stateDirs     = '.passport', '.passport-beta', '.passport-stage'
$failed        = $false
New-Item -ItemType Directory -Path $dir -Force | Out-Null
function Write-Log($m) { $l = '{0}  {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $m; Add-Content $log $l; Write-Output $l }

try {
    Write-Log "Starting as $([Security.Principal.WindowsIdentity]::GetCurrent().Name)"

    # 1. Remove the install-time logon task first so it cannot re-run setup mid-uninstall
    Stop-ScheduledTask -TaskName $setupTask -ErrorAction SilentlyContinue
    if (Get-ScheduledTask -TaskName $setupTask -ErrorAction SilentlyContinue) {
        Unregister-ScheduledTask -TaskName $setupTask -Confirm:$false
        Write-Log "Removed task '$setupTask'"
    }

    # 2. Find the install location (64-bit and 32-bit registry views)
    $installLocation = $null
    foreach ($view in [Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32) {
        $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $view)
        $key  = $base.OpenSubKey('SOFTWARE\Postman\Passport CLI')
        if ($key) { $installLocation = $key.GetValue('InstallLocation'); $key.Close() }
        $base.Close()
        if ($installLocation) { break }
    }

    $passport = ''
    if ($installLocation) {
        $candidate = Join-Path $installLocation 'passport.exe'
        if (Test-Path -LiteralPath $candidate -PathType Leaf) { $passport = $candidate }
    }
    if ($passport) { Write-Log "Found $passport" } else { Write-Log 'WARNING: passport.exe not found; skipping the CLI-based teardown.' }

    # 3. Resolve the signed-in user and their profile folder
    $loggedOn    = (Get-CimInstance Win32_ComputerSystem).UserName
    $profilePath = $null
    if ($loggedOn) {
        try {
            $sid = (New-Object System.Security.Principal.NTAccount($loggedOn)).Translate([System.Security.Principal.SecurityIdentifier]).Value
            $profilePath = (Get-CimInstance Win32_UserProfile -Filter "SID='$sid'").LocalPath
        } catch {
            Write-Log "WARNING: could not resolve the profile of $loggedOn ($($_.Exception.Message))"
        }
    }

    # 4. Read the CA thumbprints now - the per-user teardown deletes ca.pem
    $caThumbprints = @()
    if ($profilePath) {
        foreach ($stateDir in $stateDirs) {
            $pem = Join-Path $profilePath "$stateDir\ca.pem"
            if (Test-Path -LiteralPath $pem -PathType Leaf) {
                try {
                    $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($pem)
                    $caThumbprints += $cert.Thumbprint
                } catch {
                    Write-Log "WARNING: could not read $pem ($($_.Exception.Message))"
                }
            }
        }
    }

    # 5. Per-user teardown, run as the signed-in user via a one-shot scheduled task
    if (-not $loggedOn -or -not $profilePath) {
        Write-Log 'WARNING: No signed-in user; skipping per-user cleanup (daemon, HKCU environment, ~\.passport state remain).'
    } else {
        $userScript = @'
# Passport-User-Cleanup.ps1
# Runs as the SIGNED-IN USER (launched by the Passport uninstall script via a one-shot scheduled task).
# Log: %LOCALAPPDATA%\Postman\Passport\IntuneUninstall.log

param([string]$PassportExe = '')

$ErrorActionPreference = 'Continue'

$logDir  = Join-Path $env:LOCALAPPDATA 'Postman\Passport'
$logFile = Join-Path $logDir 'IntuneUninstall.log'
New-Item -ItemType Directory -Path $logDir -Force | Out-Null

function Write-Log {
    param([string]$Message)
    $line = '{0}  {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
    Add-Content -Path $logFile -Value $line
    Write-Host $line
}

function Invoke-Passport {
    param([string[]]$Arguments)
    $output   = & $PassportExe --color off --no-animation @Arguments 2>&1 | Out-String
    $exitCode = $LASTEXITCODE
    if ($null -eq $exitCode) { $exitCode = 0 }
    Write-Log "passport $($Arguments -join ' ') -> exit $exitCode"
    if ($output.Trim()) { Write-Log "Output:`r`n$output" }
    return $exitCode
}

$failed = $false
try {
    Write-Log "Starting. User: $([Security.Principal.WindowsIdentity]::GetCurrent().Name)"

    if ($PassportExe -and (Test-Path -LiteralPath $PassportExe -PathType Leaf)) {
        # Lens is best-effort: a non-zero exit just means it was not running
        [void](Invoke-Passport @('lens', 'stop'))
        [void](Invoke-Passport @('lens', 'clean'))

        # Stops the daemon, removes the Startup Apps entry, restores HKCU\Environment
        if ((Invoke-Passport @('daemon', 'uninstall')) -ne 0) { $failed = $true }
    } else {
        Write-Log 'WARNING: passport.exe not available; skipping lens/daemon teardown.'
    }

    # Passport owns these directories outright and they are channel-scoped
    foreach ($stateDir in '.passport', '.passport-beta', '.passport-stage') {
        $target = Join-Path $env:USERPROFILE $stateDir
        if (Test-Path -LiteralPath $target) {
            try {
                Remove-Item -LiteralPath $target -Recurse -Force -ErrorAction Stop
                Write-Log "Removed $target"
            } catch {
                Write-Log "ERROR: could not remove $target ($($_.Exception.Message))"
                $failed = $true
            }
        }
    }

    if ($failed) { exit 1 }
    Write-Log 'Per-user cleanup complete.'
    exit 0
}
catch {
    Write-Log "ERROR: $($_.Exception.Message)"
    exit 1
}
'@
        Set-Content -Path $cleanupPs1 -Value $userScript -Encoding UTF8 -Force
        Write-Log "Wrote $cleanupPs1"

        try {
            $taskArgs = "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$cleanupPs1`""
            if ($passport) { $taskArgs += " -PassportExe `"$passport`"" }
            $action    = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $taskArgs
            $principal = New-ScheduledTaskPrincipal -UserId $loggedOn -LogonType Interactive -RunLevel Limited
            $settings  = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
                           -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
            Register-ScheduledTask -TaskName $cleanupTask -Action $action -Principal $principal -Settings $settings -Force | Out-Null

            $startedAt = (Get-Date).AddSeconds(-2)
            Start-ScheduledTask -TaskName $cleanupTask
            Write-Log "Started per-user cleanup for $loggedOn"

            $deadline = (Get-Date).AddMinutes(5)
            $finished = $false
            while (-not $finished -and (Get-Date) -lt $deadline) {
                Start-Sleep -Seconds 2
                $state = (Get-ScheduledTask -TaskName $cleanupTask).State
                $info  = Get-ScheduledTaskInfo -TaskName $cleanupTask
                if ($state -ne 'Running' -and $state -ne 'Queued' -and $info.LastRunTime -gt $startedAt) { $finished = $true }
            }

            $userLog = Join-Path $profilePath 'AppData\Local\Postman\Passport\IntuneUninstall.log'
            if (Test-Path -LiteralPath $userLog) { Write-Log "User cleanup log (tail):`r`n$(Get-Content -LiteralPath $userLog -Tail 40 | Out-String)" }

            if (-not $finished) {
                Write-Log 'ERROR: Per-user cleanup did not finish within 5 minutes.'
                Stop-ScheduledTask -TaskName $cleanupTask -ErrorAction SilentlyContinue
                $failed = $true
            } elseif ($info.LastTaskResult -ne 0) {
                Write-Log "ERROR: Per-user cleanup failed with result $($info.LastTaskResult)."
                $failed = $true
            } else {
                Write-Log 'Per-user cleanup succeeded.'
            }
        } finally {
            Unregister-ScheduledTask -TaskName $cleanupTask -Confirm:$false -ErrorAction SilentlyContinue
        }
    }

    # 6. Stop any Passport process still running from the install folder so the MSI can remove it
    if ($installLocation) {
        Get-Process -Name 'passport', 'passport-daemon' -ErrorAction SilentlyContinue | Where-Object {
            $_.Path -and $_.Path.StartsWith($installLocation, [StringComparison]::OrdinalIgnoreCase)
        } | ForEach-Object {
            Write-Log "Stopping $($_.Name) PID $($_.Id)"
            Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
        }
        Start-Sleep -Seconds 2
    }

    # 7. Remove the Passport CA from LocalMachine\Root (SYSTEM is already elevated)
    if ($caThumbprints.Count -gt 0) {
        $store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root', 'LocalMachine')
        $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
        try {
            foreach ($thumbprint in ($caThumbprints | Select-Object -Unique)) {
                $found = $store.Certificates.Find('FindByThumbprint', $thumbprint, $false)
                if ($found.Count -eq 0) { Write-Log "Passport CA $thumbprint is not in LocalMachine\Root"; continue }
                foreach ($certificate in $found) { $store.Remove($certificate) }
                Write-Log "Removed Passport CA $thumbprint from LocalMachine\Root"
            }
        } catch {
            Write-Log "ERROR: could not remove the Passport CA ($($_.Exception.Message))"
            $failed = $true
        } finally {
            $store.Close()
        }
    } else {
        Write-Log 'No Passport CA found to remove.'
    }

    # 8. Uninstall the MSI, located by its UpgradeCode
    $installer    = New-Object -ComObject WindowsInstaller.Installer
    $productCodes = @($installer.RelatedProducts($upgradeCode))
    if ($productCodes.Count -eq 0) {
        Write-Log 'Passport MSI is not installed.'
    }
    foreach ($productCode in $productCodes) {
        Write-Log "Uninstalling MSI $productCode"
        $msi = Start-Process -FilePath (Join-Path $env:WINDIR 'System32\msiexec.exe') `
                 -ArgumentList "/x $productCode /qn /norestart /l*v `"$msiLog`"" -Wait -PassThru
        switch ($msi.ExitCode) {
            0       { Write-Log 'MSI uninstalled.' }
            3010    { Write-Log 'MSI uninstalled; a restart is required to finish.' }
            1605    { Write-Log 'MSI was already uninstalled.' }
            default { Write-Log "ERROR: msiexec failed with exit code $($msi.ExitCode). See $msiLog"; $failed = $true }
        }
    }

    # 9. Remove the helper scripts (logs are kept)
    Remove-Item -LiteralPath $setupPs1, $cleanupPs1 -Force -ErrorAction SilentlyContinue

    if ($failed) {
        Write-Log 'Passport removal finished with errors; see above.'
        exit 1
    }
    Write-Log 'Passport removed.'
    Write-Log 'Remove the HKLM\Software\Policies\Postman\Passport policy separately through Intune/GPO.'
    Write-Log 'Restart open applications to clear inherited environment values.'
    exit 0
}
catch {
    Write-Log "ERROR: $($_.Exception.Message)"
    exit 1
}
```