Model backup checks: verify a saved SHA-256 manifest

model-backupself-hostingpythondata-integrity

TL;DR: Verify a restored model directory against a manifest saved from a known-good copy, rather than generating a new baseline after restoring. File existence and size checks miss same-size changes. A SHA-256 comparison detects differences from the saved baseline, but does not establish model safety, publisher identity, or runtime compatibility.

What you’ll be able to do:

  • Capture hashes for an explicit selection of local model files.
  • Check a restored copy without loading model weights or installing PyTorch.
  • Budget the storage reads and preserve an independent baseline.

Honest take: Keep the manifest outside the model directory and test the restore against that saved file; a fresh hash of a damaged copy is no recovery evidence.

What does a model checksum check prove?

A model checksum check compares current file bytes with expected digests. If the expected values came from an independent, trusted baseline, a match is evidence that those selected bytes agree with that baseline. It does not prove that the original file was harmless or that the model will load.

That distinction closes a gap between our shard presence preflight and an actual application restore. A file can exist, be nonempty, and have the expected size while containing changed bytes. The Safetensors header inspection tutorial checks another layer: structural metadata. Payload integrity requires separate evidence.

Use separate labels in recovery notes: files present, selected hashes matched, format parsed, and application workload passed. Each label should describe a check you actually ran. Combining them into a single “model verified” message hides which failure modes remain untested.

Should you use Hub verification or a local manifest?

Use the Hub’s verification tooling when you need to compare a downloaded artifact with repository information, and a saved local manifest when you need an offline comparison against your own approved copy. The Hugging Face download guide documents pinned revisions; record a full revision in the recovery notes rather than relying only on a branch label.

CheckUseful evidenceDoes not establish
Path and sizeSelected file exists and has the recorded byte countSame-size contents are unchanged
Local SHA-256 manifestSelected bytes match the saved baselineBaseline publisher or model safety
Format validationThe inspected file satisfies that parser’s checksUseful inference or artifact provenance
Application restore drillRecorded workload works in the restored environmentEvery possible workload works

Our offline model backup guide describes the larger download and recovery workflow. This article implements the local comparison step with Python’s standard library. It does not call Hub APIs, fetch weights, import custom model code, or run an inference engine.

How much hardware and storage does hashing need?

The helper needs a Python 3.9-or-later interpreter and read access to the selected files, with write access only when creating a manifest. It uses Path.is_relative_to, which Python documents as added in 3.9. No GPU is required because the operation reads bytes and updates a digest; it does not interpret model tensors.

The helper reads in blocks of 1,048,576 bytes. That block size is an implementation choice, not a model-memory requirement. File data is processed incrementally instead of loading an entire weight file into a Python byte string. Python, filesystem buffers, and the operating system still consume memory, so the block size is not a total process-memory limit.

Budget storage traffic explicitly. If your selected set contains a hypothetical 12 GB of files, one complete capture reads 12 GB, and one subsequent verification reads another 12 GB. That is 24 GB of logical file reads across those operations, before backup copying and filesystem overhead. These are illustrative arithmetic values, not benchmark results or a real model’s download size.

There is no paid API or software subscription in this helper. Existing storage, electricity, and time are still real costs; no universal dollar estimate follows from a digest count. Do not buy a GPU to accelerate a recovery check that is simply waiting for a storage device to deliver bytes.

How do you capture and verify an explicit file set?

Save the following as model_manifest.py. Use a materialized directory whose selected paths are ordinary files, not a Hub cache snapshot composed of symbolic links. The restrictive path policy is deliberate: it avoids following files outside the selected root during this small tutorial.

import argparse
import hashlib
import json
import re
from pathlib import Path, PurePosixPath

BLOCK = 1024 * 1024
MAX_MANIFEST = 8 * 1024 * 1024

def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("duplicate JSON key")
        result[key] = value
    return result

def selected_file(root, name):
    if not isinstance(name, str) or not name or "\\" in name:
        raise ValueError("invalid path")
    part = PurePosixPath(name)
    if part.is_absolute() or any(x in (".", "..") for x in name.split("/")):
        raise ValueError("non-relative path")
    if any(not x or ":" in x for x in name.split("/")):
        raise ValueError("unsupported path")
    current = root
    for component in part.parts:
        current = current / component
        if current.is_symlink():
            raise ValueError("symlink not supported")
    resolved = current.resolve(strict=True)
    if not resolved.is_relative_to(root) or not resolved.is_file():
        raise ValueError("not a contained regular file")
    return resolved

def measure(path):
    digest = hashlib.sha256()
    size = 0
    with path.open("rb") as stream:
        while True:
            block = stream.read(BLOCK)
            if not block:
                break
            size += len(block)
            digest.update(block)
    return {"bytes": size, "sha256": digest.hexdigest()}

