Know what a model file will do before you load it.

Hayward is a static security scanner for machine-learning model files. It reads PyTorch, pickle, joblib, GGUF, SafeTensors, ONNX and Keras, and Hugging Face configs, without executing them, and tells you whether loading a checkpoint would run code.

Loading a model is running it.

torch.load, joblib.load and numpy.load(allow_pickle=True) execute code from the file they read. That is not a bug. It is what pickle does. The file is named pytorch_model.bin, and the .bin is doing a lot of work in that sentence: it is a program, and loading it is running it, on your machine, with your credentials, as you.

Hayward reads the file first and tells you what the loader would have done.

Read the file. Don't run it.

Format is decided by magic bytes, then the file is read structurally. Nothing in it is executed, and a clean result means the file was examined, not that the parser gave up.

01

Nothing is executed

Pickle streams are walked opcode by opcode with pickletools, simulating the stack and the memo. No pickle.load, no eval, no import. XML goes through defusedxml, so entity expansion is refused rather than performed.

02

Judged by arguments, not names

Deny lists lose: an attacker needs one callable nobody listed. Hayward resolves each call's arguments and convicts on the evidence. A URL, a shell-shaped command, or a host and port promote an unknown callable to HIGH, whatever it is called.

03

Structural integrity, not just code

Native loaders allocate from numbers in a file's header. Hayward replays the container arithmetic for GGUF, SafeTensors and TFLite and reports a header that disagrees with the file, the integer-overflow class behind CVE-2025-53630 and CVE-2026-42627.

04

Coverage is a finding

A payload hidden behind a deliberate parse error can scan cleaner than the plain one. In Hayward no parse attempt may lower suspicion or erase prior evidence, and a file it could not fully read is reported as unread, never passed as clean.

One scanner across the model-file zoo.

Format comes from magic bytes, so a payload renamed weights.safetensors, or given no extension at all, does not walk past on the strength of its name.

Pickle, and everything that wraps it

PyTorch (zip, legacy and .ptl mobile), joblib, NumPy .npy / .npz, TorchServe .mar, NVIDIA NeMo, skops.

Tensor containers

SafeTensors, GGUF and TFLite, checked for headers whose declared spans do not fit the file.

Graph formats

ONNX custom ops and external-data references, TensorFlow SavedModel, Keras Lambda layers (H5 and .keras), and PMML.

Repo config

Hugging Face config.json and tokenizer_config.json: auto_map, trust_remote_code, and Jinja chat_template injection, all of which run on load.

Embedded executables

PE, ELF and Mach-O magic anywhere in a model file. No serialisation format writes one.

Fits where the model enters.

pip install hayward
hayward scan ./models
Command line and reportsA local CLI with JSON, SARIF and HTML output, plus a desktop GUI.
Load guardimport hayward.guard makes torch.load and friends refuse a dangerous call as it runs.
Coding agentsAn MCP server your agent calls to check a checkpoint before it loads it.
Continuous integrationA GitHub Action that fails the build and uploads SARIF to code scanning.
Pre-commitA staged-files hook that scans model files before they land.
In the browserThe same scanner compiled to WebAssembly. The file never leaves your machine.
- uses: hedgerow-dev/hayward@v1
  with:
    path: models
    fail-on: high

A verdict you can check, not just trust.

A scan is only as good as your trust in the scanner. Hayward writes a signed in-toto receipt for a scan, and the hayward-verify service publishes a signed verdict for popular Hugging Face repos at pinned commits.

hayward verify org/name

hayward verify fetches the published verdict for a checkpoint before you download it, checks it was signed by the service's own workflow and signed recently, and lists every file the verdict does not cover. A pass means vouched for: no published verdict is not a pass, and a coverage gap is a fail.

A smoke alarm, not a survey of the building.

A clean report means Hayward read the files and recognised nothing dangerous in them. Not that the model is safe. It is built to stay quiet, because a gate that cries wolf gets switched off, and it reports the rooms it could not get into rather than calling them clean. Any accuracy figures we quote are self-measured and not independently reproducible; the harness runs against your own corpus.

Quiet on real models, by the numbers.

On 222 benign files, 210 real Hugging Face models pinned by hash and 12 the harness generates, Hayward raised no alarms and warned on 3% of them, 7 files, while returning a verdict on every file. At its default gate it blocks no benign file. That quiet is the point: a gate that cries wolf gets switched off.

The honest other half: on attack sets other people wrote, Hayward is neither the loudest tool nor silent. At its default threshold it lets some PickleCloak chains through, and a lower threshold catches more, still with no benign file blocked. We build Hayward and wrote the benchmark harness, the figures are self-measured, and the attack sets were not held back from our own development. Both thresholds, the method and the limits are in the write-up.

One dependency. No network calls.

Hayward installs with a single dependency and no model framework, runs on Python 3.10 and up, and hayward scan makes no outbound calls, so it runs on a laptop, in CI, or inside a regulated environment where nothing is allowed to leave the network. MIT licensed. The most useful thing you can send us is a file it gets wrong.

pip install hayward
hayward scan ./models