safetensors over pickle, never set trust_remote_code=True on a repo you have not read, scan weights with picklescan or modelscan before promotion, pin to a commit hash for provenance, and gate licenses and access the way you already gate images. The Hub scans every upload for you, but that is a backstop, not your control plane.A model is just files in a repo. So is a backdoor. The hard part of Hugging Face security is not that the Hub is unsafe; it is that a model artifact looks inert and is not. A PyTorch checkpoint in the legacy pickle format is a program. When your data scientist runs from_pretrained, that program executes with their user rights: SSH keys, cloud credentials, environment variables, the lot. If you run the platform, you have seen this shape before. It is the same problem as letting an unscanned image into your registry, and the same controls apply.
A model repo is a registry artifact. Gate it like one.
The mental model that keeps you out of trouble: the Hub is to models what a container registry is to images. A repo has a name, revisions (git commits), tags, an owner, a license, and metadata you can vet. Nothing about «it is on Hugging Face» means it is safe, exactly as nothing about «it is on Docker Hub» meant an image was clean. You would never let a random public image run in production without a scan and a pinned digest. A model deserves the same gate: pull it, scan it, check its license and provenance, pin it to an immutable revision, mirror it internally, and only then let it touch a GPU. The parallel to a registry scanning and RBAC stack is exact, which is why this part links hard to the Harbor for Beginners series: the Hub is the model registry, and your job is to put a gate in front of it.
The threats that matter, mapped to controls you run
Five things go wrong, and each one has a control you have deployed before under a different name. The point of the table is not the threat; it is the column on the right, where the work you already do maps onto models with almost no translation.
| Threat on the Hub | What it really is | Control you already know |
|---|---|---|
| Pickle weights run code on load | Arbitrary code execution from a deserialized file | Block the unsafe format; scan before promote |
trust_remote_code | Running unreviewed Python shipped in the repo | Code review gate before anything executes |
| Backdoored or poisoned weights | Tampered artifact, no integrity guarantee | Pin to a commit hash; verify provenance |
| License trap | Terms that forbid your use or your region | License policy gate, like image base-layer policy |
| Uncontrolled access | Anyone pulling anything into prod | RBAC, private mirror, token scoping |
The file format is the control: pickle versus safetensors
This is the single highest-leverage decision in the whole part, so start here. Pickle is Python object serialization, and it is the default for legacy PyTorch weights. Deserializing a pickle can execute code, because the format carries instructions, not just data. The dangerous opcodes are REDUCE, GLOBAL and STACK_GLOBAL: they import a callable and invoke it. That is the whole exploit. A weights file named pytorch_model.bin can import os.system and run a command on the box that loads it.
safetensors exists to remove that capability. It stores raw tensor data plus a small JSON metadata header and nothing else. No Python objects, no __reduce__, no imports, no code path at load time. You cannot hide a payload in a safetensors file because there is nowhere to put one. If you set one policy this quarter, make it this: prefer safetensors, and treat a repo that only ships pickle as something to scan and convert, not to load.
Here is how you see the danger, and how you avoid it. First, inspect a pickle without loading it, then force the safe path in code.
# Read a pickle checkpoint without executing it
pip install picklescan
picklescan -p ./pytorch_model.bin
# Clean result:
# ----------- SCAN SUMMARY -----------
# Scanned files: 1 Infected files: 0 Dangerous globals: 0
# Malicious result flags the import outright:
# Dangerous globals: 1
# posix.system FOUND in pytorch_model.bin
# Force the safe format, or refuse to load
from transformers import AutoModel
model = AutoModel.from_pretrained('org/model', use_safetensors=True)
# Raises if no safetensors file exists, instead of silently loading pickle.
# Loading a safetensors file directly: raw tensors only, no code runs
from safetensors.torch import load_file
state_dict = load_file('model.safetensors')
The failure mode to know: picklescan only has something to scan when the file is a pickle. If the repo ships safetensors, the scanner reports nothing, and that is the desired state, not a gap. The gap is the reverse: a repo that ships only a legacy .bin and asks you to trust it.
Gotcha
Some loaders silently fall back to pickle when a safetensors file is missing. A model card can advertise safetensors while the revision you pull still contains a pytorch_model.bin. Pin the revision and assert the format in code with use_safetensors=True, so a missing safe file is a hard error in CI rather than a quiet pickle load in production.
What the Hub already scans, and why you still cannot lean on it
Hugging Face is not naive about this. Every file pushed to the Hub goes through a security scanner. Today that includes ClamAV antivirus and a pickle import scan that reads the opcode stream with pickletools.genops and lists the imports a pickle would pull, flagging suspicious ones on the file page without executing anything. On top of the platform scanners, third-party scanners from Protect AI and JFrog run on uploaded models and surface their findings on the repo. Protect AI alone has scanned millions of model versions and flagged tens of thousands of repos with unsafe or suspicious issues. This is real coverage and you should read those badges.
It is also a backstop, not your control plane, for the same reason an upstream registry scan does not satisfy your own gate. The Hub itself says the pickle import list is best-effort and not foolproof. Scanners have been bypassed in the wild: malicious checkpoints stored with non-standard compression slipped past detection until the tooling was updated. The lesson is the one you already apply to base images. Upstream scanning is a signal, your gate is the decision. You run the scan again, on your side, on the exact bytes you intend to promote.
trust_remote_code is remote code execution with a friendly name
Plenty of models need custom modeling code that does not ship in transformers. The repo includes Python, and the library will import and run it if you pass trust_remote_code=True. Read that flag literally: you are running unreviewed code from a public repo, with your user’s full rights, on the machine that does the load. It is the same risk as the pickle path, just more honest about it.
# This imports and runs Python shipped in the repo. Do not do this blind.
from transformers import AutoModel
model = AutoModel.from_pretrained('org/model', trust_remote_code=True)
# Safer: review the repo code at a specific commit, then pin to it.
model = AutoModel.from_pretrained(
'org/model',
revision='9f1a2b3c4d', # an immutable commit hash, never a branch name
)
The production rule: trust_remote_code=True is allowed only after a human reads the modeling code at a fixed commit and that commit is what you pin. A branch name like main is a moving target; the code you reviewed is not the code you might run tomorrow. If a repo cannot be used without remote code and you are not willing to review it, that repo does not enter your environment. When the custom code is really about hardware, send people to the NVIDIA AI series for the supported serving path instead.
Gated models, licenses, and provenance are access control
Three governance facts decide whether a model is even allowed in, before security gets a vote. Gated models sit behind an access boundary the owner sets: you request access while logged in, you agree to share your username and email with the author, and approval is automatic or manual. Access is granted to individual users, never to a whole organization, which matters for how you wire automation. In CI you authenticate with a scoped token tied to an account that has been granted access; there is no shortcut around the per-user grant.
Licenses are the trap that bites later. A model can carry terms that forbid commercial use, restrict a field of use, or block distribution into specific regions. The Hub even supports a metadata flag that disallows a gated model for users in the European Union when the license requires it. Treat the license like a base-image policy: a named allow list of licenses your org accepts, checked at the gate, not discovered by legal after launch. Provenance closes the loop. The Hub supports GPG-signed commits, which prove origin even though they do not prove safety. Combined with a pinned commit hash, a signed commit gives you the integrity story you would demand of any artifact: this is who published it, and this is the exact version we vetted.
A scan-before-promote gate you can run today
Put it together into one CI step that pulls an exact revision, scans the snapshot, and fails the job on any finding. This is the smallest gate that is worth having, and it maps one to one onto the image-promotion gate you already run.
pip install -U "huggingface_hub[cli]" modelscan
# Pull one immutable commit into an isolated dir, never a moving branch
hf download org/model --revision 9f1a2b3c4d --local-dir ./candidate
# Scan the whole snapshot for code in weights
modelscan -p ./candidate
# Fail the pipeline on any finding
if [ $? -ne 0 ]; then echo BLOCKED; exit 1; fi
echo PROMOTED
Worked example
A clean run on a safetensors-only repo: modelscan reports zero issues across the snapshot, the job prints PROMOTED, and your pipeline copies that exact revision into the internal mirror. Drop a legacy .bin with an os.system import into the same dir and rerun: modelscan reports the operator-issued call, returns a non-zero exit code, the job prints BLOCKED and stops. The artifact never reaches the mirror, so it never reaches a GPU. Same gate, two outcomes, no human in the hot path.
For the enterprise layer, Hugging Face Team and Enterprise plans add the controls a platform owner expects: SSO, audit logs, resource groups for access control, token governance, and storage region selection. Those are the org-level equivalents of the project RBAC and audit trails you run on your registry, and they are where the on-prem and air-gapped story begins, which is the subject of the finale. For the underlying concepts behind these models, fine-tuning and quantization as ideas rather than tooling, link back to the Generative AI series.
trust_remote_code use as a code review item, and keep a human approval step for anything touching regulated data.What I’d enforce
Model security on Hugging Face is not a new discipline you have to learn from zero. It is supply-chain control with unfamiliar file formats, and you already own supply-chain control. My recommendation is blunt: stand up a scan-and-pin gate between the Hub and an internal model mirror, default to safetensors, and forbid blind trust_remote_code. Why this is right: it puts the decision on your side of the boundary, where it belongs, and it produces an audit trail. When it is not right: for a throwaway research spike on an isolated box with no credentials and no production reach, the full gate is overhead, so let researchers pull freely in a sandbox and gate only at promotion. What to validate first: confirm the exact revision you scanned is the revision you serve, because an unpinned pull quietly breaks the entire chain. Build the gate once, wire it into CI, and every model after that crosses it without a meeting. If you take one action this week, add use_safetensors=True and a pinned revision to the loaders already running in your pipelines, then put the scan in front of promotion.
References
- Hugging Face Hub docs: Pickle Scanning
- Hugging Face Hub docs: Gated Models
- Hugging Face Hub docs: Third-party scanner Protect AI
- Hugging Face Hub docs: Third-party scanner JFrog


DrJha