DevKitLab Logo DevKitLab
Hashing / Checksums / Integrity / Debugging

Why Doesn't My Checksum Match? A Field Guide to Hash Mismatches

A checksum that doesn't match feels like an alarm, but the alarm has only one setting: the two byte streams are not identical. It can't tell you why. The skill isn't re-running the hash — it's narrowing a mismatch to one of three causes: the data really differs, you're comparing the two hashes unfairly, or the reference value you're trusting is itself wrong. This guide walks all three.

You download a release — an ISO, a .tar.gz, an installer — and the project helpfully lists a SHA-256 next to it. You run sha256sum on your copy, line the two strings up, and they don’t match. Now what? The honest answer most people skip past is that the mismatch, on its own, tells you almost nothing. A hash is designed so that a single flipped bit and a completely different file both produce a wildly different digest. There’s no “close.” The comparison is all-or-nothing, and a failed comparison means exactly one thing: the two byte streams that went into the two hashes were not identical. Not corrupted, not tampered, not broken — just not the same. Everything useful is in figuring out why they weren’t.

(A note on words: this article uses “checksum” in the everyday release-download sense — the value you compare after downloading a file. Strictly, SHA-256 is a cryptographic hash or digest, while something like CRC32 is a non-cryptographic checksum. MD5 is still a cryptographic hash, but its collision resistance is broken: it catches most accidental changes in non-adversarial file checks, yet it isn’t fit for security. The debugging below is the same whichever you’re holding.)

So don’t re-run the hash five times hoping it changes. It won’t. Instead, treat the mismatch as the start of a three-way split. A checksum comparison has exactly three moving parts, and a mismatch means one or more of them is off — sometimes several at once, which is why the checklist at the end walks all three rather than stopping at the first hit:

  1. The data really is different. The bytes you hashed aren’t the bytes the reference was computed from — a truncated or interrupted download, the wrong file, a re-packaged archive, genuine corruption in transit.
  2. You’re comparing the two hashes unfairly. The bytes might be identical, but you’re not putting the two digests on equal footing — different algorithm, a hex-versus-base64 representation gap, or copy-paste noise in the string you’re eyeballing.
  3. The reference value itself is wrong or untrustworthy. The number you’re matching against is stale, from a different build, or served from a place you have no reason to trust — in which case a “match” would have been the real problem.

This guide walks the three in the order you should actually check them, because the cheapest and most common cause — you compared the strings wrong — is the one people suspect last.

The one idea: a hash is a fingerprint, and fingerprints don’t come in “almost”

Everything below rests on one property. A cryptographic hash maps any input — one byte or a 4 GB disk image — to a fixed-length fingerprint, and it’s built to be avalanche-sensitive: change a single bit of input and roughly half the output bits flip. That’s the whole point. It’s what lets a 64-character string stand in for a gigabyte and still catch a one-byte corruption.

The consequence people forget is the flip side of that power: the output carries no information about how different the inputs were. Two files that differ by one byte and two files that share nothing produce digests that look equally, totally unrelated. So when you see a mismatch, you cannot read severity out of it. “The hash is completely different” does not mean “the file is badly corrupted” — it’s what you’d see from a single trailing newline, too. This is why staring at the two hex strings for longer never helps, and why the debugging is never “how different are they” but always “which of the three moving parts is off.”

Keep that in mind: the fingerprint gives you one bit of information and stops talking. And note which direction is which. A mismatch proves the byte streams differ — that direction is airtight. A match is strong evidence they’re the same, but with a collision-resistant algorithm it’s evidence, not a mathematical proof; two different inputs sharing a digest (a collision) is astronomically unlikely for SHA-256 but not logically impossible. For this article that asymmetry barely matters — you’re debugging a mismatch, and a mismatch never lies about whether the bytes differ. It just won’t tell you why. The rest is on you.

The commands you’ll actually run

Before the categories, the tools. On macOS or Linux you either compute one hash and eyeball it, or let the system verify a whole checksums file for you:

sha256sum file.iso            # Linux: print the SHA-256
shasum -a 256 file.iso        # macOS: same thing, BSD tooling
sha256sum -c checksums.txt    # verify every file listed against its recorded hash

That last form is the one to prefer when a project ships a checksums.txt (or SHA256SUMS): it does the string comparison for you and prints OK or FAILED per file, which sidesteps the copy-paste mistakes in the next section entirely. On Windows PowerShell:

Get-FileHash .\file.iso -Algorithm SHA256

With those in hand, here are the reasons the digest or comparison can still fail.

Check first: are you even comparing the two hashes fairly?

This is the least glamorous cause and one of the most common, so rule it out before you touch the download. The bytes may be perfectly fine; you’re just not comparing the two digests on equal terms.

Copy-paste noise. You selected the reference hash off a web page and picked up a leading space, a trailing newline, or — on a line that wrapped — a chunk that’s silently truncated. A digest is fixed-length, so length is your friend here: MD5 is 32 hex characters, SHA-1 is 40, SHA-256 is 64, SHA-512 is 128. If the string you pasted is 63 characters where it should be 64, you didn’t get a different file; you got a bad copy. Count the characters before you conclude anything.