parser = argparse.ArgumentParser()
parser.add_argument("mode", choices=["capture", "verify"])
parser.add_argument("root", type=Path)
parser.add_argument("manifest", type=Path)
parser.add_argument("files", nargs="*")
args = parser.parse_args()
try:
    root = args.root.resolve(strict=True)
    if not root.is_dir():
        raise ValueError("root is not a directory")
    manifest = args.manifest.resolve()
    if manifest.is_relative_to(root):
        raise ValueError("keep manifest outside root")
    if args.mode == "capture":
        if not args.files or len(set(args.files)) != len(args.files):
            raise ValueError("select unique explicit files")
        records = {name: measure(selected_file(root, name)) for name in args.files}
        document = {"version": 1, "files": records}
        with args.manifest.open("x", encoding="utf-8") as stream:
            json.dump(document, stream, indent=2, sort_keys=True)
        print(f"CAPTURED files={len(records)}")
    else:
        if args.files:
            raise ValueError("verify uses the saved file selection")
        with args.manifest.open("rb") as stream:
            raw = stream.read(MAX_MANIFEST + 1)
        if len(raw) > MAX_MANIFEST:
            raise ValueError("manifest too large")
        document = json.loads(raw, object_pairs_hook=unique_object)
        if not isinstance(document, dict) or document.get("version") != 1:
            raise ValueError("unsupported manifest")
        records = document.get("files")
        if not isinstance(records, dict) or not records:
            raise ValueError("empty or invalid file selection")
        for name, expected in records.items():
            if not isinstance(expected, dict):
                raise ValueError("invalid record")
            if type(expected.get("bytes")) is not int or expected["bytes"] < 0:
                raise ValueError("invalid byte count")
            if not isinstance(expected.get("sha256"), str):
                raise ValueError("invalid digest")
            if not re.fullmatch("[0-9a-f]{64}", expected["sha256"]):
                raise ValueError("invalid digest")
            actual = measure(selected_file(root, name))
            if actual != expected:
                raise ValueError("file differs from baseline")
        print(f"VERIFIED files={len(records)}")
except (OSError, ValueError) as error:
    parser.exit(1, f"FAIL: {error}\n")

Create the manifest only after checking the original selection and its provenance. Place it somewhere independent of the copy you will restore. This illustrative invocation names a configuration and a weight file; substitute your actual selected files:

python3 model_manifest.py capture ./approved-model ./baseline.json config.json model.safetensors
python3 model_manifest.py verify ./restored-model ./baseline.json

Successful output has the form CAPTURED files=2 followed, after a successful restore comparison, by VERIFIED files=2. The capture refuses to overwrite an existing manifest. Verification uses the saved selection; supplying new filenames to verification is rejected so that a caller cannot quietly narrow the check to whatever survived.

Keep repository ID, full revision, runtime version, license, and selection rationale in separate recovery notes. This helper does not fetch those facts or infer them from a directory name. It is your baseline comparison, not a publisher signature verifier.

What did the small-file test detect?

The helper was tested on November 10, 2026 with two synthetic files: config.json containing {}\n and weights.bin containing ABCD. They total 7 bytes. The binary name is deliberately generic: these bytes are not a valid model checkpoint.

A capture followed by verification passed. Changing ABCD to WXYZ preserved the 4-byte size but failed verification. Deleting the file failed as well. The fixture also exercised a malformed digest, duplicate JSON keys, an attempted parent-directory path, a symlink, an existing manifest, and an empty selection.

Fixture stateSize-only comparisonSaved-manifest comparison
Original two filesSamePass
ABCD replaced by WXYZSameFail
Weight file removedMissingFail
New unrelated extra fileOriginal sizes unchangedPass for the selected files

The extra-file case is intentional. This verifier checks the manifest’s selected set and does not assert that the directory contains nothing else. Inspect extra files separately before loading an application that discovers files automatically. A successful selection check cannot certify unlisted scripts, adapters, tokenizer files, or dependencies.

This helper rejects symlinks, even when a legitimate cache snapshot points to a valid blob. That is an unsupported layout under this tutorial’s policy, not evidence that the Hub cache is corrupt. Materialize a selected copy or use cache-aware verification tooling instead of changing the script to follow arbitrary links casually.

The root is resolved before comparison, and selected path components are checked. These checks assume a stable directory you control. They do not protect against another process swapping paths while the script runs. Stop writers or verify an immutable snapshot if concurrent modification is possible.

Python’s pathlib documentation explains resolution and path relationships. The hashlib documentation defines incremental digest updates. Neither API turns a changing directory into a transactional filesystem snapshot.

Honest take: when should you avoid this approach?

Avoid this helper when your backup system already provides an independently stored integrity catalog and a tested restore verifier. Another handmade manifest can add maintenance without adding evidence. Prefer the existing system if it checks the selected bytes and reports failures clearly.

Do not use a manifest stored only beside the restored weights as proof against deliberate tampering. Someone able to replace both files and baseline can make them agree. Preserve the baseline independently and protect its provenance; this program neither signs it nor authenticates its author.

Finally, do not stop at hashes when the goal is service recovery. Restore packages, configuration, tokenizer assets, and runtime state, then run a representative workload. The standard-library helper is useful precisely because its claim is narrow: the selected bytes match the saved baseline.

FAQ

Q: Can a model with matching hashes still fail to load? A: Yes. The original file set may be incomplete, incompatible with the runtime, or unsuitable for the task. Matching hashes establish agreement with the baseline only.

Q: Does this prove a model is safe to execute? A: No. File integrity, provenance, licensing, and code execution policy require separate review. The helper never loads model code.

Q: Is the manifest a complete directory inventory? A: No. It records the explicitly selected paths and ignores extras during verification. Inspect the complete restore before using automatic file discovery.

Sources

Last updated November 10, 2026. Local tests used synthetic bytes, not model weights or inference. Python is distributed under the PSF license; model files retain their own licenses.

Was this article helpful?

What self-hosting actually costs

Real cost breakdowns for self-hosted AI: hardware floors, power, maintenance hours, and the honest comparison against paying for it. No spam, unsubscribe anytime.