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.

Automate New-User Onboarding in Active Directory

This guide builds a PowerShell script that onboards one new employee in four steps: it creates an Active Directory user account , adds them to security groups , provisions an on-premises Exchange mailbox , and creates their home folder on a file server and sets NTFS permissions . The purpose is to replace the error-prone click-through in Active Directory Users and Computers with one repeatable, reviewable run.

10 min read

Reset and Unlock AD User Accounts in Bulk with PowerShell

This guide resets the passwords of a list of Active Directory user accounts, clears any lockout on them, and flags each account to require a new password at the next sign-in. The usual reason is a batch of expired or compromised accounts, or a group of seasonal staff coming back online.

8 min read

Bulk-Disable and Archive Inactive AD Accounts with PowerShell

This procedure finds enabled user accounts that have not logged on for a set number of days , disables them, stamps a note in the account's description, and moves them into a dedicated "archive" OU so they are out of your working OUs but not deleted. Its purpose is routine account hygiene: a disabled, quarantined account can't be used for a logon, which shrinks your attack surface, but nothing is destroyed and everything is reversible.

9 min read

Find and Report AD Accounts That Have Never Logged In

This guide gives you a PowerShell script that reads Active Directory and produces a report (on screen and as a CSV) of user accounts that have never authenticated against any domain controller. It changes nothing — it does not disable, delete, or edit a single account. Its purpose is to hand you a clean list of candidates for review before you decide what to do with them.

9 min read