Publishing first-party skills¶
Most marketplaces the gateway governs live somewhere else and are fetched. A hosted marketplace is the other kind: your organisation's own skills, pushed straight to the gateway, with no forge repository standing in the middle purely so there is something to pull from.
What does not change is everything after the push. A pushed commit is quarantined, manifest-checked, vetted and held exactly like fetched content, and is served only once somebody approves it. Content your organisation wrote is exactly as capable of carrying a planted credential or a prompt injection as content it did not.
Step 1 — register the marketplace¶
A hosted marketplace takes no clone URL, because there is nothing to clone from:
$ curl -X POST localhost:8080/api/marketplaces \
-H 'Content-Type: application/json' \
-d '{"name": "platform-skills", "origin": "hosted"}'
{"id": 7, "name": "platform-skills", "url": null,
"origin": "hosted", "pushPolicy": "append-only",
"publishPath": "/publish/platform-skills"}
Supplying a url for a hosted marketplace is refused rather than ignored — the
two are mutually exclusive, and a silently unused field is how a marketplace
ends up with an upstream nobody meant to give it. The origin repository is
created here, so you can push the moment this returns.
The origin is immutable
A marketplace is hosted or upstream at registration and stays that way, for the same reason an upstream URL is immutable: changing it would swap the supply chain under snapshots that were already approved.
Step 2 — mint a push-scoped token¶
Publication authority is not fetch authority. It is a separate scope, and it has no "all marketplaces" form:
$ curl -X POST localhost:8080/api/tokens \
-H 'Content-Type: application/json' \
-d '{"name": "platform-skills-ci", "pushScopes": ["platform-skills"]}'
| Fetch scopes | Push scopes | |
|---|---|---|
| Omitted means | every marketplace | none |
| Wildcard exists | yes, by omission | no |
| Names | any marketplace, or the catalog | hosted marketplaces only |
That asymmetry is deliberate. Every token that existed before publication did can publish nothing, and no token can be granted publication to everything by forgetting a field.
The cleartext is returned exactly once, as with any access token.
Step 3 — push¶
$ git remote add gateway https://token:$TOKEN@gateway.example.com/publish/platform-skills
$ git push gateway main
The repository you push must look like any other skill marketplace: a
.claude-plugin/marketplace.json whose plugin sources are relative paths inside
the repository. See Registering a marketplace
for what the manifest is checked against — a hosted marketplace faces the same
check, and a manifest declaring a non-local source lands rejected just as it
would from an upstream.
You can git clone the same URL to bootstrap a new machine or a CI checkout;
the same push scope authorizes it. What you are cloning is your own source of
record, not the served marketplace.
What a push may do¶
| Rule | Why |
|---|---|
Only refs/heads/main |
One marketplace, one history — the same single-lineage guarantee the gateway enforces on an upstream's default branch. A second branch or a tag is refused. |
| No ref deletions | Snapshots were taken from that lineage. |
| Fast-forward only, by default | A rewrite changes what a reviewer approved from. |
A rejected push says why:
! [remote rejected] main -> main (only refs/heads/main may be published; a marketplace has one lineage)
If you really do need to rewrite¶
Register the marketplace with "pushPolicy": "allow-rewrite". Then a
git push --force succeeds — and writes a marketplace-lineage-rewritten entry
to the audit ledger naming both the old
and the new tip.
Be clear about what you are trading. Approved snapshots keep their content
regardless: they are pinned by SHA, and a rewrite cannot alter bytes that are
already pinned. What a rewrite destroys is the lineage — the answer to "what
history was this approved out of". Under allow-rewrite that answer lives only
on the ledger.
Step 4 — approve it¶
The push ingests itself. Within moments there is a held snapshot at the
commit you pushed, with a vetting chain run against it:
$ curl -s localhost:8080/api/marketplaces | jq '.[] | select(.name=="platform-skills") | .snapshots'
From here it is the ordinary flow — review the findings, waive what you accept, approve. See Approving snapshots. Until then the facade does not serve the marketplace at all, exactly as for held upstream content.
There is no fast path
There is deliberately no auto-approval for trusted internal publishers, not even for content the organisation wrote itself. Removing the human from the gate is a product decision that has been made once already, in the negative — see ADR 0006 — and it is not something a hosting feature gets to decide as a side effect.
Declaring one in the estate¶
A hosted marketplace is declarable like any other (Declarative estate):
skills-gateway:
estate:
marketplaces:
- name: platform-skills
origin: hosted
push-policy: append-only
No url, and no sync-mode other than on-demand: a hosted marketplace has no
upstream to poll or be notified about, and its ingestion trigger is the push
itself. Asking for a different sync mode is refused.
Push scopes on tokens stay API-only, for the same reason tokens themselves do: they are user-owned credentials whose secret is shown exactly once.
What this is not¶
- Not a general git host. One branch, no tags, no pull requests, no issues. It is an ingestion point that happens to speak git.
- Not a way around review. Same quarantine, same connectors, same approval.
- Not a mirror target. Pushing content the gateway could have fetched works, but you lose the upstream's provenance for no gain. Register it as an upstream marketplace instead.