Hex case. People panic when one string is uppercase and the other lowercase. Don’t: case does not change the value. AB and ab are the same byte, 0xAB. Hex is just a rendering of the digest, and sha256sum happens to emit lowercase while some Windows tools and release pages use uppercase. Any correct comparison is case-insensitive on the hex. The trap isn’t the case itself — it’s that when you “fix” the case by hand, or copy across a case boundary, you sometimes drop or add a character in the process. The value is identical across case; a literal === string compare in a script is not, so lowercase both sides before comparing in code.

Hex versus base64. Same digest, two alphabets. A SHA-256 is 32 raw bytes; written as hex it’s 64 characters, but written as base64 it’s a 44-character string ending in =. Subresource Integrity attributes in HTML (integrity="sha256-…") use base64; sha256sum uses hex. If one side looks like 47DEQpj8HBSa… and the other like e3b0c44298fc…, you may be looking at one digest in two encodings rather than two different files — but confirm that both references name the same algorithm first, then convert one alphabet to the other and compare. Two unlike-looking strings aren’t proof of a shared digest on their own; they’re a prompt to check. (Base64 has its own reasons for looking “wrong” that trip people up in exactly this way — that’s a whole story of its own.)

To take the guesswork out of the algorithm and the hex case, drop your file — or paste the text you’re hashing — into the hash generator, select the exact algorithm the reference names, and compare its hexadecimal output. You can switch algorithms and toggle upper- or lower-case hex, so you can match the reference’s exact function and casing instead of assuming — note the tool outputs hex only, not base64, so if the reference is base64 you’ll need to convert one side.

Then: are you sure it’s the same algorithm?

If both values are in the same hexadecimal representation and their lengths differ, this one’s usually obvious — you hashed with SHA-256 (64 hex chars) and the page listed an MD5 (32) or a SHA-512 (128). (The caveat matters: hex and base64 encode the same digest at different lengths, so rule out an encoding gap first — a 44-character base64 SHA-256 isn’t “a shorter algorithm.”) Match the algorithm and move on.

The nastier version is same length, different family. SHA-256 and SHA3-256 both produce 64 hex characters and are completely different functions. The same issue applies to SHA-224 and SHA-384, to truncated digest variants, and to BLAKE2 configured to a 256-bit output — all easy to confuse with a same-length neighbor. A page that just says “SHA256: …” is usually SHA-2, but “SHA3”, “Keccak”, or “BLAKE” in the label means you must select that exact function, not the same-length default. When two 64-character hashes disagree and the file is genuinely identical, an algorithm-family mismatch is the first thing to suspect — the length gave you false confidence that you were comparing like with like.

Text inputs: the bytes changed even though the “content” didn’t

Everything so far assumed you’re hashing a file — a fixed sequence of bytes that either arrives intact or doesn’t. Hashing text opens a second front, because “the same text” can be several different byte sequences, and the hash sees bytes, not meaning. A digest computed from a string will diverge if:

  • the character encoding differs (UTF-8 vs UTF-16, or UTF-8 vs a legacy code page),
  • there’s a byte-order mark on one side and not the other,
  • the line endings are LF on one machine and CRLF on another,
  • one side has a trailing newline and the other doesn’t — the classic case of hashing a bare byte versus the same byte with a newline appended:
printf x     | sha256sum   # one byte: 'x'
printf 'x\n' | sha256sum   # two bytes: 'x' plus a newline — a completely different digest

(This is exactly the trap in echo "x" | sha256sum, since echo silently appends that newline for you.) Each of these changes the input bytes without changing anything a human would call “the content,” and with a cryptographic hash like SHA-256 the avalanche effect turns any one of them into a totally different digest. Text encoding and newline differences are involved enough — and cause enough of these “same input, different hash” surprises across two machines — to get a separate guide; here it’s enough to know that if you’re hashing text rather than a downloaded file, the encoding and newline layer is a prime suspect. The fix depends on who controls the two sides: if both are yours, normalize them to match (same encoding, same line endings, a deliberate choice about the trailing newline) before hashing. If you’re checking against a published reference, you can’t “normalize” someone else’s fixed value — you have to reproduce the exact bytes it was computed from (the right encoding, the right newline convention), then hash those.

Now the download: the data really is different

