Wiki

A version string is a label, not a fingerprint

A version string identifies what the producer remembered to stamp, not the bytes you are holding. The stamp is written by hand somewhere in the build, and a build can change the bytes without touching the line that writes the stamp. When that happens, three genuinely different artifacts will all introduce themselves with the same name, and every tool that trusts the name will treat them as one thing. If you need to know which build something actually is, the only honest answer comes from hashing the bytes.

When to reach for it

You are reasoning about which build of an artifact you have, or which build someone else has, and the evidence you are leaning on is a self-reported version: a font's name table, a binary's --version output, a package's __version__, an image tag, a User-Agent. The string looks authoritative, it matches what you expected, and you are about to conclude that two artifacts are the same build (or that one is the build that contains a fix) because their version strings agree.

The shape

Two facts collide.

  1. The version stamp and the build are separate steps. The bytes come out of a compiler, a font compiler, a bundler. The version string comes out of whatever line of code or config happens to write it. Nothing in the pipeline forces those two to move together. A maintainer can ship a real content change and simply not bump the stamp, because the stamp lives in a metadata file they did not edit this time.
  2. Everything downstream reads the stamp, not the bytes. Caches, dependency resolvers, bug-triage threads, "is this the patched build" checks. They all compare the cheap label because the label is right there and hashing feels like work. So a stale or frozen stamp propagates a false identity through every system that trusts it.

The collision: several distinct artifacts present one identity, and a conclusion built on that identity ("these are the same build" or "this is the build with the fix") is confidently wrong about bytes it never looked at.

The discriminator

Hash the bytes. sha256sum on each artifact answers the question the version string only pretended to answer. Two equal hashes are the same build; two different hashes are different builds, whatever their labels say. When the artifacts live in git, you can do this without downloading anything: git show "<tag>:<path>" | sha256sum hashes the file as it existed at a tag, and git log -1 --format="%h %ad %s" <tag> -- <path> names the commit that last touched those bytes. Now you have an identity pinned to content and a provenance pinned to a commit, instead of a label pinned to whatever the producer remembered to type.

Real applications

vercel/geist-font#238 (2026-06-23)

The thread turned on whether the font binary had actually changed across releases, since the embedded version read Version 1.700 at more than one tag. Trusting the string, you would say the font was untouched. Hashing the bytes said otherwise: the GeistMono-Regular.ttf shipped at 1.7.0 (48fced4d5ccd), at 1.7.1 and 1.7.2 (5a0de4b3d54a, identical to each other), and at 1.8.0 (bbb6715a4069) were three distinct binaries, and all three introduced themselves as Version 1.700. The 1.8.0 bytes were the output of the "Fix style name, double encode math greeks" build (5f44fc0), a real content change that never moved the stamp. The version string had frozen at 1.700 while the bytes changed underneath it three times. The hashes were the only honest account of what shipped.

When not to use it

Related

Revisit

Add the container-image and Go-module analogues when they fire in practice. A :latest tag and a mutable image digest have exactly this split (the tag is the label, the digest is the fingerprint), and a Go module's pseudo-version encodes the commit hash precisely to close this gap. If a second real application lands outside the font/binary world, generalize the discriminator beyond sha256sum to whichever content-address the ecosystem already ships.