How to Find Duplicate Files Using PowerShell

To find duplicate files with PowerShell, hash every file with Get-FileHash and group the results by hash. Files with the same hash have the same content, whatever their names are:

Get-ChildItem -Path C:\Shared -File -Recurse | Get-FileHash | Group-Object -Property Hash | Where-Object Count -GT 1

Below I run it on a sample folder, speed it up for big folders, catch same-name files that differ, plan a safe cleanup and compare two folders.

Find duplicates by content

My sample Shared folder has an office photo saved three times under different names, and a note saved twice. To keep the output short, I print paths relative to the folder:

$root = (Resolve-Path -Path .\Shared).Path + '\'

Get-ChildItem -Path .\Shared -File -Recurse |
    Get-FileHash -Algorithm SHA256 |
    Group-Object -Property Hash |
    Where-Object Count -GT 1 |
    Sort-Object -Property Count -Descending |
    ForEach-Object {
        "Duplicate set ($($_.Count) files):"
        $_.Group | Sort-Object -Property Path | ForEach-Object { '  ' + $_.Path.Replace($root, '') }
    }

Output:

Duplicate set (3 files):
  Docs\Old\office.jpg
  Photos\austin-office.jpg
  Photos\copy-of-austin-office.jpg
Duplicate set (2 files):
  Docs\backup-notes.txt
  Docs\notes.txt
PowerShell find duplicate files with Get-FileHash
Two sets of duplicate files found by hash (PowerShell 7)

The two budget files share a name but have different content, so they’re correctly left out. SHA256 is the default algorithm, and a matching hash means the bytes are identical. To hash a single file, see getting the hash of a file.

Speed it up on big folders

Hashing reads every byte, which takes a while on a drive full of videos. Files with different sizes can’t be duplicates, so group by Length first and hash only the files that share a size:

$root = (Resolve-Path -Path .\Shared).Path + '\'

$candidates = Get-ChildItem -Path .\Shared -File -Recurse |
    Where-Object Length -GT 0 |
    Group-Object -Property Length |
    Where-Object Count -GT 1 |
    ForEach-Object Group

"Files hashed: $($candidates.Count) of $(@(Get-ChildItem -Path .\Shared -File -Recurse).Count)"
$candidates | Get-FileHash | Group-Object -Property Hash | Where-Object Count -GT 1 |
    Sort-Object -Property Count -Descending |
    ForEach-Object { ($_.Group.Path | Sort-Object | ForEach-Object { $_.Replace($root, '') }) -join '  =  ' }

Output:

Files hashed: 5 of 8
Docs\Old\office.jpg  =  Photos\austin-office.jpg  =  Photos\copy-of-austin-office.jpg
Docs\backup-notes.txt  =  Docs\notes.txt

Only 5 of the 8 files needed hashing here, and real folders save far more. Skipping empty files also avoids matching every zero-byte file with every other one.

Length is the size in bytes, the same value shown in getting a file’s size.

Same name, different content

Copies with the same name aren’t always duplicates. This check groups by name and counts how many different versions each name has:

$root = (Resolve-Path -Path .\Shared).Path + '\'

Get-ChildItem -Path .\Shared -File -Recurse |
    Group-Object -Property Name |
    Where-Object Count -GT 1 |
    ForEach-Object {
        $hashes = @($_.Group | Get-FileHash | Select-Object -ExpandProperty Hash -Unique)
        "$($_.Name): $($_.Count) copies, $($hashes.Count) different versions"
    }

Output:

budget-2026.xlsx: 2 copies, 2 different versions

Two different versions means someone edited one copy, so don’t delete either without looking. To track down every copy of one name, see finding a file by name.

Plan which copies to delete

A common rule is to keep the oldest copy and remove the rest. This lists the plan without deleting anything:

$root = (Resolve-Path -Path .\Shared).Path + '\'

Get-ChildItem -Path .\Shared -File -Recurse | Where-Object Length -GT 0 |
    Get-FileHash | Group-Object -Property Hash | Where-Object Count -GT 1 |
    Sort-Object -Property Count -Descending |
    ForEach-Object {
        $files = $_.Group | ForEach-Object { Get-Item -Path $_.Path } | Sort-Object -Property LastWriteTime
        "Keep:   $($files[0].FullName.Replace($root, ''))"
        $files | Select-Object -Skip 1 | ForEach-Object { "Delete: $($_.FullName.Replace($root, ''))" }
    }

Output:

Keep:   Photos\austin-office.jpg
Delete: Photos\copy-of-austin-office.jpg
Delete: Docs\Old\office.jpg
Keep:   Docs\notes.txt
Delete: Docs\backup-notes.txt
PowerShell keep the oldest duplicate file
The oldest copy is kept, newer copies are marked for deletion (Windows PowerShell 5.1)

When the plan looks right, pipe the files after the first to Remove-Item -WhatIf, check the output again, then remove -WhatIf. Deleted files skip the Recycle Bin, as explained in PowerShell Remove-Item.

Compare two folders

Before you delete a backup folder, check which of its files already exist elsewhere. -in compares the hashes:

$shared = Get-ChildItem -Path .\Shared -File -Recurse | Get-FileHash
$backup = Get-ChildItem -Path .\Backup -File | Get-FileHash

$backup | Where-Object { $_.Hash -in $shared.Hash } | ForEach-Object { "Already in Shared: $(Split-Path -Path $_.Path -Leaf)" }
$backup | Where-Object { $_.Hash -notin $shared.Hash } | ForEach-Object { "Only in Backup:    $(Split-Path -Path $_.Path -Leaf)" }

Output:

Already in Shared: austin-office.jpg
Already in Shared: notes.txt
Only in Backup:    vendors.csv
PowerShell compare two folders for duplicate files
Two backup files already exist in Shared, one doesn’t (PowerShell 7)

Only vendors.csv is unique to Backup, so that’s the one to keep. To find big files worth checking first, see finding the largest folders.

Frequently Asked Questions

How do I find duplicate files with PowerShell?

Run Get-ChildItem -Recurse -File | Get-FileHash | Group-Object Hash | Where-Object Count -GT 1. Grouping is covered in Microsoft’s Group-Object reference.

Does Get-FileHash find duplicates with different names?

Yes. The hash depends only on the content, so renamed copies still get the same hash.