Most domains carry dead weight. Laptops were reimaged under new names, lab VMs were deleted from the hypervisor but never from AD, and a whole branch office closed years ago. Stale computer objects make reports lie, clutter the OUs where GPOs apply, and give you “enabled” accounts nobody is watching. Cleaning them up is simple, but it has to be done in the right order. Deleting too early can cost you BitLocker recovery keys or break a cluster. This guide uses a three-stage approach (report, disable and move, delete later) with the ActiveDirectory module from RSAT.
What you need
- The ActiveDirectory PowerShell module. On Windows 10/11 it comes with RSAT as a Feature on Demand. On Windows Server it’s a feature:
# Windows 10/11 client (elevated) Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 # Windows Server Install-WindowsFeature RSAT-AD-PowerShell - Delegated rights, not Domain Admins. Your account needs to read computer objects (every authenticated user can), write
userAccountControlanddescription, and move and delete computer objects in the source OUs and the quarantine OU. Delegate these on the OUs with the Delegation of Control wizard, and use that account for the job. - A quarantine OU, for example
OU=Disabled Computers,DC=corp,DC=example,DC=com, with no GPOs linked except perhaps one that blocks logon. - The AD Recycle Bin, ideally. If it’s not on yet, enabling it is a one-time, one-way forest change, so agree it with whoever owns the forest:
Enable-ADOptionalFeature 'Recycle Bin Feature' -Scope ForestOrConfigurationSet -Target 'corp.example.com'
How “inactive” is measured, and why it’s fuzzy
AD has two last-logon attributes. lastLogon is precise but isn’t replicated: each DC keeps its own. lastLogonTimestamp is replicated, but on purpose it only updates when the stored value is roughly 9–14 days old. That’s the default msDS-LogonTimeSyncInterval of 14 days, minus a random offset. Search-ADAccount -AccountInactive and the LastLogonDate property both use the replicated value. That makes them fine for “not seen in 90 days” and useless for “not seen since Tuesday”.
A useful second signal is PasswordLastSet. Domain members change their machine account password every 30 days by default. A computer whose password hasn’t changed in 90+ days probably hasn’t been talking to a DC.
Step 1 — Report (change nothing yet)
Import-Module ActiveDirectory
$cutoff = (Get-Date).AddDays(-90)
$stale = Get-ADComputer -Filter { Enabled -eq $true -and LastLogonTimeStamp -lt $cutoff } `
-Properties LastLogonDate, PasswordLastSet, OperatingSystem, Description, DistinguishedName |
Where-Object { $_.PasswordLastSet -lt $cutoff }
$stale | Select-Object Name, OperatingSystem, LastLogonDate, PasswordLastSet, DistinguishedName |
Sort-Object LastLogonDate |
Export-Csv C:\Admin\stale-computers.csv -NoTypeInformation
The same query with the purpose-built cmdlet, if you prefer:
Search-ADAccount -AccountInactive -ComputersOnly -TimeSpan 90.00:00:00 `
-SearchBase 'OU=Workstations,DC=corp,DC=example,DC=com'
Computers that have never logged on have an empty LastLogonTimeStamp, so the first filter skips them. List them separately by whenCreated. A PC created three days ago and still in the build room isn’t stale.
Step 2 — Remove the exceptions
Send the CSV to the people who know the estate before you touch anything. Take out:
- Cluster name objects and virtual computer objects (failover clusters, SQL availability groups). Their “logon” pattern looks idle but they are in use.
- Anything under a Domain Controllers or server OU, until a server owner confirms it.
- Devices known to be on long leave: spare laptops in a cupboard, shipboard or seasonal-site machines.
- Objects with BitLocker recovery information you might still need (see the mistakes section).
Step 3 — Disable, stamp and move
Disabling is reversible within seconds. The description stamp tells the next admin what happened and when:
$approved = Import-Csv C:\Admin\stale-computers-approved.csv
$target = 'OU=Disabled Computers,DC=corp,DC=example,DC=com'
$stamp = "Disabled $(Get-Date -Format yyyy-MM-dd) by $env:USERNAME - stale cleanup"
foreach ($c in $approved) {
$obj = Get-ADComputer -Identity $c.Name
Disable-ADAccount -Identity $obj -WhatIf
Set-ADComputer -Identity $obj -Description $stamp -WhatIf
Move-ADObject -Identity $obj.DistinguishedName -TargetPath $target -WhatIf
}
Run it with -WhatIf first, read the output, then remove the switch. If someone calls to say their PC “lost trust”, re-enable the account and move it back. The machine password hasn’t changed, so the secure channel usually recovers after a reboot.
Step 4 — Delete after a holding period
Wait a fixed period, 30 to 60 days is common, then delete what’s still in quarantine:
Get-ADComputer -SearchBase 'OU=Disabled Computers,DC=corp,DC=example,DC=com' -Filter { Enabled -eq $false } -Properties whenChanged |
Where-Object { $_.whenChanged -lt (Get-Date).AddDays(-45) } |
ForEach-Object { Remove-ADObject -Identity $_.DistinguishedName -Recursive -Confirm:$false -WhatIf }
Use Remove-ADObject -Recursive rather than Remove-ADComputer. Computer objects often have child objects (BitLocker recovery entries, service connection points), and Remove-ADComputer refuses to delete a non-leaf object.
Common mistakes
- Deleting before exporting BitLocker keys. If recovery passwords are escrowed to AD, they are child objects of the computer. Delete the computer and the keys go with it (recoverable only through the Recycle Bin, and only for its lifetime). Export them, or check that the disk has been wiped, first.
- Treating
LastLogonDateas exact. A 14-day uncertainty is built in. Don’t use 15- or 30-day thresholds. - Running the whole domain in one go. Start with one workstation OU and check a week of helpdesk tickets before you widen the scope.
- Skipping the stamp. Six months later, nobody remembers why a machine was disabled.
Tools that make this a recurring job
RSAT and PowerShell cost nothing and cover all of this; see our RSAT review. If you’d rather have scheduled stale-object reports and cleanup driven from a web console with approvals, ManageEngine ADManager Plus lists stale-account cleanup and inactive-object reports among its features. To keep an independent record of who disabled or deleted what, pair either approach with a change-auditing product such as Netwrix Auditor. The difference between those two products is covered in ADManager Plus vs Netwrix Auditor, and the wider field is in Active Directory Management & Auditing.