SCRIPT LIBRARY · POWERSHELL
Check Whether Two Files (or Two Folders) Are Really Identical
Compare files, whole folder trees, or a download against its published checksum with Get-FileHash, and get back a clear Match or Different for every file.
- What it does
- Hashes files with Get-FileHash and compares them file-to-file, folder-to-folder (matched by relative path), or against a hash you were given. Files with different sizes are flagged without being hashed.
- Requires
- PowerShell 7+ or Windows PowerShell 5.1
- No modules
- Permissions
- Read access to the files. Nothing is changed.
- Runs on
- Windows, macOS, Linux (PowerShell 7)
- Tested
- Parse-checked and run against test files and folder trees in PowerShell 7.4, covering matches, content changes, size changes, one-sided files and expected-hash checks
Part 4 of the thread File wrangling
"Are these the same file?" comes up more than you'd think. Did the backup actually copy everything? Is the ISO on the share the one you downloaded, or the one from last year with the same name? Did that config file on server two drift from server one?
Names, sizes and dates can all lie. A hash doesn't. Get-FileHash reads every byte and boils it down to a fingerprint, and if two fingerprints match, the files are the same.
The first version of this post compared two hard-coded paths with MD5 and printed a green or red line. This one takes parameters, defaults to SHA-256, compares whole folders as well as single files, checks downloads against a published hash, and returns objects you can filter and export. (Also: the old post was tagged Microsoft Graph for some reason. It has nothing to do with Graph.)
<#
.SYNOPSIS
Tells you whether two files, or two whole folders, really contain the same bytes.
.DESCRIPTION
Uses Get-FileHash to compare a file against another file, a folder against another folder
(matched by relative path), or a file against a hash you were given, like the SHA-256 on a
download page. Files with different sizes are reported as different without hashing them.
Read-only: nothing is changed.
.PARAMETER ReferencePath
The file or folder you trust.
.PARAMETER DifferencePath
The file or folder you're checking against it.
.PARAMETER ExpectedHash
A published hash to check ReferencePath against, instead of a second file.
.PARAMETER Algorithm
SHA256 (default), SHA384, SHA512, SHA1 or MD5.
.PARAMETER Recurse
When comparing folders, include subfolders.
.PARAMETER DifferencesOnly
When comparing folders, leave matching files out of the results.
.EXAMPLE
.\Compare-FileHash.ps1 -ReferencePath .\setup.iso -ExpectedHash 3F2A...9C1D
.EXAMPLE
.\Compare-FileHash.ps1 -ReferencePath D:\Photos -DifferencePath \\nas01\Backup\Photos -Recurse -DifferencesOnly
#>
[CmdletBinding(DefaultParameterSetName = 'Compare')]
param(
[Parameter(Mandatory, Position = 0)][ValidateScript({ Test-Path -LiteralPath $_ })][string]$ReferencePath,
[Parameter(Mandatory, Position = 1, ParameterSetName = 'Compare')][ValidateScript({ Test-Path -LiteralPath $_ })][string]$DifferencePath,
[Parameter(Mandatory, ParameterSetName = 'Expected')][ValidatePattern('^[0-9A-Fa-f]{32,128}$')][string]$ExpectedHash,
[ValidateSet('SHA256', 'SHA384', 'SHA512', 'SHA1', 'MD5')][string]$Algorithm = 'SHA256',
[switch]$Recurse,
[switch]$DifferencesOnly
)
function Get-Hash([string]$File) { (Get-FileHash -LiteralPath $File -Algorithm $Algorithm).Hash }
function Compare-OneFile([string]$Name, [IO.FileInfo]$Ref, [IO.FileInfo]$Dif) {
$out = [ordered]@{ Name = $Name; Status = $null; ReferenceHash = $null; DifferenceHash = $null; Algorithm = $Algorithm }
if (-not $Dif) { $out.Status = 'OnlyInReference' }
elseif (-not $Ref) { $out.Status = 'OnlyInDifference' }
elseif ($Ref.Length -ne $Dif.Length) { $out.Status = 'Different'; $out.ReferenceHash = '(size differs)' }
else {
try {
$out.ReferenceHash = Get-Hash $Ref.FullName
$out.DifferenceHash = Get-Hash $Dif.FullName
$out.Status = if ($out.ReferenceHash -eq $out.DifferenceHash) { 'Match' } else { 'Different' }
}
catch {
$out.Status = "Error: $($_.Exception.Message)"
}
}
[pscustomobject]$out
}
$refItem = Get-Item -LiteralPath $ReferencePath
if ($PSCmdlet.ParameterSetName -eq 'Expected') {
if ($refItem.PSIsContainer) { throw '-ExpectedHash works on a single file, not a folder.' }
$actual = Get-Hash $refItem.FullName
return [pscustomobject]@{
Name = $refItem.Name
Status = if ($actual -eq $ExpectedHash.Trim()) { 'Match' } else { 'Different' }
ReferenceHash = $actual
Expected = $ExpectedHash.ToUpper()
Algorithm = $Algorithm
}
}
$difItem = Get-Item -LiteralPath $DifferencePath
if ($refItem.PSIsContainer -ne $difItem.PSIsContainer) { throw 'Compare a file with a file, or a folder with a folder.' }
if (-not $refItem.PSIsContainer) {
return Compare-OneFile $refItem.Name $refItem $difItem
}
# Folders: index both sides by path relative to their root, then walk the union of names.
$index = foreach ($root in $refItem, $difItem) {
$table = @{}
foreach ($f in Get-ChildItem -LiteralPath $root.FullName -File -Recurse:$Recurse -Force) {
$table[$f.FullName.Substring($root.FullName.TrimEnd('\', '/').Length + 1)] = $f
}
, $table
}
$refFiles, $difFiles = $index
Write-Verbose "Reference: $($refFiles.Count) files. Difference: $($difFiles.Count) files."
$names = @($refFiles.Keys) + @($difFiles.Keys) | Sort-Object -Unique
foreach ($name in $names) {
$row = Compare-OneFile $name $refFiles[$name] $difFiles[$name]
if (-not ($DifferencesOnly -and $row.Status -eq 'Match')) { $row }
}
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-ReferencePath | string | — | The file or folder you trust. First positional parameter. |
-DifferencePath | string | — | The file or folder you're checking. Second positional parameter. Must be the same kind of thing as ReferencePath. |
-ExpectedHash | string | — | A hash to check ReferencePath against instead of another file, like the SHA-256 on a vendor's download page. Case doesn't matter. |
-Algorithm | string | SHA256 | SHA256, SHA384, SHA512, SHA1 or MD5. Match whatever the publisher used when you're checking a download. |
-Recurse | switch | — | Include subfolders when comparing folders. |
-DifferencesOnly | switch | — | Hide files that match, so you only see what's wrong. |
Run it
Two files, same name, different servers.
.\Compare-FileHash.ps1 \\app01\c$\App\appsettings.json \\app02\c$\App\appsettings.jsonCheck a download against the hash on the vendor's site.
.\Compare-FileHash.ps1 .\installer.msi -ExpectedHash 9f2c4e8a1b7d3c6e5f4a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1eDid the backup really get everything?
.\Compare-FileHash.ps1 D:\Photos \\nas01\Backup\Photos -Recurse -DifferencesOnlySave a report of every difference.
.\Compare-FileHash.ps1 D:\Site \\web02\d$\Site -Recurse -DifferencesOnly | Export-Csv .\site-drift.csv -NoTypeInformationWhat you'll see
Name Status ReferenceHash DifferenceHash Algorithm
---- ------ ------------- -------------- ---------
2025\IMG_0142.jpg Different (size differs) SHA256
2025\IMG_0188.jpg Different 3E1B0C...A94F 7D22F9...0B11 SHA256
2026\IMG_0007.jpg OnlyInReference SHA256
Thumbs.db OnlyInDifference SHA256
How it works
- Pick a mode from the parameters. With
-ExpectedHash, it hashes one file and compares. With two paths, it checks that both are files or both are folders. - File against file. Sizes first. If they differ, the files are different and there's no need to read them. If they're the same size, both get hashed and compared.
- Folder against folder. Both trees are indexed by path relative to their root, so
D:\Photos\2025\a.jpglines up with\\nas01\Backup\Photos\2025\a.jpg. Then it walks every name that appears on either side. Files on only one side are reported asOnlyInReferenceorOnlyInDifference. - Handle errors per file. A file that can't be read (locked, access denied) gets an
Error:status and the comparison carries on. - Return objects. Every row has the name, status, both hashes and the algorithm, so you can sort, filter, or export them.
Take it further
- Save a baseline. Run
Get-ChildItem -Recurse -File | Get-FileHashon a known-good folder once and export it to CSV. Later, compare against the CSV to spot anything that changed, which is a poor man's file integrity monitor. - Check after a migration. If you've just moved data with robocopy in copy mode, run this between the two sides before you delete the originals.
- Find duplicates. Group
Get-FileHashoutput byHashand any group with more than one file is a set of duplicates, whatever they're named.
Things that'll trip you up
- Big folders take a while. Every same-sized pair gets read in full, on both sides. Comparing a few hundred GB across the network is a lunch-break job, not a coffee-break one. The size check up front helps a lot when files really have changed.
- MD5 and SHA1 are fine for "did it copy right?", not for security. They're broken against someone deliberately forging a match. For checking downloads or anything that matters, stick with the SHA-256 default.
- Same content, different hash? Check line endings. A text file that went through Git or an FTP transfer in ASCII mode can come back with CRLF swapped for LF. It looks identical in an editor but it's a different file as far as a hash is concerned.
- Names are matched case-insensitively. That's right for Windows. If you're comparing Linux folders where Report.txt and report.txt are two different files, they'll get mixed up.
- Only file contents are compared. Timestamps, permissions and alternate data streams aren't part of the hash. Two files can match here and still have different ACLs.