Verifying a Downloaded GGUF File's Checksum
8 min read · updated August 11, 2026
Most model cards do not publish a checksum, which leads people to conclude there is nothing to check against. There is: Hugging Face stores a SHA256 for every large file as a side effect of how it stores them, and it is available over the API without downloading anything.
What you are comparing against
Files above a size threshold on the Hugging Face Hub are stored through Git LFS, and an LFS pointer identifies its object by content hash. The algorithm is SHA256, so the oid recorded for a GGUF file is the SHA256 of the bytes you will receive. Nobody has to publish it separately; it is structural.
Two things that hash does and does not establish, because they are routinely confused. It establishes that your copy is byte-identical to the copy the Hub is serving — which is what catches a truncated download, a corrupted USB transfer, a proxy that mangled something, or a resumed download that overlapped. It does not establish that the weights are what the model card claims or that the uploader is trustworthy, because the same party publishes both the file and the hash. A checksum is an integrity check, not a provenance one.
Getting the published hash
Two routes, both cheap. The repository tree API returns metadata for every file including the LFS record:
curl -s "https://huggingface.co/api/models/bartowski/Meta-Llama-3.1-8B-Instruct-GGUF/tree/main" \ | python -c "import json,sys; [print(e['lfs']['oid'], e['size'], e['path']) for e in json.load(sys.stdin) if 'lfs' in e]"
Alternatively, a HEAD request against the download URL returns the same value in the X-Linked-ETag header. This is the more convenient form when you have a URL rather than a repository id, and it works for a file inside a subdirectory without a recursive listing:
curl -sI "https://huggingface.co/bartowski/Meta-Llama-3.1-8B-Instruct-GGUF/resolve/main/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf" \ | grep -i "x-linked-etag\|x-repo-commit"
One trap here, and it will waste an hour if you hit it. The plain etag header on the redirect target is not the SHA256 — on Xet-backed repositories it is a content-addressed-store hash from the chunked transfer layer, and it will never match your sha256sum. The header you want is X-Linked-ETag. The same repository also exposes a xetHash field in the tree API, which is likewise not what you are comparing.
Capture X-Repo-Commit while you are there. The hash tells you which bytes you have; the commit tells you which revision of the repository they came from, which is what you will want if the uploader re-quantizes and the file at main changes underneath you. This is a real failure mode rather than a theoretical one: a GGUF filename does not encode which llama.cpp version produced it, so the same path can serve different bytes six months apart with nothing in the name to warn you.
The procedure
- Read the published hash and size before downloading. Run one of the two commands above. Note both values: the size gives you a fast first check that costs no computation.
- Download the file.
hf download <repo> <filename>prints the local path on its last line, which is the path to hash. A directcurl -Lworks too; what matters is that the transfer completed rather than how. - Check the size first.
ls -l, orGet-Item <path> | Select-Object Lengthon PowerShell. A truncated download fails here in a second, and hashing 4.9 GB to learn the same thing takes considerably longer. - Compute the SHA256. Linux:
sha256sum <path>. macOS:shasum -a 256 <path>. Windows PowerShell:Get-FileHash -Algorithm SHA256 <path>. Windows cmd:certutil -hashfile <path> SHA256. All four produce the same 64 hex characters; PowerShell prints them upper-case, so compare case-insensitively. - Compare, without reading them yourself. Eyeballing two 64-character strings is how a mismatch gets missed. Do it in the shell:
EXPECTED=$(curl -sI "https://huggingface.co/<repo>/resolve/main/<file>.gguf" \ | tr -d '\r' | awk -F'"' '/^[Xx]-[Ll]inked-[Ee][Tt]ag/ {print $2}') ACTUAL=$(sha256sum ./<file>.gguf | cut -d' ' -f1) [ "$EXPECTED" = "$ACTUAL" ] && echo "OK $ACTUAL" || echo "MISMATCH: $ACTUAL != $EXPECTED" - Record the pair. Write the filename, the hash and the
X-Repo-Commitinto whatever holds your model inventory. This is what lets you answer “is the file on this machine the one we validated” six months later, and it is the whole point of doing this before the file is ever loaded.
When they do not match
A mismatch is nearly always mundane, and the order to check is by likelihood:
- Wrong hash compared. You used
etagorxetHashinstead of the LFS oid. Check this first, because it is the most common cause and it costs nothing to rule out. - Incomplete download. The size check catches this. A partially written file in the Hugging Face cache is left with an
.incompletesuffix; hashing one of those will never match. - The repository moved. Uploaders re-quantize when llama.cpp changes and force-push the same filename. Compare your recorded
X-Repo-Commitwith the current one — if they differ, you are hashing a different file that happens to share a name. - Actual corruption. Rare over HTTPS, much less rare over a USB stick or a flaky mount. Re-download and re-hash; if the second copy matches the published value and the first did not, the transfer was the problem.
Letting the tooling do it
The Hugging Face CLI will verify a cached repository against the Hub without you assembling any of the above:
hf cache verify meta-llama/Llama-3.2-1B-Instruct hf cache verify <repo> --revision <commit-sha>
It checks every file in a cached revision and reports whether all checksums match. Prefer it when the files came through the cache. Do the manual procedure when they did not — a file copied from a colleague, restored from a backup, or carried on a USB drive, which are exactly the paths where a silent corruption is plausible and where nothing has verified anything for you.
Note also that some repositories are gated: meta-llama requires accepting a licence before the API returns file metadata at all, so a tree request may come back empty rather than wrong. That is an access condition to satisfy on the model page, not something to route around.