# Sync to Hugging Face Spaces

A Space runs your app on Hugging Face's infrastructure, so there is no process
to inject into: the Space's **secrets** are its environment, and the Hub is the
only way to set them. A binding owns one Space's secrets.

| At a glance | |
| --- | --- |
| **What seekrit writes** | A Space's secrets, delivered to the container as environment variables |
| **Addressed by** | The Space ID, `owner/name` |
| **Connection carries** | Nothing — a token plus the Space ID is the whole address |
| **Token permission** | A Hugging Face access token with **write** on the Space's repository |
| **Takes effect** | On the restart the write triggers |
| **Value visibility** | Write-only — the Hub never shows a secret's value again, not even to you |

New to sync? Read [Third-party sync](/docs/guides/third-party-sync) first — the
decryption grant, name mapping, deletions, and failure handling are the same on
every destination.

> **Warning:** **A push restarts the Space.** The Hub applies a configuration change by restarting your app, and it has no bulk endpoint for a Space that already exists — so a run that rotates five secrets makes five requests and the Space restarts as it applies them, settling on the final set. In-flight requests to the Space are interrupted. Nothing periodic causes this. seekrit re-runs a binding when a secret it reads actually changed, so a Space restarts when you rotate something rather than on a timer — with one exception, in the troubleshooting table below: a binding holding a name that can *never* land keeps retrying, and on this destination that is visible.

## 1. Create an access token

On huggingface.co, **Settings → Access Tokens → Create new token**. The token
needs **write** access to the Space's repository. A *fine-grained* token scoped
to that one repository — or to the organization that owns it — is the
least-privilege choice; a classic `write` token reaches everything you own.

```bash
printf '%s' "$HF_TOKEN" \
  | seekrit sync connect --name acme-hf --provider huggingface-spaces
```

That is the whole connection. There is nothing to scope: a Hugging Face token
belongs to one user and carries their access everywhere, and a Space ID is
globally unique — so the token plus the destination is the whole address.

> **Note:** **There is no self-hosted option here**, and that is deliberate. `HF_ENDPOINT` in the Python client points at a Hub *mirror*, which serves repository content — not the settings API this writes to, and not somewhere a secret should be sent. seekrit always talks to `huggingface.co`.

## 2. Bind an environment

A Space has **one** set of secrets, shared by every replica, so the Space is the
whole destination. The Hub's convention is that staging and production are
separate Spaces (`acme/demo`, `acme/demo-staging`), so pointing at an
environment means naming that Space.

```bash
seekrit sync verify acme-hf --provider huggingface-spaces \
  --hf-space acme/support-demo

seekrit sync enable --connection acme-hf --provider huggingface-spaces \
  --hf-space acme/support-demo \
  --app support-demo --env production --acknowledge-decryption
```

`--hf-space` is the `owner/name` shown under the Space's title; a pasted
`huggingface.co/spaces/…` URL is accepted and reduced to the ID. `--app` is the
seekrit application the environment belongs to.

Verify reads the Space's **variables** rather than its secrets. It proves the
same two things you can get wrong from the connection dialog — the token works,
and the Space is reachable — and variables are by definition the Hub's public,
non-sensitive lane, so the check pulls nothing sensitive into seekrit.

## Secrets, never variables

A Space has two slots, and only one of them is a place for a credential:

| | Secrets | Variables |
| --- | --- | --- |
| Visible after saving | No — write-only | Yes, to anyone who can see the settings |
| Copied when the Space is duplicated | No | Yes |
| What seekrit writes | **Everything** | Nothing |

seekrit never writes a variable. There is no option to, because a seekrit secret
pushed into that slot would be published.

## How a push behaves

- **Each secret is its own request.** The Hub's endpoint is an upsert keyed by
  name, so a push touches exactly the names seekrit manages and nothing else. A
  secret you added by hand survives untouched — the same property Heroku's merge
  semantics give, for the same reason.
- **A rejected name costs only itself.** Unlike destinations that write the
  whole environment in one request, one bad name here fails one name and the
  rest land normally.
- **Removals are last.** A name this binding wrote and no longer manages is
  deleted after the writes, so a Space that has been deleted is discovered by a
  failing write rather than mistaken for a key that was already gone.
- **seekrit cannot tell a redundant write from a real one**, because the Hub
  never returns a value. It does not try to work around that by recording
  anything derived from your values in the Hub's readable `description` field —
  a hash or a fingerprint there would be an offline-guessable oracle for every
  low-entropy secret, sitting in someone else's UI. Every secret seekrit writes
  is described as `Managed by seekrit`, and nothing more.
- **A value the Hub echoes back in an error is scrubbed** before the reason is
  stored on the run row.

## Name rules

> **Note:** The Hub sets some environment variables in every Space itself — `SPACE_ID`, `SPACE_HOST`, `SPACE_AUTHOR_NAME`, `SPACE_REPO_NAME`, `SPACE_TITLE`, `SPACE_CREATOR_USER_ID`, `ACCELERATOR`, `CPU_CORES`, and `MEMORY`. seekrit refuses those names rather than sending them, and reports the refusal against that name alone. Watch for it if you use a `--prefix`. The four OAuth variables the Hub injects when a Space enables **Hugging Face** OAuth — `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, `OAUTH_SCOPES`, `OPENID_PROVIDER_URL` — are *not* refused, because a Space that talks to some other OAuth provider has a real reason to set them. If your Space uses Hugging Face OAuth, do not sync those four names to it. Unlike some destinations, a secret with an **empty value** is fine here.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| The connection fails to verify, "rejected the access token" | The token was revoked, or expired | Create a new one under Settings → Access Tokens and re-create the connection |
| `403` on every name | A read token, or a fine-grained token that does not cover this repository | Give the token **write** on the Space's repo (or its organization) |
| `404` on the Space | The browser URL or a bare name in the ID slot, a model/dataset repo rather than a Space, or a private Space this token cannot see | Use `owner/name`, and check it is a Space |
| One name failed, rest landed | The mapped name is one the Hub sets itself | Rename it, or fix the prefix |
| The Space restarts several times during one sync | Expected — the Hub applies each secret on its own, and restarts each time | Nothing to fix; it settles on the final set |
| The Space restarts on a schedule | A value really is changing every run — an interpolated reference, or a rotation schedule | Check the run ledger for which name |
| The Space restarts every minute, forever | One name in this environment can never land — a built-in above, a name the Hub rejects, or an oversized value. A run that pushes some names and fails others never counts as a clean success, so seekrit keeps retrying it | `exclude` that name from the binding, or rename it. The run ledger names it |

## Alternatives worth knowing

A Space is a runtime you do not control, which is why sync is the answer here.
Two neighbouring Hugging Face surfaces are not:

- **Jobs** take their secrets at submit time (`hf jobs run`), so there is nothing
  persistent to sync into — run the submitting command under
  [`seekrit run`](/docs/guides/run) and no copy exists at the platform at all.
- **Notebooks on the Hub, Colab, or anywhere else** should resolve at the top of
  the notebook instead. See [Jupyter notebooks](/docs/guides/notebooks).

## See also

- [Third-party sync](/docs/guides/third-party-sync) — the shared model, naming and filtering, deletions
- [Jupyter notebooks](/docs/guides/notebooks) — resolving in a kernel instead of copying to a platform
- [CLI reference](/docs/reference/cli#third-party-sync) — every `seekrit sync` flag
