create_repo with private=True first, then push_to_hub or hf upload. Treat the repo like a private registry project: scope tokens to one repo, use org roles for read/write/admin, and pin every consumer to a commit hash instead of main. Push safetensors only, ship a real model card so provenance and license travel with the weights, and let hf_xet handle the large transfers.hf CLI logged in (see Part 3), and a model or dataset sitting on disk.You finish a fine-tune on Friday. You run model.push_to_hub('my-model') and go home. Monday, half the team is already pulling it, the licensing reviewer is asking what data it was trained on, and the repo is sitting public on the open internet because nobody set private. That is not a Hugging Face problem. It is a registry-governance problem, and you have solved it before for container images.
The Hub is a model registry. Pushing to it is promotion of an artifact from a workstation into a shared, versioned, access-controlled store. Every control you already enforce on images has a direct equivalent here: private projects, RBAC, immutable tags, provenance metadata, and supply-chain checks on the file format. The verbs change, the discipline does not. This part covers how to push models and datasets the way you would promote any other build artifact, and where the Hub will quietly do the wrong thing if you let it.
The mental model: push is promotion, not save
A push to the Hub creates or updates a Git-based repository. The weights live in large-file storage behind that repo, every change is a commit, and consumers resolve a model by repo id plus a revision. If you have ever promoted an image from a build runner into a private registry and then pinned deployments to a digest, you already have the right diagram in your head. Here it is in Hub terms.
Create the repo first, and make it private
The single most common mistake is letting push_to_hub auto-create a public repo. Create the repo yourself, set private=True, and only then push into it. Creating it explicitly also means the repo exists with the right visibility before any bytes move, so you never have a window where weights are public.
from huggingface_hub import create_repo, whoami
print(whoami()['name']) # confirm WHICH identity you are pushing as
repo = create_repo(
repo_id='acme-ml/intent-classifier', # org/name, not just name
repo_type='model', # or 'dataset'
private=True, # never rely on the default
exist_ok=True, # idempotent: safe to re-run in CI
)
print(repo.url)
# https://huggingface.co/acme-ml/intent-classifierExpected: the repo is created (or left alone) and private. Failure mode: a bare repo_id='intent-classifier' creates it under your personal namespace, not the org, so teammates cannot find it and your token may not even have rights to it. Always namespace with the org.
RBAC: orgs, roles, and scoped tokens
A personal repo is a single-owner artifact. For anything a team depends on, push into an organization. Org repos give you per-member roles, which is the registry RBAC you already run, just renamed. Pair that with fine-grained tokens: a token scoped to write to exactly one repo is the equivalent of a deploy key or a robot account with one push target. That is what belongs in CI, never your personal write-all token.
| Hub control | What it gates | Registry analogy |
|---|---|---|
private=True | Repo invisible outside you or the org | Private project, no anonymous pull |
| Org member roles | read / write / admin per person | Project-level RBAC |
| Fine-grained token | One repo, read or write only | Scoped robot account / deploy key |
| Gated repo | Access behind terms and approval | Pull policy plus license gate |
Three ways to push, and when to use each
There are three upload paths and people pick the wrong one constantly. push_to_hub() is the convenience method on a transformers model, tokenizer, or datasets object you already have in memory. upload_folder() and its CLI twin hf upload take a directory on disk and are the right tool for anything large or anything produced outside Python. upload_file() is for a single file. The decision is mechanical.
Worked example
Promote a trained model directory into the private repo with a real commit message, on a named branch, as a pull request so a reviewer signs off before it reaches main:
hf upload acme-ml/intent-classifier ./out
--repo-type model
--revision candidate-v3
--create-pr
--commit-message 'v3: f1 0.91 on holdout, trained on tickets-2026q1'
# Output (trimmed):
# Uploading: 100% 1.34G/1.34G
# pr created: https://huggingface.co/acme-ml/intent-classifier/discussions/7That single command does what a registry promotion plus a merge request does: it lands the bytes on a branch, opens a reviewable PR, and leaves main untouched until a human approves. The commit message carries the metric and the data source so the lineage is in the history, not in someone’s memory.
Versioning: commits, branches, and why you pin the hash
Every push is a commit. The Hub gives you branches and tags on top of that, and consumers resolve a model through the revision argument. The mistake that bites teams in production is loading from main and assuming it is stable. It is not. main is a moving pointer, exactly like the latest tag on an image. Someone pushes a new commit and every job that loads main silently changes behaviour on the next pull.
from transformers import AutoModelForSequenceClassification as Model
# Fragile: tracks whatever main points to right now
m = Model.from_pretrained('acme-ml/intent-classifier')
# Production: pinned to an immutable commit, reproducible forever
m = Model.from_pretrained(
'acme-ml/intent-classifier',
revision='a1b2c3d', # full or short commit hash, or a tag
)Expected: the pinned load returns the exact weights from that commit every time. Failure mode: omit revision and a teammate’s push to main changes your model under you with no deploy on your side. Pin the hash, or cut an immutable tag per release and pin that.
Pushing datasets without melting your bandwidth
Datasets push the same way, but the failure modes are about data movement, not code. A datasets object has its own push_to_hub(), which shards the data into Parquet and uploads it. For raw files already on disk, treat it as a folder upload. Either way, the modern client routes large transfers through hf_xet, which chunks and deduplicates so that re-uploading a dataset that changed in one shard does not re-send the whole thing. That is content-addressed storage doing for your weights and Parquet what a layer cache does for images.
from datasets import load_dataset
ds = load_dataset('csv', data_files='tickets_2026q1.csv')
ds.push_to_hub('acme-ml/support-tickets', private=True)
# Faster large transfers (hf_xet is installed by default):
# export HF_XET_HIGH_PERFORMANCE=1
# then run the push again to saturate bandwidth and coresExpected: the dataset lands as Parquet shards in a private repo. Failure mode: pushing a giant single file with no sharding, or setting the legacy HF_HUB_ENABLE_HF_TRANSFER flag that no longer does anything since hf_xet replaced hf_transfer. Use HF_XET_HIGH_PERFORMANCE=1 instead.
push_to_hub, ask whether the rows contain customer text, PII, or anything under a data agreement. A private repo limits who can read it, but it does not strip the data. Run the same review you would run before copying that data into any shared store, because the Hub is a shared store.The model card is provenance, not documentation
A model card is a README.md with a YAML metadata header. To the infra reader it is the label and provenance manifest that travels with the artifact: license, base model, training data, intended use, evaluation. Part 2 of this series covered reading a card before you pull a model. Now you are the producer, and the same metadata your reviewers will demand has to be written by you, ideally in code so it cannot be skipped in CI.
from huggingface_hub import ModelCard, ModelCardData, metadata_update
card_data = ModelCardData(
license='apache-2.0',
base_model='distilbert-base-uncased',
datasets=['acme-ml/support-tickets'],
metrics=['f1'],
library_name='transformers',
)
card = ModelCard.from_template(card_data, model_id='intent-classifier')
card.push_to_hub('acme-ml/intent-classifier')
# Update one field later without rewriting the card:
metadata_update('acme-ml/intent-classifier', {'license': 'apache-2.0'})Expected: the repo now carries machine-readable license, base model, and dataset links that your governance tooling and the Hub UI can both read. Failure mode: leaving the license blank, which makes the artifact legally ambiguous and unusable for any reviewer doing license checks. A missing license is a blocker, not a nicety.
Pair the card with the format discipline from Part 7 on safetensors: push weights as safetensors so the artifact you promote is the one your scanners already trust. This is the same control surface you run on container images in Harbor, only the artifact is weights instead of layers. The concept of versioned, governed artifacts is covered vendor-neutral in the GenAI series; here you are doing it with Hub tooling.
Disclaimer
Pushing makes an artifact and metadata real for everyone with access. Before pushing anything trained on internal or regulated data, confirm the repo is private, confirm the token in use is write-scoped to that repo, and confirm the dataset has cleared review. A public push of regulated data is hard to fully undo because clones can happen in the window it was visible.
Wiring the push into CI so it cannot be skipped
A push you run by hand from a laptop is a push that drifts. The token is whatever was lying around, the visibility is whatever you remembered, and the model card is whatever you had time for. The fix is the same one you applied to image builds: move the promotion into the pipeline and make the safe path the only path. A pipeline step authenticates with a write-scoped repo secret, creates the repo idempotently as private, uploads the artifact directory on a branch as a pull request, and fails the job outright if the model card has no license. Nothing reaches the shared store without passing those gates.
The operational payoff is auditability. When every promotion runs through one job, you get a log of who pushed what, when, and from which commit of your training code, which is exactly the trail a security or compliance review asks for. It also kills the worst failure mode, a one-off manual push of regulated data to a public repo, because the manual path simply stops being how artifacts get promoted. If you already gate image promotion behind CI checks, this is the same control re-pointed at weights, and most of your existing policy-as-code carries over with only the artifact type changed.
What I would do in production
Push into an organization, never a personal namespace, so RBAC and revocation are centralised. Create every repo with private=True and exist_ok=True from CI, so the pipeline is idempotent and there is never a public window. Give CI a fine-grained, single-repo write token and rotate it on the registry schedule. Promote through a named branch with --create-pr so a human approves before anything reaches main, then cut an immutable tag and have every serving job pin that tag or a commit hash. Write the model card in code with the license filled in, push safetensors only, and review datasets for PII before they leave the workstation.
Where this does not apply: a throwaway demo or a public benchmark reproduction does not need an org, a PR flow, or pinning. The overhead is for artifacts other people and other jobs depend on. And validate one thing first before you standardise on this: confirm your token really is scoped to the single repo by trying a write against a second repo and watching it fail. A token you think is scoped but is not gives you the illusion of control, which is worse than none.
Next up, the series moves from producing models to serving them. If you have been promoting artifacts this way, you already have clean, pinned, governed inputs for the serving layer. Try the PR-and-pin flow on one real internal model this week and see how much of your existing registry runbook transfers unchanged.
References
Upload files to the Hub
Create and manage a repository
Create and share Model Cards
huggingface_hub Command Line Interface


DrJha