Shore Up
A cabinet of index cards where an archivist pulls out dusty, cobwebbed cards, sets them aside in a holding tray, and only later drops the oldest into a shredder.
WindowsSecurity

Clean Up Stale Computer Accounts in Active Directory with PowerShell

Ketan Aagja8 min read
No ratings yet

Before you run this

This guide finds Active Directory computer accounts that haven't logged in for a long time and retires them in three deliberate stages: report, disable and move to a holding OU, then delete. The purpose is to keep AD tidy and reduce the attack surface of forgotten machine accounts without accidentally killing a computer that's simply been powered off for a while.

Privileges: you need a domain account with rights to modify and delete computer objects — in practice a member of a delegated group or Domain Admins. Run these commands in an elevated PowerShell session on a domain-joined machine that has the ActiveDirectory module installed (part of RSAT on a workstation, or the AD DS role tools on a server). The reporting steps are read-only; the disable, move, and delete steps make changes and require write access.

Test first. Read every command before you run it. The first stage only reads AD — start there. When you move to disabling and deleting, run it against one test computer object you create for the purpose, or use -WhatIf so nothing actually changes until you've seen what it would touch. Never point the delete step at a broad scope on a live domain the first time.

What changes, and what's irreversible. Disabling and moving an object is easily reversed (re-enable, move it back). Deletion is not. A deleted computer object may be recoverable from the AD Recycle Bin only if that feature is enabled in your forest — do not assume it is. Once a computer object is gone, that machine can no longer authenticate with its existing trust and will need to be rejoined to the domain. Treat the delete stage with the caution it deserves.

Assumptions

  • Windows Server 2019/2022 domain, or a Windows 10/11 admin workstation with RSAT installed.
  • Windows PowerShell 5.1 with the ActiveDirectory module (Import-Module ActiveDirectory). PowerShell 7 works too if you import the module explicitly.
  • A single domain. Replace every placeholder — example.com, "OU=Disabled Computers,DC=example,DC=com" — with your real values.

One important caveat before you rely on the timestamps below: the LastLogonDate property is derived from lastLogonTimestamp, which by design only replicates every 9–14 days. So a machine's "last logon" can lag reality by up to two weeks. That's why 90 days is a sane, conservative threshold and 30 is not.

Stage 1 — Report, and only report

Start by listing candidates so you and (ideally) the machine owners can eyeball them. Search-ADAccount has a built-in switch for exactly this.

Import-Module ActiveDirectory

# Computers with no activity in the last 90 days.
# -TimeSpan takes a d.hh:mm:ss string; "90.00:00:00" is 90 days.
$stale = Search-ADAccount -AccountInactive -ComputersOnly -TimeSpan "90.00:00:00" |
    Where-Object { $_.Enabled -eq $true }

$stale |
    Select-Object Name, LastLogonDate, DistinguishedName |
    Sort-Object LastLogonDate |
    Format-Table -AutoSize

Export it so you have a record and something to review before you touch anything:

$stale |
    Select-Object Name, LastLogonDate, DistinguishedName |
    Sort-Object LastLogonDate |
    Export-Csv -Path "C:\path\to\stale-computers.csv" -NoTypeInformation

If you'd rather build the filter yourself, Get-ADComputer works too and lets you also inspect PasswordLastSet, which is another good staleness signal (a live domain member rotates its machine password roughly every 30 days):

$cutoff = (Get-Date).AddDays(-90)
Get-ADComputer -Filter { LastLogonTimeStamp -lt $cutoff -and Enabled -eq $true } `
    -Properties LastLogonDate, PasswordLastSet |
    Select-Object Name, LastLogonDate, PasswordLastSet |
    Sort-Object LastLogonDate |
    Format-Table -AutoSize

Look through this list. Exclude anything you know is legitimately offline long-term — lab boxes, seasonal kiosks, cluster name objects, imaging templates. Don't skip this human step.

Stage 2 — Disable and move to a holding OU

The safe pattern is never "delete straight from the list." Instead, disable the account (so it can't authenticate) and move it to a dedicated OU where it sits for a grace period — say 30 days. If someone screams, you re-enable and move it back. If nobody does, it gets deleted later.

Create the holding OU first (once), either in Active Directory Users and Computers or with New-ADOrganizationalUnit. Then:

$targetOU = "OU=Disabled Computers,DC=example,DC=com"   # your holding OU

# Preview first — -WhatIf shows what WOULD happen, changes nothing.
foreach ($comp in $stale) {
    Disable-ADAccount -Identity $comp.DistinguishedName -WhatIf
    Move-ADObject   -Identity $comp.DistinguishedName -TargetPath $targetOU -WhatIf
}

Read that output carefully. When you're satisfied it's hitting the right objects, remove -WhatIf from both lines and run it for real. It's worth stamping a note on each object as you go so future-you knows why it was disabled:

foreach ($comp in $stale) {
    Disable-ADAccount -Identity $comp.DistinguishedName
    Set-ADComputer   -Identity $comp.DistinguishedName `
        -Description "Disabled $(Get-Date -Format yyyy-MM-dd): stale >90d"
    Move-ADObject    -Identity $comp.DistinguishedName -TargetPath $targetOU
}

Run the whole foreach on a single test object first. Create a throwaway computer object, put it in scope, and watch it get disabled and moved before you trust the loop on the real set.

Stage 3 — Delete, after the grace period

After the objects have sat disabled in the holding OU for your grace window with no complaints, delete them. Scope the delete to the holding OU only — this is the guardrail that stops a bad filter from deleting live machines.

$targetOU = "OU=Disabled Computers,DC=example,DC=com"

# Only objects that are STILL disabled AND have sat for 30+ days,
# and only within the holding OU.
$grace = (Get-Date).AddDays(-30)
$toDelete = Get-ADComputer -SearchBase $targetOU -Filter { Enabled -eq $false } `
    -Properties whenChanged |
    Where-Object { $_.whenChanged -lt $grace }

# ALWAYS preview first.
$toDelete | Remove-ADComputer -WhatIf

When — and only when — the -WhatIf output is exactly what you intend to remove, run the deletion. Remove-ADComputer prompts for confirmation by default; keep that prompt in place rather than suppressing it:

$toDelete | Remove-ADComputer   # answer the confirmation prompt per object, or "Yes to All"

Edge case: if an object has child objects (some clustered or specially configured machines do), Remove-ADComputer will refuse. Rather than reach for a recursive delete reflexively, inspect why it has children first. Remove-ADObject -Recursive exists for that situation — check Microsoft Learn for its exact behaviour before using it, and never point it at anything but a single, verified object.

Verify

Confirm the disabled/moved objects landed where you expect:

Get-ADComputer -SearchBase "OU=Disabled Computers,DC=example,DC=com" -Filter * |
    Select-Object Name, Enabled | Format-Table -AutoSize

Confirm a deleted object is actually gone — this should return an error saying it can't be found, which is what you want:

Get-ADComputer -Identity "OldPC01"   # expect: cannot find object

Undo

  • Re-enable a wrongly disabled machine: Enable-ADAccount -Identity "OldPC01".
  • Move it back to its original OU: Move-ADObject -Identity <DN> -TargetPath "OU=Workstations,DC=example,DC=com".
  • A deleted object can only be restored if the AD Recycle Bin is enabled in your forest. If it is, restore it with Restore-ADObject (check Microsoft Learn for the exact syntax and how to find the deleted object first). If the Recycle Bin is not enabled, the object is gone and the computer must be rejoined to the domain.

That staged rhythm — report, disable-and-park, then delete on a delay — is the whole point. It gives every real machine a chance to check back in before anything becomes irreversible.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.