Hash Mismatch Debugging: Why Your SHA Doesn't Match
Debug hash mismatches: encoding differences, trailing newlines, BOM markers, algorithm confusion, and hex case sensitivity. Complete troubleshooting guide.
The Problem
Hash mismatches appear random because the difference between inputs is invisible: a trailing newline, a BOM marker, different character encoding, or even uppercase vs lowercase in the hex output. These bugs can block deployments, break authentication, and corrupt data integrity checks.
You compute a hash, compare it to the expected value, and they don't match. The inputs look identical. You've triple-checked the algorithm. But the hashes are different. Hash mismatch bugs are among the most frustrating in software development because hashing is deterministic - the same input always produces the same output. If the hashes differ, the inputs are different, even if they look the same. This guide shows you exactly where to look.
Common errors covered
Trailing newline changes the hash
Hash matches in online tool but not in terminal
echo 'hello' | sha256sum gives different result than expected
The echo command appends a newline (\n) by default. Hashing 'hello' (5 bytes) gives a completely different result than hashing 'hello\n' (6 bytes). This is the #1 cause of hash mismatches.
Step-by-step fix
- 1 Use the Hash Generator - it hashes exactly what you type, no trailing newline.
-
2
In terminal, use
echo -n(no newline):echo -n 'hello' | sha256sum. -
3
Use
printfinstead ofecho:printf 'hello' | sha256sum. -
4
Compare byte counts:
echo 'hello' | wc -cvsecho -n 'hello' | wc -c.
# echo adds trailing newline: $ echo 'hello' | sha256sum 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 # That's the hash of 'hello\n' (6 bytes), not 'hello'
# echo -n prevents trailing newline: $ echo -n 'hello' | sha256sum 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 # That's the hash of 'hello' (5 bytes)
Different character encoding produces different hash
Same text produces different hashes on different systems
Hash from Python differs from hash in JavaScript
Hashing operates on bytes, not strings. The string 'hello' in UTF-8 and UTF-16 produces completely different byte sequences and therefore different hashes. Even UTF-8 with BOM vs without BOM differs.
Step-by-step fix
- 1 Explicitly use UTF-8 encoding on all platforms.
- 2 Use the Hash Generator as a reference (it uses UTF-8 internally).
-
3
In Python:
hashlib.sha256('hello'.encode('utf-8')). -
4
In JavaScript:
new TextEncoder().encode('hello')(always UTF-8).
# Encoding mismatch:
import hashlib
# UTF-16 encoding:
hash_utf16 = hashlib.sha256('hello'.encode('utf-16')).hexdigest()
# Different result - includes BOM bytes!
# Always use UTF-8:
import hashlib
hash_utf8 = hashlib.sha256('hello'.encode('utf-8')).hexdigest()
# '2cf24...' - matches all other UTF-8 implementations
BOM (Byte Order Mark) prepended to file content
File hash doesn't match after re-saving
Hash of downloaded file differs from source
A UTF-8 BOM (\xEF\xBB\xBF, 3 bytes) at the start of a file is invisible in text editors but changes the hash. Windows Notepad and some editors add it by default.
Step-by-step fix
-
1
Check for BOM:
hexdump -C file.txt | head -1- look foref bb bf. - 2 Use the Diff Checker to compare files byte-by-byte.
-
3
Strip BOM:
sed -i '1s/^\xEF\xBB\xBF//' file.txt. - 4 Save files as 'UTF-8 without BOM' in your editor settings.
# File with BOM (3 extra bytes at start): $ hexdump -C file.txt | head -1 00000000 ef bb bf 68 65 6c 6c 6f |...hello| $ sha256sum file.txt a1b2c3... # Hash of BOM + content
# File without BOM: $ hexdump -C file.txt | head -1 00000000 68 65 6c 6c 6f |hello| $ sha256sum file.txt 2cf24d... # Hash of just content
Wrong algorithm selected (MD5 vs SHA-256 vs SHA-512)
Hash length doesn't match expected
32-char hash compared against 64-char hash - always fails
Different hash algorithms produce different length outputs: MD5 = 32 hex chars, SHA-1 = 40, SHA-256 = 64, SHA-512 = 128. Comparing hashes from different algorithms always fails.
Step-by-step fix
- 1 Check the hash length to identify the algorithm: 32=MD5, 40=SHA-1, 64=SHA-256, 128=SHA-512.
- 2 Use the Hash Generator to compute all algorithms side by side.
- 3 Match the algorithm to what was used by the source/API/verification system.
- 4 For security: avoid MD5 and SHA-1 (collision attacks exist); use SHA-256 or higher.
# Comparing hashes from different algorithms: expected = '5d41402abc4b2a76b9719d911017c592' # 32 chars (MD5) actual = '2cf24dba5fb0a30e26e83b2ac5b9e29e...' # 64 chars (SHA-256) - will NEVER match
# Same algorithm on both sides: import hashlib expected = '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' actual = hashlib.sha256(b'hello').hexdigest() assert expected == actual # True - both SHA-256
Uppercase vs lowercase hex comparison fails
String comparison returns false despite identical hash
'ABC123' !== 'abc123' in case-sensitive comparison
Hash outputs are hexadecimal strings. Some tools output uppercase (ABC123), others lowercase (abc123). Direct string comparison is case-sensitive and fails when cases don't match.
Step-by-step fix
- 1 Normalize both hashes to lowercase before comparing.
- 2 The Hash Generator outputs lowercase by default.
-
3
In code:
hash1.toLowerCase() === hash2.toLowerCase(). -
4
For constant-time comparison (security): use
crypto.timingSafeEqual().
// Case-sensitive comparison fails: const expected = '2CF24DBA5FB0A30E...'; // uppercase const actual = '2cf24dba5fb0a30e...'; // lowercase expected === actual; // false!
// Normalize case before comparing: const match = expected.toLowerCase() === actual.toLowerCase(); // For security-sensitive comparisons (timing-safe): const a = Buffer.from(expected.toLowerCase(), 'hex'); const b = Buffer.from(actual.toLowerCase(), 'hex'); crypto.timingSafeEqual(a, b);
Debugging Approach
- 1 Compute the hash with the Hash Generator tool as a reference baseline.
- 2 Check byte count: is your input exactly the number of bytes you expect?
- 3 Look for invisible differences: trailing newlines, BOM markers, different encoding.
- 4 Verify the algorithm: check hash length (32, 40, 64, or 128 hex characters).
- 5 Normalize hex case (lowercase) and compare character by character.
Prevention Checklist
-
Always use
echo -norprintfinstead ofechowhen piping to hash commands. - Explicitly encode strings as UTF-8 bytes before hashing on all platforms.
- Save files as UTF-8 without BOM.
- Check hash length to confirm the correct algorithm: 32=MD5, 40=SHA-1, 64=SHA-256.
- Normalize hex strings to lowercase before comparing.
-
Use constant-time comparison (
timingSafeEqual) for security-sensitive hash checks.
Frequently Asked Questions
Why does the same file have different hashes on Windows and Mac?
Most likely cause: line ending conversion. Windows uses CRLF (\r\n) and Mac uses LF (\n). Git's core.autocrlf setting may convert line endings on checkout, changing the file's bytes and therefore its hash.
Is MD5 still safe for file integrity checks?
For integrity (detecting accidental corruption): MD5 is fine. For security (detecting tampering): MD5 is broken - collision attacks can create two different files with the same MD5 hash. Use SHA-256 for security-sensitive checksums. Our Hash Generator supports both.
How do I verify a downloaded file's checksum?
Download the file and the published checksum. Run sha256sum downloaded-file in terminal or use the Hash Generator tool. Compare the output with the published checksum. They must match exactly (case-insensitive).
Related Debug Guides
Related Tools
Still stuck? Try our free tools
All tools run in your browser, no signup required.