Why this matters
A model repository is executable supply chain, not just data. A pickle-based checkpoint can run arbitrary code when it is loaded. A repo that needs trust_remote_code runs its own Python inside your inference process. An unpinned main branch can change under you between two pod restarts. Inference nodes usually have GPU drivers, cloud credentials and network access to internal systems, so they make an attractive target.
Work through this checklist before any open-weight model or fine-tune reaches a production node pool. Run it again when you change the revision.
1. Source and identity
- The publisher is the organisation you expect: check the org name on the Hub, not only the model name. Typo-squatted orgs and re-uploads of popular models exist.
- The licence allows your use (commercial, redistribution, fine-tune outputs). Record the licence file's commit along with the model.
- For a gated model, the download token belongs to a service identity with read-only scope, not a person's account.
- Fine-tunes and merges list their base model. Assess the base as well: a problem in it carries through.
2. Pin an exact revision
Never deploy a branch name. Resolve it to a commit and pin that commit everywhere:
hf download meta-llama/Llama-3.1-8B-Instruct \
--revision <40-character-commit-sha> \
--local-dir /models/llama-3.1-8b-instruct
Get the current commit SHA from the API:
from huggingface_hub import HfApi
info = HfApi().model_info("meta-llama/Llama-3.1-8B-Instruct", revision="main", files_metadata=True)
print(info.sha)
for f in info.siblings:
if f.lfs:
print(f.rfilename, f.lfs.sha256, f.size)
- The serving config pins the same SHA:
vllm serve ... --revision <sha>orrevision=infrom_pretrained. - The tokenizer and chat template come from the same revision as the weights. A mismatched template gives quietly bad output, not an error.
Warning
--revision maintogether with a cache that gets wiped on node replacement means a new node can download different weights from the rest of the fleet. You end up with two models behind one service name, and evaluation results that no longer describe production.
3. Serialization format
| Format | Risk on load | Action |
|---|---|---|
*.safetensors | No code execution. Tensors only | Preferred |
*.bin, *.pt, *.pth, *.ckpt (pickle) | Can run arbitrary code | Scan, then convert to safetensors in a sandbox, or reject |
*.gguf | Tensor format. Parser bugs have happened | Keep llama.cpp / Ollama patched |
*.h5, Keras Lambda layers | Can contain serialized code | Scan, or reject |
| ONNX / TensorRT engines | Custom ops can load native libraries | Build engines yourself from vetted weights |
- The repo has safetensors weights. If both formats are present, download only safetensors:
from huggingface_hub import snapshot_download
snapshot_download(
repo_id="<org>/<model>",
revision="<sha>",
allow_patterns=["*.safetensors", "*.json", "tokenizer*", "*.model", "*.jinja"],
local_dir="/staging/<model>",
)
- Loader settings refuse pickle where the library supports it:
use_safetensors=Truein Transformers, andtorch.load(..., weights_only=True)for any of your own checkpoints.
4. Scan the files
Run ModelScan on the staged directory, in CI, on a machine with no credentials:
pip install modelscan
modelscan -p /staging/<model>
echo "exit code: $?"
ModelScan exit codes (check the docs for your version): 0 no issues, 1 issues found, 2 scan error, 3 no supported files, 4 usage error.
- The pipeline fails on any non-zero exit except
3(3is expected for a safetensors-only directory). - Run a malware scanner over the directory as well. Repos sometimes contain scripts, notebooks and archives besides the weights.
- No unexpected executables or archives:
find /staging/<model> -type f ! -name '*.safetensors' ! -name '*.json' ! -name '*.txt' ! -name '*.md' ! -name '*.model' ! -name '*.jinja'.
Tip
A clean scan doesn't prove the model behaves well. Scanners catch serialization exploits, not backdoored weights that answer normally until they see a trigger phrase. That is what the evaluation step (section 7) is for.
5. Remote code
- Search the repo for custom code:
ls /staging/<model>/*.pyand checkauto_mapinconfig.json. - If the model needs
trust_remote_code=True, someone reviews every.pyfile at the pinned SHA, and the review is recorded with the SHA. - Prefer a release where the architecture is supported natively by your serving engine version, so you don't need remote code at all.
-
--trust-remote-codenever appears in a shared base image or Helm default. It's set per model, with a reference to the review.
6. Hash, mirror and promote
Production nodes should never pull from the public Hub. Promote the vetted files to an internal store and verify the hashes on every load.
cd /staging/<model>
find . -type f -print0 | sort -z | xargs -0 sha256sum > ../<model>-<sha>.sha256
az storage blob upload-batch --account-name mlmodels --destination models/<model>/<sha> \
--source . --auth-mode login
# or: aws s3 sync . s3://ml-models/<model>/<sha>/
In the pod's init container, verify before the server starts:
cd /models/<model> && sha256sum --quiet -c /manifests/<model>-<sha>.sha256 || exit 1
- For LFS files, the hashes match the
lfs.sha256values from the Hub API in section 2. - The store path includes the commit SHA, and the path is immutable (versioning, or a write-once policy).
- Inference pods set
HF_HUB_OFFLINE=1, and egress tohuggingface.cois blocked from the inference namespace, so a missing file fails the pod instead of quietly downloading from the internet. - Only the promotion pipeline's identity can write to the model store. Inference identities can only read it.
7. Behavioural checks before promotion
- Run your standard evaluation suite against the exact staged files, served with the production engine version and flags.
- Add safety probes: refusal behaviour, prompt injection resistance, and a set of trigger-style prompts (unusual tokens, rare strings) compared against the base model's output.
- Compare against the version currently in production. Look for large unexplained shifts in answer length, refusal rate or tool-call format.
- Record the model SHA, scan result, eval scores and approver in the model registry entry.
8. Runtime hardening
| Control | Why |
|---|---|
| Model volume mounted read-only | A compromised process can't swap the weights |
| Inference pods run as non-root, no privilege escalation | Limits what a loader exploit can do |
| No cloud credentials on inference pods beyond reading the model store | Contains the blast radius |
| Egress NetworkPolicy: only the gateway, model store and telemetry | Blocks exfiltration and silent downloads |
| Image and model SHA logged at startup | You can tell which weights served a given request |
kubectl -n inference get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].args}{"\n"}{end}' | grep -o -- '--revision [a-f0-9]*' | sort | uniq -c
If that command prints more than one revision for a single service, the fleet is serving mixed weights.
Prevent it next time
- Make the checklist a pipeline: download at a pinned SHA, scan, hash, evaluate, promote. Nobody copies weights by hand.
- Re-run scans when you upgrade ModelScan or your serving engine. New detections and loader changes apply to old models too.
- Review the model inventory every quarter: drop unused models, and confirm every production model still has a recorded SHA, licence and approver.