The two commands in the title answer the same question from opposite ends of the machine. Get-FileHash is a PowerShell cmdlet: it reads a file, computes a digest and hands back an object with three properties. certutil -hashfile is a binary that ships with Windows and prints its answer as text, which is what you reach for on a server where PowerShell is not the shell in front of you.
Running either one is trivial. Comparing the answer is where the afternoon goes. Get-FileHash returns upper case hex, almost every vendor publishes its checksums in lower case, and a PowerShell script has at least ten ways of asking whether two strings are the same string. Of the ten measured below, seven report a match and three report a mismatch. Only one of those three was ever asked to be case sensitive.
What follows measures all ten of those comparisons, names the default that certutil documents nowhere on its reference page, counts the algorithm lists the two tools actually accept and shows why the same visible text, saved twice, produces two different hashes. Every output block below is the stdout of a script run on PowerShell 7.4.6, against either the 20 byte file the prerequisite section creates or the short string named beside it, so the numbers reproduce on any machine.
Applies to: Windows PowerShell 5.1, PowerShell 7.4 and PowerShell 7.5
Quick answer
SHA256 is the default for Get-FileHash, so -Algorithm is optional. It is not optional for certutil, for reasons the documentation section below measures. Compare the result with [string]::Equals and an ordinal comparison after forcing one case, never with a bare .Equals().
# Hash one file. SHA256 is the documented default.
Get-FileHash -Path C:\bat\test\sample.txt
# Compare against a published checksum without depending on how it was typed.
# .ToUpperInvariant() puts both sides in the case Get-FileHash returns;
# Ordinal then compares code point by code point, with no culture rules.
$expected = '222ef30656bbcfba8fa9be864202257a4c2f27547031a8e5caf3276d2bbd55ec'
$actual = (Get-FileHash -LiteralPath C:\bat\test\sample.txt).Hash
[string]::Equals($actual, $expected.ToUpperInvariant(), [System.StringComparison]::Ordinal)
From cmd.exe, where there is no pipeline and no object:
rem Typed at a command prompt, not saved in a batch file.
rem Always name the algorithm: the reference page never says what you get without it.
certutil -hashfile C:\bat\test\sample.txt SHA256
What the two commands do
A hash function reads bytes and produces a fixed length value. The same bytes always produce the same value, and changing one byte changes the whole value. Neither command looks at the file name, the timestamp or the attributes: only at the contents.
Get-FileHash has three parameter sets, and the third one is the reason the cmdlet can hash something that is not a file at all. certutil -hashfile has one form, with a trailing optional argument.
| Spelling | What it takes | What comes back |
|---|---|---|
Get-FileHash [-Path] <String[]> [[-Algorithm] <String>] | One or more paths. Wildcards are permitted and an array is accepted. | One object per file resolved |
Get-FileHash [-LiteralPath] <String[]> [[-Algorithm] <String>] | Paths used exactly as typed. No character is treated as a wildcard. | One object per file |
Get-FileHash [-InputStream] <Stream> [[-Algorithm] <String>] | An open stream, which can be a memory stream or a network stream. | One object, with no path |
certutil [options] -hashfile InFile [HashAlgorithm] | One file path. The algorithm name is optional and positional. | Text printed to the console |
The wildcard difference between -Path and -LiteralPath is the one worth knowing before anything else, because it is the one place in this article where a command produces no answer and no complaint. Measured on a folder holding a single file called report[2026].txt:
files on disk 1
-Path, objects returned 0
-Path, error records none
-Path, $? afterwards True
-LiteralPath, objects returned 1
-LiteralPath, hash 52CB0C6ABB02DCED4EB97286D6F91D4C4D09DFFCD60797F58A2915A53B981412
-Path treated the square brackets as a wildcard character class, matched nothing, returned nothing and wrote no error record, leaving $? as True. -LiteralPath on the same name returned the hash. Square brackets are legal in a Windows file name, so any script that takes a path from a variable should use -LiteralPath.
Before the first example
Three things have to be in place, and each one is a single command. Most of the hashes printed in this article come from the file the second step creates, so running these three means those output blocks can be reproduced rather than taken on trust. Where a block measures something else, the prose above it says what.
1. Confirm which shell you are in. Both shells carry Get-FileHash, and the algorithm lists they accept are not the same list.
# 5.1 means Windows PowerShell; 7.x means PowerShell 7.
# The edition matters for the algorithm list, not for SHA256.
$PSVersionTable.PSVersion.ToString()
$PSVersionTable.PSEdition
2. Create the folder and the sample file. The encoding is spelled out on purpose. Set-Content and Out-File write different bytes in the two shells, and the hash is taken over bytes.
New-Item -ItemType Directory -Path 'C:\bat\test' -Force | Out-Null
# WriteAllText with an explicit UTF8Encoding($false) means no byte order mark and
# no trailing newline, so the file is exactly the 20 characters in the string.
[System.IO.File]::WriteAllText('C:\bat\test\sample.txt', 'zaur.it hashing demo', [System.Text.UTF8Encoding]::new($false))
'{0} bytes' -f (Get-Item 'C:\bat\test\sample.txt').Length
20 bytes
3. Confirm certutil is on PATH. It ships with Windows, so this is a check rather than an install step. If where prints nothing, the later certutil examples will not run.
rem Typed at a command prompt, not saved in a batch file.
where certutil
20 bytes, the file on disk is the file this article measured, and every hash below will match on your machine.
Example 1: hash a file and read what comes back
The problem: A colleague sends a config file and asks whether it is the same one that is on the server. Before anything can be compared, there has to be a number to compare.
The solution: Get-FileHash with no -Algorithm uses SHA256, which the reference states in three separate places. Piping to Format-List puts all three properties of the returned object on their own lines.
Set-Location 'C:\bat\test'
Get-FileHash -Path .\sample.txt | Format-List | Out-String -Width 160
Algorithm : SHA256
Hash : 222EF30656BBCFBA8FA9BE864202257A4C2F27547031A8E5CAF3276D2BBD55EC
Path : C:\bat\test\sample.txt
Hash line above is the hash of the 20 bytes the prerequisite section wrote, so it reproduces exactly on any machine. The Path line is the only part of any output block in this article that differs by machine, and it is shown here as it appears when the file sits under C:\bat\test.
Three properties come back, and all three are strings. Algorithm names what was used, which is worth reading rather than assuming. Hash is 64 hex characters for SHA256 and is always upper case. Path is the resolved full path, not the relative one that was typed.
Example 2: check a download against a published hash
The problem: A vendor publishes a SHA256 checksum beside an installer. The download finished and the checksum has to be confirmed before the installer runs on a production host.
The solution: Compute the hash, then compare it with the published string. The comparison is the part that goes wrong, so this example runs three of them side by side on a file that is known to be correct.
Set-Location 'C:\bat\test'
# What the vendor printed beside the download, in the case vendors normally use.
$published = '222ef30656bbcfba8fa9be864202257a4c2f27547031a8e5caf3276d2bbd55ec'
$computed = (Get-FileHash -LiteralPath .\sample.txt -Algorithm SHA256).Hash
'{0,-38}{1}' -f 'the shell operator says', ($computed -eq $published)
'{0,-38}{1}' -f 'the .NET method says', ($computed.Equals($published))
'{0,-38}{1}' -f 'normalised, ordinal, says',
[string]::Equals($computed, $published.ToUpperInvariant(), [System.StringComparison]::Ordinal)
the shell operator says True
the .NET method says False
normalised, ordinal, says True
.Equals() is the method anyone arriving from C# or Python reaches for first, and on a file that is byte for byte correct it returns False. Nothing is wrong with the download. The two strings differ only in case, and the .NET reference for String.Equals states that the single argument instance method performs an ordinal, case sensitive comparison.
The next two sections take that apart properly. For now the working form is the third line: force one case, then compare ordinally.
Example 3: hash a folder and find what changed
The problem: A config directory on an application server is supposed to be unchanged since the last release. Timestamps are useless because a deployment tool touched every file, and the directory is too large to read.
The solution: Hash every file once, keep the result as a baseline, and compare the folder against that baseline later. Get-FileHash accepts pipeline input, so Get-ChildItem feeds it directly.
Set-Location 'C:\bat\test'
$enc = [System.Text.UTF8Encoding]::new($false)
# Three more files, so the folder looks like a small config directory.
[System.IO.File]::WriteAllText('C:\bat\test\app.conf', 'timeout=30', $enc)
[System.IO.File]::WriteAllText('C:\bat\test\logging.conf', 'level=info', $enc)
[System.IO.File]::WriteAllText('C:\bat\test\readme.txt', 'test folder', $enc)
# Take the baseline. It is written OUTSIDE the folder being hashed, on purpose:
# see the hidden gems section for what happens when it is written inside.
Get-ChildItem -File | Get-FileHash -Algorithm SHA256 |
Select-Object @{ n = 'FileName'; e = { Split-Path $_.Path -Leaf } }, Hash |
Export-Csv -Path 'C:\bat\baseline.csv' -NoTypeInformation
'{0,-28}{1}' -f 'files in the baseline', @(Import-Csv 'C:\bat\baseline.csv').Count
files in the baseline 4
Now something changes. One file is edited, one is added and one is deleted, which is the realistic shape of drift rather than a single edit.
Set-Location 'C:\bat\test'
$enc = [System.Text.UTF8Encoding]::new($false)
# Somebody edits one file, adds one and deletes one.
[System.IO.File]::WriteAllText('C:\bat\test\app.conf', 'timeout=90', $enc)
[System.IO.File]::WriteAllText('C:\bat\test\extra.conf', 'added later', $enc)
Remove-Item 'C:\bat\test\readme.txt'
# Load the baseline into a lookup, hash the folder again, then classify every name
# that appears on either side. -ceq is safe here because both sides of the
# comparison were produced by Get-FileHash, so both are already upper case.
$before = @{}
Import-Csv 'C:\bat\baseline.csv' | ForEach-Object { $before[$_.FileName] = $_.Hash }
$now = @{}
Get-ChildItem -File | Get-FileHash -Algorithm SHA256 |
ForEach-Object { $now[(Split-Path $_.Path -Leaf)] = $_.Hash }
'{0,-16}{1}' -f 'file', 'state'
'{0,-16}{1}' -f '--------------', '-----'
foreach ($f in (@($before.Keys) + @($now.Keys) | Sort-Object -Unique)) {
$state = if (-not $before.ContainsKey($f)) { 'new' }
elseif (-not $now.ContainsKey($f)) { 'missing' }
elseif ($before[$f] -ceq $now[$f]) { 'unchanged' }
else { 'CHANGED' }
'{0,-16}{1}' -f $f, $state
}
file state
-------------- -----
app.conf CHANGED
extra.conf new
logging.conf unchanged
readme.txt missing
sample.txt unchanged
Two details in that pair of blocks are deliberate. The baseline column is called FileName rather than Name, because Name is a member PowerShell supplies on many objects and a calculated property with that name is asking to be shadowed. The file name itself has to be derived with Split-Path, because the object Get-FileHash returns carries a Path and no Name at all.
The comparison uses -ceq rather than -eq, which is safe here for a specific reason: both sides of this comparison came out of Get-FileHash, so both are already upper case. The moment one side comes from somewhere else, the rule changes. A portable text manifest, the format that sha256sum -c reads, is a different subject and belongs with the Linux tools.
Example 4: find duplicate files by hash
The problem: A share has accumulated .bak and .old copies of config files over several years. Some are genuine history and some are byte identical to the live file, and the names do not say which is which.
The solution: Hash everything, group by the hash, and keep only the groups holding more than one file. Identical hashes mean identical contents, which is the one thing a file name cannot tell you.
$enc = [System.Text.UTF8Encoding]::new($false)
New-Item -ItemType Directory -Path 'C:\bat\dup' -Force | Out-Null
# Four names, and only two distinct file contents between them.
[System.IO.File]::WriteAllText('C:\bat\dup\app.conf', 'timeout=30', $enc)
[System.IO.File]::WriteAllText('C:\bat\dup\app.conf.bak', 'timeout=30', $enc)
[System.IO.File]::WriteAllText('C:\bat\dup\app.conf.old', 'timeout=45', $enc)
[System.IO.File]::WriteAllText('C:\bat\dup\web.config', 'timeout=30', $enc)
'{0,-28}{1}' -f 'files created', @(Get-ChildItem -File -Path 'C:\bat\dup').Count
files created 4
Set-Location 'C:\bat\dup'
# Group by hash, then keep only the groups holding more than one file.
# Both sides of every comparison came out of Get-FileHash, so the upper case
# is consistent and Group-Object needs no normalising here.
$groups = Get-ChildItem -File | Get-FileHash -Algorithm SHA256 |
Group-Object -Property Hash | Where-Object Count -gt 1
'{0,-10}{1,-8}{2}' -f 'hash', 'copies', 'names'
'{0,-10}{1,-8}{2}' -f '--------', '------', '----------------------------------------'
foreach ($g in $groups) {
$names = ($g.Group | ForEach-Object { Split-Path $_.Path -Leaf } | Sort-Object) -join ', '
'{0,-10}{1,-8}{2}' -f $g.Name.Substring(0, 8), $g.Count, $names
}
''
# Measure-Object -Sum over the group sizes, minus one keeper per group.
$redundant = (@($groups) | Measure-Object Count -Sum).Sum - @($groups).Count
'{0,-34}{1}' -f 'files examined', @(Get-ChildItem -File).Count
'{0,-34}{1}' -f 'distinct contents', @(Get-ChildItem -File | Get-FileHash | Group-Object Hash).Count
'{0,-34}{1}' -f 'groups with more than one', @($groups).Count
'{0,-34}{1}' -f 'copies that are redundant', $redundant
hash copies names
-------- ------ ----------------------------------------
80D7C837 3 app.conf, app.conf.bak, web.config
files examined 4
distinct contents 2
groups with more than one 1
copies that are redundant 2
Four files, two distinct contents. The interesting row is the one that groups web.config with the two app.conf copies: they share contents despite sharing no part of their names, which is exactly the case a name based sweep misses. Note also that the duplicate group holds three files but only two of them are redundant, because one copy has to survive.
Get-ChildItem -Filter or a size filter first, since two files of different sizes cannot be byte identical.
Example 5: hash a string rather than a file
The problem: An API expects the SHA256 of a token, or a script has to compare a value against a stored digest. There is no file anywhere in the task.
The solution: There is no string parameter, and the reference says so in plain words: PowerShell does not provide a cmdlet to compute the hash of a string. The documented route is to write the string into a stream and pass the stream. Three spellings look like they ought to work; here is what each one does instead.
$s = 'Hello world'
# Three spellings that look like they should hash the string. None of them does.
try { '{0,-34}{1}' -f '-Path with the string', (Get-FileHash -Path $s -ErrorAction Stop).Hash }
catch { '{0,-34}{1}' -f '-Path with the string', $_.FullyQualifiedErrorId }
try { '{0,-34}{1}' -f 'the string down the pipeline', ($s | Get-FileHash -ErrorAction Stop).Hash }
catch { '{0,-34}{1}' -f 'the string down the pipeline', $_.FullyQualifiedErrorId }
try { '{0,-34}{1}' -f '-InputObject', (Get-FileHash -InputObject $s -ErrorAction Stop).Hash }
catch { '{0,-34}{1}' -f '-InputObject', $_.FullyQualifiedErrorId }
# The documented route: write the string into a stream and hash the stream.
$stream = [System.IO.MemoryStream]::new()
$writer = [System.IO.StreamWriter]::new($stream)
$writer.Write($s)
$writer.Flush()
$stream.Position = 0
$viaStream = (Get-FileHash -InputStream $stream).Hash
$fromDocs = '64EC88CA00B268E5BA1A35678A1B5316D212F4F366B2477232534A8AECA37F3C'
'{0,-34}{1}' -f '-InputStream over the string', $viaStream
''
'{0,-34}{1}' -f 'value published in the docs', $fromDocs
'{0,-34}{1}' -f 'matches the published value', ($viaStream -ceq $fromDocs)
-Path with the string FileNotFound,Microsoft.PowerShell.Commands.GetFileHashCommand
the string down the pipeline FileNotFound,Microsoft.PowerShell.Commands.GetFileHashCommand
-InputObject NamedParameterNotFound,Microsoft.PowerShell.Commands.GetFileHashCommand
-InputStream over the string 64EC88CA00B268E5BA1A35678A1B5316D212F4F366B2477232534A8AECA37F3C
value published in the docs 64EC88CA00B268E5BA1A35678A1B5316D212F4F366B2477232534A8AECA37F3C
matches the published value True
The last two lines are the point. The value the stream produces is the same value Microsoft publishes in Example 4 of the Get-FileHash reference for the same string, so the technique is confirmed rather than merely plausible.
The quiet danger in the first line is worth its own run. If the string you meant to hash also happens to name a file in the current directory, -Path does not fail. It finds the file and hashes the file, and the answer looks entirely reasonable.
Set-Location 'C:\bat\test'
$s = 'sample.txt' # meant as a string, and it also names a file here
# What -Path does with it: it finds the file and hashes the file.
$asPath = (Get-FileHash -Path $s -Algorithm SHA256).Hash
# What hashing the ten characters of the string itself gives instead.
$ms = [System.IO.MemoryStream]::new()
$w = [System.IO.StreamWriter]::new($ms)
$w.Write($s); $w.Flush(); $ms.Position = 0
$asString = (Get-FileHash -InputStream $ms -Algorithm SHA256).Hash
'{0,-30}{1}' -f 'Get-FileHash -Path $s', $asPath
'{0,-30}{1}' -f 'the string itself hashed', $asString
'{0,-30}{1}' -f 'same answer', ($asPath -ceq $asString)
Get-FileHash -Path $s 222EF30656BBCFBA8FA9BE864202257A4C2F27547031A8E5CAF3276D2BBD55EC
the string itself hashed 9496EEC54D06963F3666D7719CD27073898A3EE588453B934627BB504CF19FBD
same answer False
-Path: decide at the top of the script whether the value is a path or a payload, and use -LiteralPath or -InputStream accordingly.
The comparison that reports a good file as bad
This is the finding worth carrying away from the page, and it is the opposite of what most people expect.
Get-FileHash returns upper case hex. Vendors, Linux tools and release notes almost always publish lower case. So whenever a published checksum meets a computed one, the two strings are liable to differ in case, and the answer then depends on which comparison you happened to write.
PowerShell answers that question generously. about_Comparison_Operators states it plainly: string comparisons are case insensitive unless you use the explicit case sensitive operator, and they use the invariant culture. So -eq does the right thing on two hashes that differ only in case, and so does every shell operator in the measurement below that was not asked for case sensitivity.
.NET does not work that way, and .NET is what you get the moment a method is called on the string instead of an operator applied to it. The reference for String.Equals describes the single argument instance method as performing an ordinal comparison, case sensitive and culture insensitive.
.Equals() reads like the careful choice, and on a file that is byte for byte correct it reports a mismatch. A script written that way does not report corruption that exists. It reports corruption that does not.
Ten ways to compare two hashes, measured
The two strings below are the same SHA256 value in two cases. Both were produced from the same 20 byte file, one by Get-FileHash and one by sha256sum. Ten comparisons a script might reasonably reach for were then run over the pair.
computed by Get-FileHash 222EF30656BBCFBA8FA9BE864202257A4C2F27547031A8E5CAF3276D2BBD55EC
published beside the file 222ef30656bbcfba8fa9be864202257a4c2f27547031a8e5caf3276d2bbd55ec
-eq True
-ceq False
-like True
-match True
-contains (array) True
-in (array) True
hashtable key lookup True
.Equals() False
[string]::Equals() 2-arg False
Compare-Object (no diff) True
comparisons tested 10
reporting a match 7
reporting a mismatch 3
Ten comparisons, seven reporting a match and three reporting a mismatch. Only one of those three, -ceq, was asked for case sensitivity. The other two are .NET methods that were never governed by the shell rule in the first place.
| The comparison | What it reported | Why |
|---|---|---|
-eq, -like, -match, -contains, -in, hashtable key | match | Shell operators. Documented as case insensitive by default. |
Compare-Object | match | Compares with the same case insensitive rules, so it finds no difference object. |
-ceq | mismatch | The explicitly case sensitive form. Reporting a mismatch is exactly what was asked for. |
.Equals(), [string]::Equals() with two arguments | mismatch | A .NET method, documented as ordinal and case sensitive. The shell rule does not apply. |
-ceq and .Equals() return the same answer for opposite reasons: one is doing what the author asked, and the other is doing something the author did not know was being asked.
The fix is not to pick a luckier operator. It is to normalise both sides before comparing, then compare in a way that does not depend on anybody remembering the case rules. Stripping everything that is not a hex digit also survives the two other ways a pasted checksum arrives wrong: a trailing space, and vendors who print the value in groups of eight.
function Test-FileHashMatch {
param(
[Parameter(Mandatory)][string] $FilePath,
[Parameter(Mandatory)][string] $Expected,
[string] $Algorithm = 'SHA256'
)
# Normalise before comparing: drop everything that is not a hex digit, which
# removes pasted spaces and line breaks, then force the case Get-FileHash uses.
# Compare ordinally, so the answer does not depend on the shell's case rules.
$want = (($Expected -replace '[^0-9A-Fa-f]', '')).ToUpperInvariant()
$got = (Get-FileHash -LiteralPath $FilePath -Algorithm $Algorithm).Hash
[pscustomobject]@{
FilePath = $FilePath
Algorithm = $Algorithm
Expected = $want
Computed = $got
Matched = [string]::Equals($got, $want, [System.StringComparison]::Ordinal)
}
}
$f = 'C:\bat\test\sample.txt'
$good = '222ef30656bbcfba8fa9be864202257a4c2f27547031a8e5caf3276d2bbd55ec'
$cases = [ordered]@{
'as published, lower case' = $good
'pasted in upper case' = $good.ToUpperInvariant()
'pasted with a trailing space' = $good + ' '
'pasted with spaces every 8' = ($good -replace '(.{8})', '$1 ').Trim()
'one character altered' = $good.Substring(0, 63) + '0'
}
'{0,-32}{1}' -f 'what was pasted in', 'Matched'
'{0,-32}{1}' -f '------------------------------', '-------'
foreach ($k in $cases.Keys) {
'{0,-32}{1}' -f $k, (Test-FileHashMatch -FilePath $f -Expected $cases[$k]).Matched
}
what was pasted in Matched
------------------------------ -------
as published, lower case True
pasted in upper case True
pasted with a trailing space True
pasted with spaces every 8 True
one character altered False
Matched rather than something shorter on purpose. Count, Length and Name are poor names for a property of your own, because PowerShell may resolve any of the three to a member it supplies rather than to yours.
The default one reference states and the other does not
Both commands take the algorithm as an optional trailing argument. Only one of the two reference pages says what happens when you leave it out.
certutil reference, total lines 2816
the -hashfile section, non-blank lines 5
of those, the heading 1
of those, prose 1
of those, the fenced syntax block 3
the word "default" in that section 0
a list explaining its two arguments no
an example command no
an output capture no
lines down to the algorithm list 1978
Get-FileHash reference, total lines 240
worked examples on that page 4
output captures on that page 4
places it states the default is SHA256 3
By default, the `Get-FileHash` cmdlet uses the SHA256 algorithm, although any hash algorithm that
If no value is specified, or if the parameter is omitted, the default value is SHA256.
Default value: SHA256
The Get-FileHash page states its default three times: once in prose, once in the parameter description and once in the parameter metadata. The certutil page devotes five non-blank lines to -hashfile in total, contains no example, no output and no explanation of either argument, and lists the algorithm names 1978 lines further down, in a trailing sentence under the general options table.
| What a reader needs | Get-FileHash | certutil |
|---|---|---|
| The algorithm used when the argument is omitted | Stated in three places | Not stated |
| The list of algorithm names accepted | In the parameter description | 1978 lines below the subcommand |
| A worked example | Four | None |
| A sample of the output | Four captures | None |
certutil -hashfile defaults to SHA1. Nothing on Microsoft’s page says so, which is why this article does not say so either. It is a two command experiment on your own build, and the answer is worth more than the folklore.
Run the command once with no algorithm and once naming a candidate. If the two hex strings are identical, that candidate is the default on this build of Windows.
rem Typed at a command prompt, not saved in a batch file.
rem Three runs: the default, then two named algorithms to compare it against.
certutil -hashfile C:\bat\test\sample.txt
certutil -hashfile C:\bat\test\sample.txt SHA1
certutil -hashfile C:\bat\test\sample.txt SHA256
Counting the characters narrows it before any comparison. Each SHA variant produces a different number of hex characters, so the length alone names the family.
algorithm hex chars first 16
--------- --------- ----------------
MD5 32 E6A4482E84D068E1
SHA1 40 E3B71116046A2CC6
SHA256 64 222EF30656BBCFBA
SHA384 96 F27EA7A25405DF0E
SHA512 128 1B8E22C290355229
algorithms measured 5
distinct hex lengths 5
certutil list carries two further MD names that this article could not measure. Name the algorithm in every script and the question never arises.
Three algorithm lists that are not the same list
It is tempting to treat the two commands as interchangeable. Their algorithm lists say otherwise, and so does the gap between the two PowerShell editions. The table below was built by reading the accepted values straight out of four reference pages.
algorithm Get-FileHash 5.1 Get-FileHash 7.4 Get-FileHash 7.5 certutil
--------- ---------------- ---------------- ---------------- ----------------
MACTripleDES listed - - -
MD2 - - - listed
MD4 - - - listed
MD5 listed listed listed listed
RIPEMD160 listed - - -
SHA1 listed listed listed listed
SHA256 listed listed listed listed
SHA384 listed listed listed listed
SHA512 listed listed listed listed
names listed, Get-FileHash 5.1 7
names listed, Get-FileHash 7.4 5
names listed, Get-FileHash 7.5 5
names listed, certutil 7
distinct names, all four 9
listed by all four 5
listed by all four MD5 SHA1 SHA256 SHA384 SHA512
Nine distinct names across the four pages, and five that appear on all of them. MACTripleDES and RIPEMD160 are Windows PowerShell 5.1 only. MD2 and MD4 are on the certutil list and on neither Get-FileHash page. A script that hashes with RIPEMD160 under Windows PowerShell stops working the day the host moves to PowerShell 7.
The documentation and the runtime agree here, which is worth confirming rather than assuming. Passing all nine names to PowerShell 7.4.6 gives exactly the five the reference lists:
MD5 accepted E6A4482E84D068E1
SHA1 accepted E3B71116046A2CC6
SHA256 accepted 222EF30656BBCFBA
SHA384 accepted F27EA7A25405DF0E
SHA512 accepted 1B8E22C290355229
MACTripleDES rejected InvalidData
RIPEMD160 rejected InvalidData
MD2 rejected InvalidData
MD4 rejected InvalidData
InvalidData, and no silent fallback to a different algorithm. That is the good outcome: an unavailable algorithm fails loudly rather than quietly hashing with something else.
The documented output type is not the type you get
The Outputs section of the Get-FileHash reference names one type. The runtime returns another. All three current reference pages, for 5.1, 7.4 and 7.5, carry the same name.
$o = Get-FileHash -Path 'C:\bat\test\sample.txt' -Algorithm SHA256
'{0,-26}{1}' -f 'type name', $o.GetType().FullName
foreach ($p in $o.PSObject.Properties) { '{0,-26}{1}' -f ('property: ' + $p.Name), $p.TypeNameOfValue }
'{0,-26}{1}' -f 'Hash characters', $o.Hash.Length
'{0,-26}{1}' -f 'all characters upper case', ($o.Hash -ceq $o.Hash.ToUpperInvariant())
type name Microsoft.PowerShell.Commands.FileHashInfo
property: Algorithm System.String
property: Hash System.String
property: Path System.String
Hash characters 64
all characters upper case True
| Where the name appears | The name |
|---|---|
| Documented under Outputs on 5.1, 7.4 and 7.5 | Microsoft.PowerShell.Utility.FileHash |
| Returned by PowerShell 7.4.6, measured | Microsoft.PowerShell.Commands.FileHashInfo |
The three properties themselves are exactly as described: Algorithm, Hash and Path, all strings. Only the type name differs, and it matters in one place: any code that tests the object with -is against the documented name, or that declares the documented name in an [OutputType()] attribute, is naming a type that does not exist. Asking the session to resolve both names settles it:
name source resolves
---- ------ --------
Utility.FileHash documented False
Commands.FileHashInfo measured True
.GetType().FullName is the shortest possible way to settle what a cmdlet actually hands back, and it is worth the habit on any cmdlet whose output you are about to filter by type.
The hash is of the bytes, so the encoding decides it
A hash is taken over the contents of the file and nothing else. That is usually stated as reassurance, because renaming a file does not change its hash. The same sentence read the other way is the warning: two files that look identical on screen have different hashes whenever their bytes differ, and text files differ in bytes constantly.
The same twenty characters, written five ways:
encoding chars bytes SHA256 (first 32)
-------------------- ----- ----- --------------------------------
UTF-8, no BOM 20 20 222EF30656BBCFBA8FA9BE864202257A
UTF-8, with BOM 20 23 6F77942A96B7E7D766B169BBEC332FCD
UTF-16LE, with BOM 20 42 041ECCC35CBCE7ECB3E481D3DEECD335
UTF-16BE, with BOM 20 42 644176D812D9F3A2C06052777CBF3751
UTF-32LE, with BOM 20 84 A5A14408C22E4B9C517A0BE08AA5CBCA
files written 5
read back as the same 20 characters 5
distinct SHA256 values 5
Five files, every one of them reading back as the same twenty characters, and five different hashes. Note that the two UTF-16 rows are the same size, 42 bytes, and still hash differently: a matching byte count is not evidence of matching bytes.
This is not a laboratory curiosity, because the shells disagree about which of those five they produce. The default encoding of Out-File, and therefore of the > redirection operator behind it, changed between the editions.
| Shell | Out-File default | What lands on disk |
|---|---|---|
| Windows PowerShell 5.1 | unicode | UTF-16 with little-endian byte order, with a byte order mark |
| PowerShell 7.4 | utf8NoBOM | UTF-8 with no byte order mark |
The same rule governs a string that never reaches disk. The StreamWriter in Example 5 has an encoding of its own, and changing it changes the hash of the same eleven characters:
StreamWriter encoding bytes SHA256 (first 32)
---------------------- ----- --------------------------------
default 11 64EC88CA00B268E5BA1A35678A1B5316
UTF-8, no BOM 11 64EC88CA00B268E5BA1A35678A1B5316
UTF-8, with BOM 14 02C0B096AB1E49BA2E80F57C14BCA173
UTF-16LE, with BOM 24 FA0897F8396C6157873C79E328EB5F34
Out-File line, produces a different hash under the two shells. A checksum recorded on a 5.1 host and verified on a PowerShell 7 host will not match, and nothing about the file was tampered with. Write the bytes explicitly, as the prerequisite block in this article does, whenever the hash of the result matters.
Hidden gems
A baseline written into its own folder hashes itself. Get-ChildItem enumerates lazily and Export-Csv creates its file immediately, so the file appears in the directory while the directory is still being walked. The row count then grows on every run, which looks like files appearing out of nowhere.
files in the folder 4
rows in the baseline it just wrote 5
baseline.csv listed in its own rows True
rows on the second run 6
Four files in the folder, five rows in the baseline it just wrote, and six rows the second time the same command runs. Nothing is wrong with Get-FileHash. The output file is inside its own input. Write the baseline one level up, or exclude it by name.
-Path takes wildcards and an array, so one call can cover a set. That is the documented difference from -LiteralPath, and it means Get-FileHash does not need a loop for the common case.
# One call, several patterns. Each resolved file produces its own object.
Get-FileHash -Path C:\bat\test\*.conf, C:\bat\test\*.txt |
Sort-Object Hash |
Format-Table Hash, Path -AutoSize
-InputStream can read a network stream, so a download never has to land on disk. Example 3 of the reference opens a package URL with a web client and hashes the stream directly. That is the honest way to verify a large ISO on a host with no room for two copies of it.
MD5 is scoped rather than dead. The reference is blunt about it: MD5 and SHA1 should be used for simple change validation only, and not where protection from tampering is needed. Detecting that a file arrived corrupted across a flaky link is exactly simple change validation, and that is where MD5 still belongs.
Two hash values are worth recognising on sight. A zero byte file has a hash like any other, and for a given algorithm it is always the same value. Seeing either of these two in a report means the file is empty, which is the usual result of a download that failed without saying so.
bytes on disk 0
SHA256 E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
MD5 D41D8CD98F00B204E9800998ECF8427E
The same file in PowerShell, cmd and bash
Three shells, one file, one algorithm. PowerShell hands back an object whose Hash property is upper case hex. The GNU tools print lower case hex, two spaces and the file name. certutil prints text, and no capture of it appears anywhere on this page, for the reason given in the documentation section above.
# PowerShell, either edition. Upper case hex, one object per file.
(Get-FileHash -LiteralPath C:\bat\test\sample.txt -Algorithm SHA256).Hash
rem cmd.exe, typed at the prompt. Prints text, not an object.
certutil -hashfile C:\bat\test\sample.txt SHA256
# Run this from the folder holding sample.txt.
# sha256sum prints lower case hex, two spaces, then the file name.
printf '%-24s%s\n' "sha256sum" "$(sha256sum sample.txt)"
# shasum is a separate program that takes the algorithm as an argument, for
# systems where sha256sum is not installed. Its output format is the same.
printf '%-24s%s\n' "shasum -a 256" "$(shasum -a 256 sample.txt)"
sha256sum 222ef30656bbcfba8fa9be864202257a4c2f27547031a8e5caf3276d2bbd55ec sample.txt
shasum -a 256 222ef30656bbcfba8fa9be864202257a4c2f27547031a8e5caf3276d2bbd55ec sample.txt
same first field yes
same program no
The two agree on the hex and they are not the same binary, which is worth knowing before a script assumes one of the two names is present. What neither of them agrees with is the case PowerShell returns, and that is the whole reason the comparison sections above exist. If a value crosses between these shells in either direction, normalise it before comparing rather than trusting whichever operator came to hand.
The Linux commands cheat sheet carries the Linux side as a single row, and the OpenSSL cheat sheet has the openssl dgst form for hosts where neither of the above is installed.
Where this matters
- Verifying an installer before it runs on production. The checksum beside the download is the cheapest evidence there is that the bytes survived the transfer intact.
- Proving a restore is the file that was backed up. A hash taken before the backup and again after the restore answers the question that file sizes and timestamps only hint at, which is the same check the ROBOCOPY backup walk-through recommends after a large copy.
- Finding out whether two servers really run the same binary. Two hosts reporting the same version can still carry different builds, and the hash settles it in one line per host.
- Change detection on a config directory nobody is supposed to touch. A stored baseline turns a vague suspicion into a named file, as in the third example above.
- Clearing out years of accumulated backup copies. Grouping by hash shows which copies are byte identical to the live file and therefore safe to delete, and which are genuine history.
- Explaining why a colleague’s checksum does not match yours. Often the answer is not the file at all but the case it was printed in or the encoding it was saved with.
Tips and limitations
- No elevation is needed for either command. Read access to the file is the only requirement, so neither of them needs an administrative prompt.
- Name the algorithm in every script, even though SHA256 is the
Get-FileHashdefault. It costs one word and it removes the only question thecertutilreference cannot answer. - Use
-LiteralPathwhen the value comes from a variable.-Pathtreats square brackets as a wildcard character class, and on a name containing them it returns nothing and reports no error. - Never pass arbitrary text to
-Path. If the text happens to name a file, the command hashes the file and reports no error. - Compare with
[string]::EqualsandStringComparison.Ordinalafter forcing one case. Avoid a bare.Equals(), which is ordinal and case sensitive and will report a correct file as wrong. - Hashing reads the whole file, so a sweep over a large share is limited by disk throughput. Filter by extension or by size first, since two files of different sizes cannot be byte identical.
- The algorithm list is not the same in the two PowerShell editions.
RIPEMD160andMACTripleDESare Windows PowerShell 5.1 only, and PowerShell 7 rejects them withInvalidDatarather than falling back. - A hash is an integrity check, not a signature. If the checksum arrived through the same channel as the file, it proves the download did not corrupt and nothing more.
- The
certutilreference page opens with a caution that the tool is not recommended in production code. For reading a hash on a host with no PowerShell session in front of you it remains the pragmatic answer, and that is the scope used here.
Official documentation
- Get-FileHash: Microsoft.PowerShell.Utility, PowerShell 7.4 | Microsoft Learn
- Get-FileHash: Microsoft.PowerShell.Utility, Windows PowerShell 5.1 | Microsoft Learn
- certutil: Windows Commands | Microsoft Learn
- about_Comparison_Operators: PowerShell | Microsoft Learn
- String.Equals Method: System | Microsoft Learn
- Out-File: encoding defaults in Windows PowerShell 5.1 | Microsoft Learn
- Out-File: encoding defaults in PowerShell 7.4 | Microsoft Learn
- Group-Object: grouping objects by a property value | Microsoft Learn
Related tools
- Hash generator: computes MD5, SHA1, SHA256 and SHA512 in the browser and compares the result against a checksum you paste, which is the fastest way to settle a case mismatch without opening a shell.
- Base64 Encoder Pro: the other half of what
certutilis used for, since the same binary encodes and decodes Base64 as well as hashing files.
Related guides
- Base64 encoding and decoding files in PowerShell: its round trip check hashes the original and the restored file and compares them, which is the pattern this page measures in detail.
- Encoding and decoding files with certutil: the same binary as
-hashfile, doing the job it is better documented for. - Finding which binary actually runs with where and Get-Command: hashing two paths is how it tells one build in two places from two different builds.
- OpenSSL cheat sheet: carries the
openssl dgstform for hosts where neither PowerShell norcertutilis the tool at hand. - Linux commands cheat sheet: the
sha256sumside of the same task, in one row. - Backing up files with ROBOCOPY: the natural place to take a baseline before a large copy and check it afterwards.
Five cheat sheets, one PDF
Subnet masks, PowerShell, Linux commands, HTTP status codes and the ESXi command line - one page each, free to keep. Leave an address and it arrives in a minute.