Only once you’ve confirmed you’re comparing the same algorithm, fairly, on the same kind of input does it make sense to conclude the bytes genuinely differ. When they do, the usual culprits are mechanical:

  • A truncated or interrupted transfer. The download stopped early — a dropped connection, a full disk, a proxy that closed the stream — and you hashed a partial file. Compare the on-disk size against the size a trusted source advertised — the release page, or the Content-Length header when that value is present and reliable. A file clearly smaller than the trusted size points to a truncated download, though size alone doesn’t prove it; confirm by re-downloading and re-hashing.
  • You grabbed a different artifact than the checksum describes. Checksums are per-file. The value next to app-linux-x64.tar.gz will not match app-linux-arm64.tar.gz, and the checksum for version 2.4.0 won’t match the 2.4.1 you actually pulled from a mirror. Confirm the filename and version the reference was published for.
  • The archive was re-packaged. Two .zip or .tar.gz files can contain byte-for-byte identical contents and still hash differently, because the container records timestamps, file ordering, and compression settings that differ between builds. If a mirror re-compressed the release, its checksum won’t match the original publisher’s even though every extracted file is the same. Verify against the checksum from the same source that produced the archive.
  • Genuine corruption. Rarer than the above on modern networks, but real: a flaky disk, bad RAM, a broken cable. A clean re-download that matches confirms only that the first copy differed — not why. It could equally have been the wrong file or version the first time, a mirror serving different bytes, a re-packaged archive, or a local edit. Corruption is one hypothesis the re-download is consistent with, not something it proves on its own.

Notice that “the data is different” is the last category worth a deep dive, not the first. A mismatch feels like corruption, but genuine corruption is often less common than comparison or reference errors.

Finally: should you even trust the reference?

Step back from the mechanics for a moment, because there’s a case where a mismatch is doing its job and a match would have been the worrying result. A checksum only means something if the reference value comes from a source you trust and reached you through a channel that couldn’t have altered both the file and the number together.

If the release download and its published hash live on the same page, served over plain HTTP, then anyone who could tamper with the file could also rewrite the hash to match it. The check would pass and prove nothing. This is where the purpose of a checksum matters, and where a common misconception bites: a plain MD5 or SHA-256 next to a download is an integrity check against accidental damage — a corrupted transfer, a bad mirror, a truncated file. It is not an authenticity guarantee against a deliberate attacker. A hash by itself has no secret in it; anyone can recompute it for any file, so publishing a checksum next to the file it describes can’t stop an attacker who controls that page from replacing both at once. To defend against tampering you need something an attacker can’t forge, and which one depends on your setting:

  • For public software distribution — a download page anyone can fetch — the answer is a digital signature: GPG, or Ed25519/minisign, where the publisher signs the release (or its checksum file) with a private key and you verify with their widely-published public key. This is why Linux distributions ship a signed checksums file, not a bare hash.
  • An HMAC is the right tool only when both sides already share a secret key — an API verifying a webhook payload, two services you control. It’s useless for a public download, because you’d have to hand every downloader the key, at which point it’s no longer a secret and an attacker has it too.

That distinction also decides how much the algorithm matters. For catching random, non-adversarial corruption from a source you already trust, even MD5 will reliably flag the overwhelming majority of accidental changes. For anything where someone might deliberately construct a colliding file, MD5 and SHA-1 are broken and you want SHA-256 or better — and for true tamper-proofing, a signature rather than a bare hash. That trade-off — when the aging algorithms are still fine and when they absolutely aren’t — gets its own comparison. The one-line version for right now: if the reference and the file share a fate — same untrusted server, same channel — a matching hash is false reassurance, and the fix is to get the reference from an independent, trusted place, not to keep re-hashing.

The checklist for a mismatched checksum

When the two strings don’t line up, resist the urge to re-run anything. Walk this in order — it moves from the cheapest, most common causes to the ones worth a deeper look, and any combination of them can be in play at once:

  1. Compare fairly first. Count the characters (32/40/64/128 for MD5/SHA-1/SHA-256/SHA-512). Lowercase both sides — case never changes the value. Make sure you’re not comparing hex against base64. Most “mismatches” die right here.
  2. Same algorithm? With both sides in the same hex representation, a different length points to a different algorithm — but confirm it isn’t just hex-vs-base64 first. Same length can still be a different family — SHA-256 vs SHA3-256 vs BLAKE2 all give 64 hex characters. Match the exact function named on the reference.
  3. Hashing text, not a file? Then the bytes can differ while the “content” looks the same: encoding, a BOM, LF vs CRLF, a trailing newline. If both sides are yours, normalize them to match; if you’re checking a published reference, reproduce the exact bytes it was computed from.
  4. Now suspect the data. Check the file size against what was advertised (truncated download), confirm you pulled the exact file and version the checksum names, and remember a re-packaged archive hashes differently even with identical contents.
  5. Do you trust the reference at all? If the file and its hash came from the same untrusted place, a match proves nothing. A bare hash is an accident detector, not tamper-proofing — for a public download that means a digital signature (GPG, Ed25519), and an HMAC only where you already share a secret key.

Under all five is the single idea from the top: a mismatch proves the byte streams differ; a match is evidence of equality, not absolute proof — and neither tells you why. The mismatch isn’t the answer; it’s the question. Your job is only to decide which of the three parts — the data, the comparison, or the reference — moved, and sometimes more than one did. Every case above is one specific version of that.