kmail.at
← learning

linux · difficulty ◆

sha256sum — prove a file is the file it claims to be

51.2 MB hashed in 0.115 seconds on this box — the check is far too cheap to skip, and the output format has two traps worth knowing.

Every checksum file you have ever pasted has two spaces in the middle of it, and the reason is an old argument about text files on Windows.

2026-10-14 · 5 min read

$ sha256sum

What it does

sha256sum reads bytes and prints a 64-character hexadecimal digest — SHA-256, 256 bits, computed as described in FIPS-180-2, which is what this build's own --help says. No file operand means standard input, and '-' means standard input explicitly, so a hash can come out of a pipe as easily as out of a disk. Each output line is built from four pieces: the digest, a space, one character for the input mode ('*' for binary, a space for text), and the file name. That is why the gap in the middle of a checksum file is usually two spaces wide, and GNU states plainly that on GNU systems there is no difference between binary and text mode at all — the marker is for other platforms. The half of the command most people never use is -c: give it a file produced by any of the *sum tools and it re-hashes every entry and prints a verdict per line — OK, FAILED, or FAILED open or read — then exits 0 or 1. --tag switches the output to BSD style for interop, -z terminates records with a NUL instead of a newline, and the whole thing streams, so memory stays constant whether the file is 2 KB or 200 GB.

Why it matters

Three jobs, one command. Verifying a download: the vendor publishes a SHA256SUMS file next to the tarball, you run sha256sum -c and either everything says OK or you have caught a truncated transfer before installing it. Verifying a backup: hash the archive at write time, hash it again after the copy to the NAS, and the two digests agreeing is a stronger statement than any log line claiming success. Fingerprinting a tree: pipe a manifest of per-file digests into one more sha256sum and you have a single digest for an entire deployment, which is what you compare before and after a migration to prove nothing changed. The cost is negligible — I hashed a 51.2 MB archive on this box in 0.115 seconds, roughly 445 MB/s, with no sha_ni flag in /proc/cpuinfo, so this is not exotic hardware, it is just fast. Compare that with the alternative: cmp tells you two files differ but not which of four hundred files, and a directory listing with sizes and timestamps tells you almost nothing.

What it cannot tell you

A digest is evidence of integrity, never of authenticity. If the checksum file and the tarball came from the same compromised mirror, the hashes agree perfectly and you have learned nothing about the sender — that is a job for a signature (gpg --verify, minisign), and only a signature binds the bytes to a key. Second, hashing is all-or-nothing: there is no partial check and no progress bar, and a mismatch says nothing about where or how much the files differ, so use cmp -l when you actually need to diff two big binaries. Third, SHA-256 is the right default partly because MD5 and SHA-1 are not: MD5 collisions were practical by 2004 and were used to forge a rogue certificate authority in 2008, and in February 2017 Google and CWI Amsterdam published two different PDFs with an identical SHA-1. If the digest protects something that matters, it should be SHA-256 or better; b2sum is sitting right there on this box if you want BLAKE2.

Example

$ sha256sum backup-2026-10-14.tar.gz
sha256sum -c SHA256SUMS
261edf25512c36ad24f3381a52140d6785283ccd40a96b0a8f859f27d99186e0  backup-2026-10-14.tar.gz
backup-2026-10-14.tar.gz: OK

The whole workflow in two commands, on a 51.2 MB archive I created on this box so the digest is real and reproducible from the bytes. The first line is the receipt: 64 hex characters, two spaces, the name — and those two spaces are digest, space, mode character, so a plain text-mode line always has an empty slot in the middle. The second command is the one people forget: -c does not take file names from the argument list, it reads them out of the checksum file, re-hashes each one, and prints OK only when it matches. Time this pair once and you will never skip it again: the hash of that 51.2 MB file took 0.115 seconds, about 445 MB/s, on a CPU whose flags include avx2 but no sha_ni.

$ sha256sum -c wrong.sums
sha256sum -c SHA256SUMS
backup-2026-10-14.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
backup-2026-10-14.tar.gz: OK
sha256sum: gone.bin: No such file or directory
gone.bin: FAILED open or read
sha256sum: WARNING: 1 listed file could not be read

Both halves of a real verification report, captured verbatim. The first is the outcome you are looking for the absence of: the digest in wrong.sums was 64 zeros, the computed one was not, so grep-style FAILED on the file line and a WARNING with the count on stderr — and the exit status is 1. The second is the nastier case, because the checksum list mentioned a file that is no longer on disk: you get the OS error, a verdict of 'FAILED open or read', and a separate warning that counts unreadable files rather than mismatched ones. That distinction matters when you are reading a report at 03:00 — a mismatch means corruption, an unreadable file means your list and your directory disagree about what should be there. Both exit 1, and so does a checksum file whose lines are garbage: 'no properly formatted checksum lines found'.

