I used to skip the checksum.
A vendor page would print a SHA-256 next to the download link, and I'd click, install, and move on. That habit ended the day a release tarball unpacked without a single error and then segfaulted on startup. The archive on my disk was 41 MB. The real one is 118 MB. A proxy in front of the CDN had cut the transfer, and nothing between the CDN and my filesystem noticed.
Four seconds with sha256sum would have caught it. Now I verify first, using one of three methods depending on what I already have open.
Method 1: Python 3.11 and hashlib
For files I want a streaming read. hashlib.sha256(open(path).read()) is fine for a config file and rude for a 4 GB disk image.
This is the script I keep at ~/bin/sha256file.py, running on Python 3.11.1:
#!/usr/bin/env python3 """Print the SHA-256 of one or more files, reading in 1 MiB chunks.""" import hashlib import sys CHUNK = 1024 * 1024 def sha256_file(path: str) -> str: h = hashlib.sha256() with open(path, "rb") as f: # binary mode, not "r" for chunk in iter(lambda: f.read(CHUNK), b""): h.update(chunk) return h.hexdigest() if __name__ == "__main__": for p in sys.argv[1:]: print(f"{sha256_file(p)} {p}") The two-column output is deliberate. It matches what GNU sha256sum prints, so I can paste the result into a CHECKSUMS file and diff it against the vendor's list.
$ python3 sha256file.py release-2.4.1.tar.gz 3917ea2ec94345943fd55c88fc021982508fce43c1963d0619d454fcb9d49291 release-2.4.1.tar.gz For CI I don't want output, I want a verdict:
EXPECTED = "3917ea2ec94345943fd55c88fc021982508fce43c1963d0619d454fcb9d49291" got = sha256_file("release-2.4.1.tar.gz") if got == EXPECTED: print("OK") else: raise SystemExit(f"MISMATCH\n expected {EXPECTED}\n got {got}") The rest of the algorithm list is in Python's hashlib documentation. MD5, SHA-1, SHA-256 and SHA-512 are all available, and hashlib.new("sha512") picks one by name at runtime.
One thing that bit me: the available set depends on the OpenSSL build underneath. On OpenSSL 3.0.13, hashlib.new("md4") raises ValueError: unsupported hash type md4, while the same call on an older machine worked fine. If your script has to run on both, read hashlib.algorithms_available rather than assuming.
Method 2: the shell, or Node when JS is already running
On a Linux box the quickest path is the one already installed. With GNU coreutils:
$ sha256sum -c CHECKSUMS.txt release-2.4.1.tar.gz: OK And when the file is wrong:
$ sha256sum -c CHECKSUMS.txt release-2.4.1.tar.gz: FAILED sha256sum: WARNING: 1 computed checksum did NOT match $ echo $? 1 That exit code is the entire reason I put this in a pipeline instead of eyeballing the hex. Non-zero fails the step.
When the job is already Node (I'm on v22.13.1), node:crypto does it without shelling out:
import { createHash } from "node:crypto"; import { createReadStream } from "node:fs"; const hash = createHash("sha256"); for await (const chunk of createReadStream("release-2.4.1.tar.gz")) { hash.update(chunk); } console.log(hash.digest("hex")); // 3917ea2ec94345943fd55c88fc021982508fce43c1963d0619d454fcb9d49291 Node 22 lets you for await straight over a read stream, so there is no on("data") plumbing to get wrong. For a short string it collapses to one line:
createHash("sha256").update("hello").digest("hex"); // '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' Method 3: hashing a string in the browser
Large files are the hard case. Short strings are the everyday one: a webhook body I need to fingerprint, a JSON config whose value I suspect changed, a token I'd rather not paste into a shell where it lands in history.
For those I paste the text into a browser-based hash generator and get MD5, SHA-1, SHA-256 and SHA-512 back at once. No install, no REPL warm-up, and the text stays in the tab instead of going to someone else's server. That last part is why I'll use it for a production token and not for a random paste bin.
My most frequent use is webhook debugging. A provider signs the request body with HMAC-SHA256, and before I touch the signature I hash the raw body to confirm the bytes are what I think I received. About half the time the mismatch turns out to be a trailing newline my own logging added.
If you need the same thing inside your own app, MDN's SubtleCrypto.digest() is the browser-native route, though it's async and hands you an ArrayBuffer to convert yourself.
Three ways this bites you
Your shell adds a byte you didn't ask for. echo appends a newline, printf doesn't. The vendor hashed the file bytes; you hashed the bytes plus \n:
$ echo 'hello' | sha256sum 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 - $ printf 'hello' | sha256sum 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 - Line endings are bytes too. A Windows checkout with core.autocrlf=true rewrites every \n as \r\n, and the hash follows:
$ printf 'a\r\nb\r\n' | sha256sum 58055bdcc73787eb88c78d36f0b4939e9c5dc1c3ad17e25cc85a6833cf1a0cab - $ printf 'a\nb\n' | sha256sum 911169ddaaf146aff539f58c26c489af3b892dff0fe283c1c264c65ae5aa59a2 - Python has the same trap: open with "r" instead of "rb" on Windows and the newline translation happens before your first update() call.
A matching MD5 tells you nothing about an attacker. MD5 and SHA-1 are still good at catching a truncated download and worthless if someone wanted to hand you a different file that hashes identically. SHA-1 collisions stopped being academic years ago. Anything touching security gets SHA-256.
And a hash is not encryption. sha256(password) is not password storage, use bcrypt or argon2.
Which one to reach for
| Situation | Use | Why |
|---|---|---|
| Large file on Linux | sha256sum -c | Fastest, exit code fits CI |
| Large file in a Python job | hashlib with chunked reads | No subprocess, bounded memory |
| Large file in a Node job | node:crypto + read stream | Stays in the language you're in |
| Short string on someone else's laptop | this free hash tool | Nothing to install, nothing uploaded |
| Passwords or tokens at rest | bcrypt / argon2 | Hashing is one-way, not secret |
I still check every download before I run it. Four seconds, and the one time it fails it hands back an afternoon. The 41 MB tarball cost me a morning of bisecting a library version that was never the problem. On machines I don't own, the hash page on utilvy is what I open.
More free browser-based utilities at utilvy.com