Skip to content
COR Intelligence
Back to latest

Removing the NinjaOne Agent when Add/Remove Programs will not

Pete Gypps21 May 2026 · updated 27 August 2026
5 min read702 words

A PowerShell script that finishes the job when the normal uninstall flow leaves NinjaRMM bits behind, services, registry keys, modules and the Ninja Remote driver. Copy-paste, run as administrator, done.

A laptop running a code editor in the dark, with the headline "PowerShell · MSP Tooling, Removing the NinjaOne Agent" overlaid in white on navy.
When Add/Remove Programs will not finish the job, PowerShell does.

Every MSP technician has had this Windows machine on their desk. The customer reports that NinjaOne (NinjaRMM) is still phoning home long after they cancelled the contract. You open Add/Remove Programs. The agent is not listed, or it is listed but the uninstaller errors out. The service is still running. The folder is still there. The PowerShell module is still installed. The registry is full of keys nobody can find by clicking.

The script below is what we run when the normal flow has failed. It is the script NinjaOne themselves publish for this case, dropped on the machine, run from an elevated PowerShell window. It does the cleanup Add/Remove Programs was supposed to do, properly, with a log, and without you needing to remember where the bits live.

Before you run it

Three things worth checking first.

  • Run it from an elevated PowerShell session. The script self-elevates with a UAC prompt if you launch it from a non-elevated window, but it is cleaner to start from an admin PowerShell directly.
  • Save the customer’s NinjaOne organisation ID and end-user data first, if anything still needs migrating off. Once this runs, the agent is gone.
  • If you have the device in NinjaOne’s console, deauthorise it there first. The script removes the agent locally, but the device record on NinjaOne’s side persists until you clean it up.

The script

Copy this into a new .ps1 file on the target machine (or paste it directly into an elevated PowerShell window), then run. The whole job takes a couple of minutes including the Ninja MSI uninstaller phase, which the script invokes itself.

You can also download the .ps1 file directly if you would rather not copy-paste.

powershell
function Write-LogEntry {
  param (
    [Parameter(Mandatory = $true)]
    [string]$Message
  )

  $LogPath = "$env:windir\temp\NinjaOneAgentRemoval.log"
  $TimeStamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
  Add-Content -Path $LogPath -Value "$TimeStamp - $Message"
  Write-Host "$TimeStamp - $Message"
}