$ sha256sum -c --ignore-missing SHA256SUMS
sha256sum -c --quiet SHA256SUMS
echo "261edf25512c36ad24f3381a52140d6785283ccd40a96b0a8f859f27d99186e0  backup-2026-10-14.tar.gz" | sha256sum -c -
backup-2026-10-14.tar.gz: OK
sha256sum: gone.bin: No such file or directory
gone.bin: FAILED open or read
sha256sum: WARNING: 1 listed file could not be read
backup-2026-10-14.tar.gz: OK

The three shapes you actually want in automation, all verified here. --ignore-missing is for the common case of a vendor manifest that lists a dozen files when you downloaded two: the absent entries are skipped silently and the exit status reflects only what was checked. --quiet inverts the report so stdout carries only the failures — perfect for cron, where a silent run means success and any line at all is the alert, though note that the missing-file diagnostics still go to stderr. The last one is the web-page case: a single hash copied out of a release note, pasted into a here-string with two spaces and piped into sha256sum -c -, which prints OK and exits 0. No temp file, no trust in the terminal's line wrapping.

$ find . -maxdepth 1 -name '*.bin' -type f -exec sha256sum {} + | sort -k2 | sha256sum
bf0419930ce4106d739069bba6b32e23a8730a7db98a43a781b364fee7f14f59  -

One digest for a whole set of files, and the pattern I use to prove a restored tree is identical to the original. The inner find batches every match into as few sha256sum processes as possible thanks to -exec ... {} + (the older \; form spawns one process per file and is dramatically slower on big trees), sort -k2 makes the manifest deterministic — directory traversal order is not something you should bet on — and the outer sha256sum collapses the whole manifest into a single line. Change one byte anywhere in the tree and this number changes. Two practical upgrades: use -print0 with xargs -0 when file names may contain spaces, and for huge trees throw xargs -P$(nproc) at it, because the hash of one file has nothing to do with the hash of another.

$ printf abc | sha256sum
sha256sum /dev/null
: > empty.txt; sha256sum empty.txt
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad  -
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  /dev/null
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  empty.txt

The one-line answers that prove the implementation is doing what it says. 'abc' is the FIPS 180-2 test vector for SHA-256 and this build returns ba7816bf…15ad, exactly as published — which is also the fastest way to check that the sha256sum on some unfamiliar box is genuine and not a wrapper that returns something plausible. Hashing /dev/null returns the digest of zero bytes, e3b0c442…b855, and you will meet that same constant whenever a log rotation, a failed upload or a truncated backup produced an empty file: it is the fingerprint of nothing at all, and it is worth recognising on sight because it looks like a perfectly ordinary hash. Note the '-' in the first line — that is the file name field reporting the input came from stdin.

$ sha256sum "weird
name.txt"
sha256sum -z "weird
name.txt" | tr '\0' '|'
\2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881  weird\nname.txt
2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881  weird
name.txt|

What happens when a file name contains a newline, which it legally can and occasionally does. GNU detects it and defends the one-line-per-file promise by prefixing the whole record with a backslash and replacing the newline with the two literal characters \n — so the line stays one line and a parser can tell it is escaped (I confirmed both characters with cat -A). The -z form takes the opposite approach: it prints the raw name, newline and all, and ends each record with a NUL byte instead, which is why the tr in the pipeline shows the terminator as a pipe. If you are feeding checksums to a script, -z is the safer interface; if a human will read it, the escaped form is the one you want. Either way, this is exactly the behaviour coreutils 8.25 documented when it fixed the status output to a single line per file.

Common flags

-c, --check
Read checksum lines from the given files instead of hashing them, and verify each listed file. Accepts both the GNU format and the BSD 'SHA256 (file) = hash' form that --tag produces, and prints OK, FAILED or FAILED open or read per entry. The exit status is 0 only if every entry verified.
--tag
Print BSD-style output: SHA256 (name) = digest. Added in coreutils 8.20 (2012-10-23), complete with GNU file name escaping, for interoperability with BSD and macOS tooling — and modern GNU sha256sum -c reads that form back happily (verified here).
-b, --binary / -t, --text / -z, --zero
-b and -t set the mode character to '*' or a space in both output and verification; on GNU systems the digest is identical either way, so you only care when exchanging files with Windows. -z ends records with NUL and disables name escaping, which is the format that survives any legal file name.
--ignore-missing
Skip entries whose file is not present instead of reporting them as FAILED open or read. Added to md5sum in coreutils 8.25 (2016-01-20) and applied to the whole sha*sum family, it exists for the very common case of a vendor manifest listing more files than you downloaded.
--quiet / --status
--quiet suppresses the OK lines so only the failures reach stdout; --status prints nothing at all and communicates only through the exit code — the cleanest form inside a script, and the one to use with if or set -e. Both still let the missing-file diagnostics through to stderr.
-w, --warn / --strict
--warn reports lines that are not properly formatted instead of skipping them silently, and --strict turns those same lines into a non-zero exit. Worth knowing because a checksum file full of garbage otherwise fails with the friendly but uninformative 'no properly formatted checksum lines found'.
- (stdin) and the sibling binaries
No FILE, or FILE given as '-', hashes standard input, so hashes flow through pipes in both directions. The same binary is installed under sha1sum, sha224sum, sha384sum, sha512sum, md5sum and b2sum (all coreutils 9.4 here), and since coreutils 9.0 the unified form exists too: cksum -a sha256 file.

