,

Pushing Models and Datasets to the Hugging Face Hub: Private Repos, Versioning, and Model Cards (Hugging Face Series, Part 10)

Pushing to the Hugging Face Hub is artifact promotion. Here is how to create private repos, scope tokens and org RBAC, pin consumers to a commit, write a model card for provenance, and move large files without melting your bandwidth.

Hugging Face Series · Part 10 of 17
TL;DR: Pushing to the Hub is artifact promotion, not a save button. Run 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.
Who this is for: The infrastructure or platform engineer who already runs a container registry and now has to host internal models for a data-science team. You know RBAC, scoped credentials, and artifact promotion. You want the Hub-specific commands and the operational traps, not a research tutorial. Prerequisites: a Hugging Face account, the 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.

Tested on: Python 3.11+, huggingface_hub 1.21. These are the versions this walkthrough was checked against in mid-2026. The Hugging Face stack moves quickly, so pin the version you install rather than pulling latest, and expect a major release to shift some defaults.

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.

Artifact promotion to the HubLocal filesweights + cardcreate_repoprivate=Truepush / upload= commitHub repo@ commit hashConsumerspin the hash
The same shape as image promotion: build, push to a private project, pin consumers to an immutable reference.

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-classifier

Expected: 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 controlWhat it gatesRegistry analogy
private=TrueRepo invisible outside you or the orgPrivate project, no anonymous pull
Org member rolesread / write / admin per personProject-level RBAC
Fine-grained tokenOne repo, read or write onlyScoped robot account / deploy key
Gated repoAccess behind terms and approvalPull policy plus license gate
What this means if you run the platform: A write-scoped, single-repo token is the only credential that should ever sit in a CI job. Rotate it on the same schedule as your registry robot accounts, and audit org membership the same way you audit registry project members. The day someone leaves, their personal token still works against any repo they could reach. Org roles you can revoke centrally; a personal token in a forgotten pipeline you cannot. This is the same lesson as credential scoping in Part 3, applied to write access instead of read.

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.

Which upload method?Is it already a Python object?Yes, in memorymodel.push_to_hub()No, a folder on diskhf upload / upload_folderOne single fileupload_file()
In CI that produces artifacts on disk, the folder path is almost always the right one.

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/7

That 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.

main moves, the hash does notmaincandidate-v3pin a1b2c3 here
Production loads an immutable commit. main and named branches are for promotion, not for pinning.
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 cores

Expected: 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.

Gotcha: Pushing a dataset is also publishing data. Before any 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.

Hugging Face Series · Part 10 of 17
« Previous: Part 9  |  Hugging Face Guide  |  Next: Part 11

References

Upload files to the Hub
Create and manage a repository
Create and share Model Cards
huggingface_hub Command Line Interface

About The Author


Discover more from Journal of Intelligent Infrastructure – By Dr Pranay Jha

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure - By Dr Pranay Jha

Subscribe now to keep reading and get access to the full archive.

Continue reading