function Uninstall-NinjaMSI {
  $Arguments = @(
    "/x$($UninstallString)"
    '/quiet'
    '/L*V'
    "$env:windir\temp\NinjaRMMAgent_uninstall.log"
    "WRAPPED_ARGUMENTS=`"--mode unattended`""
  )

  Start-Process "msiexec.exe" -ArgumentList $Arguments -Wait -NoNewWindow
  Write-LogEntry 'Finished running uninstaller. Continuing to clean up...'
  Start-Sleep 30
}

#Get current user context
$CurrentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
#Check user that is running the script is a member of Administrator Group
if (!($CurrentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator))) {
  #UAC Prompt will occur for the user to input Administrator credentials and relaunch the powershell session
  Write-LogEntry 'This script must be ran with administrative privileges. Script will relaunch and request elevation...'
  Start-Process powershell.exe "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs; Exit
}

$ErrorActionPreference = "SilentlyContinue"

Write-LogEntry 'Beginning NinjaRMM Agent removal...'
Write-LogEntry 'Path to log file: C:\temp\NinjaOneAgentRemoval.log'

$NinjaRegPath = 'HKLM:\SOFTWARE\WOW6432Node\NinjaRMM LLC\NinjaRMMAgent'
$NinjaDataDirectory = "$($env:ProgramData)\NinjaRMMAgent"
$UninstallRegPath = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
$NinjaModulePath = "$env:ProgramFiles\WindowsPowerShell\Modules\NJCliPSh"

if (!([System.Environment]::Is64BitOperatingSystem)) {
  $NinjaRegPath = 'HKLM:\SOFTWARE\NinjaRMM LLC\NinjaRMMAgent'
  $UninstallRegPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
}

$NinjaInstallLocation = (Get-ItemPropertyValue $NinjaRegPath -Name Location).Replace('/', '\')

if (!(Test-Path "$($NinjaInstallLocation)\NinjaRMMAgent.exe")) {
  $NinjaServicePath = ((Get-WMIObject Win32_Service | Where-Object { $_.Name -eq 'NinjaRMMAgent' }).PathName).Trim('"')
  if (!(Test-Path $NinjaServicePath)) {
    Write-LogEntry 'Unable to locate Ninja installation path. Continuing with cleanup...'
  }
  else {
    $NinjaInstallLocation = $NinjaServicePath | Split-Path
  }
}

Start-Process "$NinjaInstallLocation\NinjaRMMAgent.exe" -ArgumentList "-disableUninstallPrevention NOUI" -Wait -NoNewWindow
$UninstallString = (Get-ItemProperty $UninstallRegPath | Where-Object { ($_.DisplayName -eq 'NinjaRMMAgent') -and ($_.UninstallString -match 'msiexec') }).UninstallString

if (!($UninstallString)) {
  Write-LogEntry 'Unable to to determine uninstall string. Continuing with cleanup...' 
}
else {
  $UninstallString = $UninstallString.Split('X')[1]
  Uninstall-NinjaMSI
}

$NinjaServices = @('NinjaRMMAgent', 'nmsmanager', 'lockhart')
$Processes = @("NinjaRMMAgent", "NinjaRMMAgentPatcher", "njbar", "NinjaRMMProxyProcess64")

foreach ($Process in $Processes) {
  if ($GetP = Get-Process $Process) {
    try {
      Stop-Process $GetP -Force -ErrorAction Stop
      Write-LogEntry "Successfully stopped process: $($GetP.Name)"
    }
    catch {
      Write-LogEntry "Unable to stop $($GetP.Name) for the following reason:"
      Write-LogEntry "$($_.Exception.Message). Continuing..."
    }
  }
}

foreach ($NS in $NinjaServices) {
  if ($NS -eq 'lockhart' -and !(Test-Path "$NinjaInstallLocation\lockhart\bin\lockhart.exe")) {
    continue
  }
  if (Get-Service $NS) {
    try {
      Write-LogEntry "Stopping service $($NS)..."
      Stop-Service $NS -Force -ErrorAction Stop
    }
    catch {
      Write-LogEntry "Unable to stop $($NS) service..."
      Write-LogEntry "$($_.Exception.Message)"
      Write-LogEntry 'Attempting to remove service...'
    }
  
    & sc.exe DELETE $NS
    Start-Sleep 5
    if (Get-Service $NS) {
      Write-LogEntry "Failed to remove $($NS) service. Continuing with remaining removal steps..."
    }
    else {
      Write-LogEntry "Successfully removed $($NS) service."
    }
  }
}

if (Test-Path $NinjaInstallLocation) {
  Write-LogEntry 'Removing Ninja installation directory:'
  Write-LogEntry "$($NinjaInstallLocation)"
  try {
    Remove-Item $NinjaInstallLocation -Recurse -Force -ErrorAction Stop
    Write-LogEntry 'Successfully removed.'
  }
  catch {
    Write-LogEntry 'Failed to remove Ninja installation directory.'
    Write-LogEntry "$($_.Exception.Message)"
    Write-LogEntry 'Continuing with removal attempt...'
  }
}

if (Test-Path $NinjaDataDirectory) {
  Write-LogEntry 'Removing Ninja data directory:'
  Write-LogEntry "$($NinjaDataDirectory)"
  try {
    Remove-Item $NinjaDataDirectory -Recurse -Force -ErrorAction Stop
    Write-LogEntry 'Successfully removed.'
  }
  catch {
    Write-LogEntry 'Failed to remove Ninja data directory.'
    Write-LogEntry "$($_.Exception.Message)"
    Write-LogEntry 'Continuing with removal attempt...'
  }
}

if (Test-Path $NinjaModulePath) {
  Write-LogEntry 'Removing Ninja data directory:'
  Write-LogEntry "$($NinjaModulePath)"
  try {
    Remove-Item $NinjaModulePath -Recurse -Force -ErrorAction Stop
    Write-LogEntry 'Successfully removed.'
  }
  catch {
    Write-LogEntry 'Failed to remove Ninja PowerShell module directory.'
    Write-LogEntry "$($_.Exception.Message)"
    Write-LogEntry 'Continuing with removal attempt...'
  }
}

$MSIWrapperReg = 'HKLM:\SOFTWARE\WOW6432Node\EXEMSI.COM\MSI Wrapper\Installed'
$ProductInstallerReg = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products'
$HKCRInstallerReg = 'Registry::\HKEY_CLASSES_ROOT\Installer\Products'

$RegKeysToRemove = [System.Collections.Generic.List[object]]::New()

(Get-ItemProperty $UninstallRegPath | Where-Object { $_.DisplayName -eq 'NinjaRMMAgent' }).PSPath | ForEach-Object { $RegKeysToRemove.Add($_) }
(Get-ItemProperty $ProductInstallerReg | Where-Object { $_.ProductName -eq 'NinjaRMMAgent' }).PSPath | ForEach-Object { $RegKeysToRemove.Add($_) }
(Get-ChildItem $MSIWrapperReg | Where-Object { $_.Name -match 'NinjaRMMAgent' }).PSPath | ForEach-Object { $RegKeysToRemove.Add($_) }
Get-ChildItem $HKCRInstallerReg | ForEach-Object { if ((Get-ItemPropertyValue $_.PSPath -Name 'ProductName') -eq 'NinjaRMMAgent') { $RegKeysToRemove.Add($_.PSPath) } }

$ProductInstallerKeys = Get-ChildItem $ProductInstallerReg | Select-Object *
foreach ($Key in $ProductInstallerKeys) {
  $KeyName = $($Key.Name).Replace('HKEY_LOCAL_MACHINE', 'HKLM:') + "\InstallProperties"
  if (Get-ItemProperty $KeyName | Where-Object { $_.DisplayName -eq 'NinjaRMMAgent' }) {
    $RegKeysToRemove.Add($Key.PSPath)
  }
}

Write-LogEntry 'Removing registry items if found...'
if (($RegKeysToRemove | Measure-Object).Count -gt 0 ) {
  foreach ($RegKey in $RegKeysToRemove) {
    if (!([string]::IsNullOrWhiteSpace($RegKey))) {
      Write-LogEntry "Attempting to remove: $($RegKey)"
      try { 
        Remove-Item $RegKey -Recurse -Force -ErrorAction Stop
        Write-LogEntry 'Successfully removed.'
      }
      catch {
        Write-LogEntry 'Failed to remove registry key.'
        Write-LogEntry "$($_.Exception.Message)"
        Write-LogEntry "Continuing with removal..."
      }
    }
  }
}

if (Test-Path $NinjaRegPath) {
  try {
    Write-LogEntry "Removing: $($NinjaRegPath)"
    Get-Item ($NinjaRegPath | Split-Path -ErrorAction Stop) | Remove-Item -Recurse -Force -ErrorAction Stop
    Write-LogEntry 'Successfully removed.'
  }
  catch {
    Write-LogEntry 'Failed to remove key.'
    Write-LogEntry "$($_.Exception.Message)"
    Write-LogEntry "Continuing with removal..."
  }
}

#Checks for rogue reg entry from older installations where ProductName was missing
#Filters out a Windows Common GUID that doesn't have a ProductName
$Child = Get-ChildItem 'HKLM:\Software\Classes\Installer\Products'
$MissingPNs = [System.Collections.Generic.List[object]]::New()

foreach ($C in $Child) {
  if ($C.Name -match '99E80CA9B0328e74791254777B1F42AE') {
    continue
  }
  try {
    Get-ItemPropertyValue $C.PSPath -Name 'ProductName' -ErrorAction Stop | Out-Null
  }
  catch {
    $MissingPNs.Add($($C.Name))
  } 
}

##Begin Ninja Remote Removal##
$NR = 'ncstreamer'

if (Get-Process $NR) {
  Write-LogEntry 'Stopping Ninja Remote process...'
  try {
    Get-Process $NR | Stop-Process -Force
  }
  catch {
    Write-LogEntry 'Unable to stop the Ninja Remote process...'
    Write-LogEntry "$($_.Exception.Message)"
    Write-LogEntry 'Continuing to Ninja Remote service...'
  }
}

if (Get-Service $NR) {
  try {
    Stop-Service $NR -Force
  }
  catch {
    Write-LogEntry 'Unable to stop the Ninja Remote service...'
    Write-LogEntry "$($_.Exception.Message)"
    Write-LogEntry 'Attempting to remove service...'
  }

  & sc.exe DELETE $NR
  Start-Sleep 5
  if (Get-Service $NR) {
    Write-LogEntry 'Failed to remove Ninja Remote service. Continuing with remaining removal steps...'
  }
}

$NRDriver = 'nrvirtualdisplay.inf'
$DriverCheck = pnputil /enum-drivers | Where-Object { $_ -match "$NRDriver" }
if ($DriverCheck) {
  Write-LogEntry 'Ninja Remote Virtual Driver found. Removing...'
  $DriverBreakdown = pnputil /enum-drivers | Where-Object { $_ -ne 'Microsoft PnP Utility' }

  $DriversArray = [System.Collections.Generic.List[object]]::New()
  $CurrentDriver = @{}
    
  foreach ($Line in $DriverBreakdown) {
    if ($Line -ne "") {
      $ObjectName = $Line.Split(':').Trim()[0]
      $ObjectValue = $Line.Split(':').Trim()[1]
      $CurrentDriver[$ObjectName] = $ObjectValue
    }
    else {
      if ($CurrentDriver.Count -gt 0) {
        $DriversArray.Add([PSCustomObject]$CurrentDriver)
        $CurrentDriver = @{}
      }
    }
  }

  $DriverToRemove = ($DriversArray | Where-Object { $_.'Provider Name' -eq 'NinjaOne' }).'Published Name'
  pnputil /delete-driver "$DriverToRemove" /force
}

$NRDirectory = "$($env:ProgramFiles)\NinjaRemote"
if (Test-Path $NRDirectory) {
  Write-LogEntry "Removing directory: $NRDirectory"
  Remove-Item $NRDirectory -Recurse -Force
  if (Test-Path $NRDirectory) {
    Write-LogEntry 'Failed to completely remove Ninja Remote directory at:'
    Write-LogEntry "$NRDirectory"
    Write-LogEntry 'Continuing to registry removal...'
  }
}

$NRHKUReg = 'Registry::\HKEY_USERS\S-1-5-18\Software\NinjaRMM LLC'
if (Test-Path $NRHKUReg) {
  Remove-Item $NRHKUReg -Recurse -Force
}

function Remove-NRRegistryItems {
  param (
    [Parameter(Mandatory = $true)]
    [string]$SID
  )
  $NRRunReg = "Registry::\HKEY_USERS\$SID\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
  $NRRegLocation = "Registry::\HKEY_USERS\$SID\Software\NinjaRMM LLC"
  if (Test-Path $NRRunReg) {
    $RunRegValues = Get-ItemProperty -Path $NRRunReg
    $PropertyNames = $RunRegValues.PSObject.Properties | Where-Object { $_.Name -match "NinjaRMM|NinjaOne" } 
    foreach ($PName in $PropertyNames) {    
      Write-LogEntry "Removing item..."
      Write-LogEntry "$($PName.Name): $($PName.Value)"
      Remove-ItemProperty $NRRunReg -Name $PName.Name -Force
    }
  }
  if (Test-Path $NRRegLocation) {
    Write-LogEntry "Removing $NRRegLocation..."
    Remove-Item $NRRegLocation -Recurse -Force
  }
  Write-LogEntry 'Registry removal completed.'
}

$AllProfiles = Get-CimInstance Win32_UserProfile | Select-Object LocalPath, SID, Loaded, Special | 
Where-Object { $_.SID -like "S-1-5-21-*" }
$Mounted = $AllProfiles | Where-Object { $_.Loaded -eq $true }
$Unmounted = $AllProfiles | Where-Object { $_.Loaded -eq $false }

$Mounted | Foreach-Object {
  Write-LogEntry "Removing registry items for $($_.LocalPath)"
  Remove-NRRegistryItems -SID "$($_.SID)"
}

$Unmounted | ForEach-Object {
  $Hive = "$($_.LocalPath)\NTUSER.DAT"
  if (Test-Path $Hive) {      
    Write-LogEntry "Loading hive and removing Ninja Remote registry items for $($_.LocalPath)..."

    REG LOAD HKU\$($_.SID) $Hive 2>&1>$null

    Remove-NRRegistryItems -SID "$($_.SID)"
        
    [GC]::Collect()
    [GC]::WaitForPendingFinalizers()
          
    REG UNLOAD HKU\$($_.SID) 2>&1>$null
  } 
}

$NRPrinter = Get-Printer | Where-Object { $_.Name -eq 'NinjaRemote' }

if ($NRPrinter) {
  Write-LogEntry 'Removing Ninja Remote printer...'
  Remove-Printer -InputObject $NRPrinter
}

$NRPrintDriverPath = "$env:SystemDrive\Users\Public\Documents\NrSpool\NrPdfPrint"
if (Test-Path $NRPrintDriverPath) {
  Write-LogEntry 'Removing Ninja Remote printer driver...'
  Remove-Item $NRPrintDriverPath -Force
}

Write-LogEntry 'Removal of Ninja Remote complete.'
##End Ninja Remote Removal##

if ($MissingPNs) {
  Write-LogEntry '############################# !!! WARNING !!! ####################################'
  Write-LogEntry 'Some registry keys are missing the Product Name.'
  Write-LogEntry 'This could be an indicator of a corrupt Ninja install key.'
  Write-LogEntry 'If you are still unable to install the NinjaOne Agent after running this script...'
  Write-LogEntry 'Please make a backup of the following keys and then remove them from the registry:'
  Write-LogEntry ( $MissingPNs | Out-String )
  Write-LogEntry '##################################################################################'
}

Write-LogEntry 'Removal script completed. Please review if any errors displayed.'

What gets cleaned up

Services stopped and deleted: NinjaRMMAgent, nmsmanager, lockhart, ncstreamer. Processes killed if running: NinjaRMMAgent, NinjaRMMAgentPatcher, njbar, NinjaRMMProxyProcess64, ncstreamer.

Files and directories: the Ninja install location (typically %ProgramFiles(x86)%\\NinjaRMMAgent), the data directory at %ProgramData%\\NinjaRMMAgent, the NJCliPSh PowerShell module under %ProgramFiles%\\WindowsPowerShell\\Modules, and the Ninja Remote directory at %ProgramFiles%\\NinjaRemote.

Registry: every Uninstall entry for NinjaRMMAgent, every MSI Wrapper key, every Installer Products entry across HKLM and HKEY_CLASSES_ROOT, the Ninja Remote keys under each loaded user hive, and the Run keys per user that auto-launch the agent. The script also handles unloaded user profiles by mounting their NTUSER.DAT, cleaning, and unmounting cleanly.

Driver and printer: the Ninja Remote virtual display driver (nrvirtualdisplay.inf) is removed via pnputil, the Ninja Remote printer is removed, and the print spool driver folder under Users\\Public\\Documents\\NrSpool is cleared.

Verifying it worked

After the script finishes, run a quick check.

  • Get-Service NinjaRMMAgent: should return an error saying the service does not exist.
  • Get-Process NinjaRMMAgent: should return nothing.
  • Test-Path "$env:ProgramData\\NinjaRMMAgent": should return False.
  • Reboot the machine and re-check. Anything that comes back was either reinstalled by a deployment policy or never actually removed.
  • Check the log at %windir%\\temp\\NinjaOneAgentRemoval.log for any "Failed to" or "Unable to" lines, these usually mean a file or registry key was locked by another process and may need a reboot to clear.

A note on attribution

The script itself comes from NinjaOne’s own support documentation, they publish it specifically for cases where Add/Remove Programs cannot finish the uninstall. We use it on customer machines often enough that having it on hand, with a copy-button and a download link, is faster than going back to their support site every time. If NinjaOne update theirs, we will update ours.

Useful? See more of us on Google.

One click adds COR Intelligence to your preferred sources in Search and its AI features.

Read next

More from COR Intelligence