Reading Time: 5 minutes

Every AD environment I’ve inherited has had the same problem. Hundreds of GPOs, half of them doing nothing, and nobody willing to delete any of them because nobody knows which ones are actually in use. So they sit there. They slow down gpresult, or They confuse the next admin. They make troubleshooting a mess because you can’t tell at a glance which policies matter.

I’ve stopped trying to clean these up by hand. PowerShell handles it in about thirty seconds, and the safest first move isn’t deletion. It’s renaming. I prefix anything suspect with Review - and let the rename sit for a quarter. If nothing screams, then I delete.

Here’s how I do it.

What counts as a candidate

Two categories I go after first.

Empty GPOs. No user settings, no computer settings. Someone created them, never configured them, and walked away. These are pure noise.

Unlinked GPOs. They have settings, but they aren’t linked to any OU, site, or domain. They don’t apply to anything. Sometimes these were intentionally unlinked during an incident and never cleaned up. Sometimes they were duplicates created during a migration. Either way, if nothing is linked to them, they aren’t doing work.

I do not lump these together in the rename. I want to know which is which when I review later.

What you need

The GroupPolicy module, which ships with RSAT. Run from a domain-joined machine with rights to read and modify GPOs. I usually do this from a jump box rather than a DC, but either works.

Import-Module GroupPolicy

If that fails, install RSAT first. On Windows 11 it’s under Optional Features, RSAT: Group Policy Management Tools.

Finding empty GPOs

The trick here is that Get-GPO doesn’t tell you whether a GPO has settings. You have to look at the XML report and check the user and computer extension data. If both are empty, the GPO has no settings.

$emptyGPOs = @()

Get-GPO -All | ForEach-Object {
    [xml]$report = Get-GPOReport -Guid $_.Id -ReportType Xml
    
    $userConfigured = $report.GPO.User.ExtensionData
    $computerConfigured = $report.GPO.Computer.ExtensionData
    
    if (-not $userConfigured -and -not $computerConfigured) {
        $emptyGPOs += $_
    }
}

$emptyGPOs | Select-Object DisplayName, Id, CreationTime, ModificationTime

The ExtensionData node only exists when something is actually configured under that side of the policy. If both come back null, the GPO is empty.

Run it once and review the list before you do anything else. I always find at least one GPO in the list that I created myself months ago and forgot about, which is humbling.

Finding unlinked GPOs

Same approach, different XML node. The LinksTo property tells you where a GPO is linked. If it’s null, nothing points at the GPO.

$unlinkedGPOs = @()

Get-GPO -All | ForEach-Object {
    [xml]$report = Get-GPOReport -Guid $_.Id -ReportType Xml
    
    if (-not $report.GPO.LinksTo) {
        $unlinkedGPOs += $_
    }
}

$unlinkedGPOs | Select-Object DisplayName, Id, CreationTime, ModificationTime

One thing to watch for. A GPO can be linked but disabled at the link level. That’s a different state, and I don’t treat those as unlinked. They were intentionally turned off, usually for a reason. If you want to catch those too, the XML has an Enabled attribute on each LinksTo entry. But for a first pass, I leave them alone.

Renaming, not deleting

This is the part that matters. Don’t delete.

I learned this the hard way years ago. Deleted what I thought was a dead GPO, found out two weeks later it was applying a registry setting to a single workstation that nobody had documented, and spent an afternoon rebuilding it from a backup. Now I rename and wait.

The rename is one line per GPO:

foreach ($gpo in $emptyGPOs) {
    $newName = "Review - $($gpo.DisplayName)"
    Set-GPO -Guid $gpo.Id -DisplayName $newName
    Write-Host "Renamed: $($gpo.DisplayName) -> $newName"
}

foreach ($gpo in $unlinkedGPOs) {
    $newName = "Review - $($gpo.DisplayName)"
    Set-GPO -Guid $gpo.Id -DisplayName $newName
    Write-Host "Renamed: $($gpo.DisplayName) -> $newName"
}

A few notes on the rename itself.

I use Review - with a space, hyphen, space because it sorts cleanly in GPMC. Everything you flag bunches together at the top of the list, which makes it obvious at a glance what’s pending review.

I don’t add a date or my initials in the rename. The GPO already tracks modification time, and you can see who last touched it in the change history. Adding metadata to the display name just makes it harder to read.

If a GPO is both empty and unlinked, my script renames it twice and you end up with Review - Review - GPO Name. Worth deduplicating before the loop if you care:

$candidates = ($emptyGPOs + $unlinkedGPOs) | Sort-Object Id -Unique

Back up before you rename

This should be obvious but I’ll say it anyway. Back up every GPO you’re about to rename. The rename itself is reversible, but if you’re going to come back later and delete, you want the backups ready.

$backupPath = "C:\GPOBackups\$(Get-Date -Format 'yyyy-MM-dd')"
New-Item -Path $backupPath -ItemType Directory -Force | Out-Null

foreach ($gpo in $candidates) {
    Backup-GPO -Guid $gpo.Id -Path $backupPath | Out-Null
}

Each backup gets its own folder under the dated parent, and Restore-GPO can pull any of them back if you need to.

The review window

After the rename, I leave it alone for about 90 days. Long enough to catch monthly and quarterly processes. If something breaks because a renamed GPO was actually doing work, I’ll hear about it, and the GPO is still there with its settings intact.

At the 90-day mark, I pull the list of Review - GPOs:

Get-GPO -All | Where-Object { $_.DisplayName -like "Review - *" }

Anything still on that list with no complaints attached gets backed up one more time and deleted. Anything that did get flagged during the window gets renamed back to its original name (or a better one) and properly documented this time.

Why this works

The rename approach gives you a passive audit. You aren’t asking anyone to confirm whether a GPO is in use, because nobody will know, and the people who do know are usually gone. You’re letting the environment tell you. If nothing breaks for a quarter, the GPO wasn’t doing anything that mattered.

It also makes the cleanup defensible. When someone asks why you deleted a GPO six months later, you can point to the rename date, the review window, and the backup. That’s a much better conversation than I thought it was dead.

I run this whole workflow about once a year on environments I own. The first run usually catches 20 to 40 dead GPOs in a medium-sized environment. After that it’s smaller, but it’s never zero. Stuff accumulates.

Tying it back

In my last post I wrote about working inside the structure the company gives you instead of fighting it. This is what that looks like at the keyboard. I’m not pitching a new product to solve GPO sprawl, or asking for budget. I am not building a case for a third-party tool that does the same thing in a prettier UI. I’m using PowerShell, which is already on the box, against AD, which is already running, with a workflow that’s reversible at every step.

That’s most of the job, honestly. The improvements that actually stick come from understanding what you already own and making it work better. Renaming a dead GPO doesn’t make for a great war story, and you won’t get a Slack shoutout for it. But the next admin who inherits this environment will be able to read the GPMC console without squinting, and that’s worth more than most of the bigger projects I’ve been asked to pitch.

Work inside the structure. Use the tools you have. Document what you changed. The rest takes care of itself.