No, pickle model files are not safe to load from an untrusted source. Python's pickle format does not store data alone: it stores instructions for rebuilding objects, and those instructions can name any importable function and call it with attacker-chosen arguments. A model checkpoint saved with pickle is therefore closer to a script than to a data file, and torch.load() or pickle.load() on a hostile file can run code with the full permissions of the process that loads it. Safetensors exists to remove that risk: it holds raw tensor bytes plus a JSON header, and nothing in the format can trigger code execution. The practical rule is simple. Treat every model artefact as code, prefer safetensors, and decide centrally which models your teams and agents may load.
Why pickle deserialization is remote code execution
Pickle is a small stack-based virtual machine. When Python unpickles a file, it walks a sequence of opcodes that push values, import names and call them. The __reduce__ protocol lets any object tell pickle "to rebuild me, call this function with these arguments". An attacker does not need a bug in your code: they write a class whose __reduce__ returns something like os.system and a shell command, pickle it, and ship the file. Whoever loads it runs the command.
That design is decades old and documented. The Python documentation warns that the pickle module is not secure and that you should only unpickle data you trust. The problem for machine learning is that much of the ecosystem grew up treating model weights as data, and moved them around the way you would move a CSV.
Formats that are pickle under the hood include:
- PyTorch checkpoints (
.pt,.pth, and olderpytorch_model.bin): a zip archive with a pickled object graph inside. - joblib and scikit-learn model dumps, which wrap pickle.
- NumPy
.npy/.npzfiles when object arrays are allowed (allow_pickle=True). - Keras/TensorFlow files are not pickle, but a saved
Lambdalayer can carry serialized Python code, which is why Keras added a safe mode for loading.
Hugging Face's own pickle security page walks through the same mechanics and points users to safetensors as the alternative.
This is not theoretical
Public model hubs make it easy to download and load a model in two lines, which is exactly what an attacker wants. JFrog's security research team reported finding around 100 models on Hugging Face with real, harmful payloads, excluding false positives, aimed at PyTorch and TensorFlow users. The pattern is the same as a malicious package on a public registry: a plausible name, a working model, and a payload that runs on load.
For AI agents the stakes rise. An agent runtime that pulls a fine-tuned model, an embedding model or a LoRA adapter at startup often runs with credentials for tools, databases and LLM providers. Code that runs at model load inherits all of that. We cover the wider pattern in AI agent supply chain security.
Why model scanners get evaded
Scanners such as picklescan, and the scanning built into model hubs and commercial tools, typically disassemble the pickle opcodes and flag imports of dangerous callables (os.system, subprocess, builtins.exec, eval and similar). That catches the lazy payload. It has structural limits:
- Denylist, not allowlist. The scanner flags names it knows are dangerous. Python's standard library and installed packages contain many other callables that can be chained into code execution. A gadget the list does not cover passes.
- Parser mismatch. The scanner and the real loader must read the file the same way. Researchers have shown payloads in malformed or unusually compressed archives that a scanner failed to parse while PyTorch still loaded the malicious part. When the scanner errors out and the loader does not, the scan gives a false sense of safety.
- Payload position. Pickle executes opcodes in order. A file can execute its payload early and then be broken afterwards; the loader has already run the code by the time it fails.
- Format drift. Each framework and version wraps pickle slightly differently. Scanners lag the formats.
Commercial model-security tools add deeper analysis, sandboxed detonation and behavioural signals, and we compare several in HiddenLayer alternatives and Protect AI alternatives. They raise the bar. None of them changes the fact that loading pickle means running a program, so the strongest control is not loading untrusted pickle at all.
What safetensors changes
Safetensors, created by Hugging Face, is deliberately boring. A file is:
- an 8-byte header length,
- a JSON header listing each tensor's name, dtype, shape and byte offsets, plus optional string metadata,
- a flat buffer of raw tensor bytes.
There is no object graph, no import, and no callable. The loader validates offsets and copies (or memory-maps) bytes. That gives three useful properties: loading cannot execute code by design, files can be loaded lazily and quickly, and the header can be inspected without reading the weights.
Safetensors does not make a model trustworthy. It removes one attack class, code execution on load. A safetensors file can still carry poisoned or backdoored weights, and the code that ships next to it (modeling_*.py loaded through trust_remote_code=True, tokenizer scripts, custom pipelines) is still code. Pair the format with provenance checks such as model signing so you know who produced the weights.
Safetensors vs pickle at a glance
| Property | Pickle-based (.pt, .bin, joblib) |
Safetensors |
|---|---|---|
| Can execute code on load | Yes, by design | No |
| Stores | Arbitrary Python object graph | Tensors plus string metadata |
| Inspect without loading | Only by disassembling opcodes | Read the JSON header |
| Scanner needed | Yes, and it can be evaded | Not for load-time code execution |
| Lazy / memory-mapped loading | Limited | Yes |
| Still needs provenance and weight-integrity checks | Yes | Yes |
Covers adjacent code (trust_remote_code, custom layers) |
No | No |
Hardening checklist for teams that load models
Use this as a baseline for data science teams and for any service, including agent runtimes, that loads model artefacts.
Where agents change the picture
Agents add two twists. First, they choose. A planner that can call a "load model" or "download from hub" tool turns model selection into a runtime decision an attacker can try to steer through prompt injection. Second, they hold credentials. The runtime that loads the artefact usually holds keys for LLM providers, MCP servers and internal APIs.
That makes model loading a governance question, not just a data science hygiene question. The controls that matter are which tools an agent may call, which models it may use, and whether there is a record of both. The broader set of supply-chain controls is in securing the agent supply chain.
How Praesidia fits
Praesidia does not scan model files or replace a model-security scanner. It works on the agent side of the problem:
- Discover. Praesidia's agent discovery builds one inventory from the agents and MCP servers you register, the traces you send, and the GitHub and GitLab repositories you connect, so you can see which agents exist and what they connect to.
- Secure. Model allow-lists and tool allow-lists are attributes of each governed connection. They are checked when a task is dispatched, before the work starts, so an agent cannot quietly switch to an unapproved model or call a tool its policy does not allow.
- Prove. Denials and decisions are written to an append-only, hash-chained audit trail, signed under the default configuration, which you can verify offline.
Pair that with safetensors, pinned digests and signed weights, and a malicious checkpoint has far fewer places to land.