History

15 August 2006 — sha256sum arrives with base64 and shuf in coreutils 6.0

The NEWS file installed with this package lists sha256sum under a header that reads, in full, '* Major changes in release 6.0 (2006-08-15) [unstable]', and under '** New programs' it sits in a small family: 'base64: base64 encoding and decoding (RFC 3548) functionality. sha224sum: print or check a SHA224 (224-bit) checksum. sha256sum: print or check a SHA256 (256-bit) checksum. sha384sum … sha512sum … shuf: Shuffle lines of text.' So SHA-2 verification, base64 and shuf all landed in the same release, four years before SHA-1's own weaknesses became embarrassing and eleven years before SHAttered. Before 6.0 the family was md5sum and sha1sum, and anyone who needed SHA-256 from a shell was reaching for openssl dgst -sha256, which prints a different format and does not verify manifest files. The other detail in that same release is worth keeping: the digest is computed as described in FIPS-180-2, which is why the output of a correct implementation is checkable against a published test vector.

2012 to 2023 — --tag, --ignore-missing, and the day cksum subsumed them all

Four dates from the same file, each one a flag I now use weekly. 8.20 (2012-10-23): 'md5sum now accepts the --tag option to print BSD-style output with GNU file name escaping' — the interoperability switch, and the reason a checksum file can now be read by both worlds. 8.22 (2013-12-13): 'md5sum can use libcrypto hash routines where allowed to potentially get better performance through using more system specific logic. sha1sum for example has improved throughput by 40% on an i3-2310M.' 8.25 (2016-01-20): --ignore-missing arrived, together with a deceptively important format fix — 'md5sum now ensures a single line per file for status on standard output, by using a \ at the start of the line, and replacing any newlines with \n' — which is the escaping behaviour I reproduced on this box with a file name containing a real newline. And finally 9.0 (2021-09-24), where the whole family was declared finished: 'cksum now supports the -a (--algorithm) option to select any of the existing sum, md5sum, b2sum, sha*sum implementations etc. cksum now subsumes all of these programs, and coreutils will introduce no future standalone checksum utility.' The successors still exist and still work — this box ships them all at 9.4 — but cksum -a sha256 file now returns the identical digest in BSD-style form, and cksum -c auto-detects tagged checksum files.

Fun facts

Pros & cons

pros

  • + Verification as a one-command pass/fail report: -c reads a manifest, re-hashes every entry and returns a single exit status, which is the difference between trusting a transfer and knowing about it
  • + Streams from stdin, files, pipes and device nodes with constant memory, and costs almost nothing — 51.2 MB in 0.115 s here — so it belongs in a deploy script rather than a wiki page
  • + Composes with the rest of the shell: find -exec ... {} + builds a manifest, sort makes it deterministic, and one more sha256sum turns an entire directory tree into a single digest you can compare before and after a migration

cons

  • − Integrity is not authenticity: without a signature it proves only that the bytes you have are the bytes that were listed, which is no defence against a manifest served by the same compromised mirror
  • − All-or-nothing and silent about differences — no partial check, no progress, and a mismatch tells you nothing about where the file diverges, which is cmp's job rather than this one
  • − Format details leak into scripts: the mode character, the escaping of newlines in file names with a leading backslash, and four distinct failure modes that all collapse into exit code 1

Takeaways

  1. 1Verify with -c before you install or archive anything: sha256sum -c SHA256SUMS, plus --ignore-missing when the manifest lists more files than you downloaded and --quiet when only failures should make noise.
  2. 2Hash on both ends of a copy. Two matching 64-character lines are evidence; a successful rsync or cp exit status is a claim.
  3. 3Fingerprint a tree with find . -type f -exec sha256sum {} + | sort -k2 | sha256sum — one digest for a whole deployment, before and after a migration or restore.
  4. 4Retire md5sum and sha1sum wherever a collision would be a problem; use sha256sum or b2sum, and remember that coreutils has already unified all of them behind cksum -a sha256.
  5. 5Never branch on the exit status alone: 1 covers a mismatch, an unreadable file and a malformed manifest. Log the words OK, FAILED and FAILED open or read, because they mean three different things at 03:00.

Related commands