---
title: "Publish a skill to the global catalog"
method: POST
path: "/api/v1/library/{owner}/{name}/publish"
tags: ["Library"]
---

# Publish a skill to the global catalog

`POST /api/v1/library/{owner}/{name}/publish`

## Request

- Base URL: `https://skillrepo.dev`
- URL: `https://skillrepo.dev/api/v1/library/{owner}/{name}/publish`
- Auth: HTTP bearer

Transitions a skill in the caller's account from `private` to `global` visibility, making it discoverable in the public catalog.

**Idempotent.** Calling publish on an already-global skill returns **200** with `action: "unchanged"` rather than a 409 — consistent with the CLI retry-loop semantics established by `POST /library` and `DELETE /library/{owner}/{name}`.

**Permission gate.** Admin+ members are always allowed. Members with `canPublish: true` on their per-membership settings (or `memberCanPublish: true` on the account default) are also allowed — see [docs/ENTITLEMENTS.md](../../docs/ENTITLEMENTS.md). The same `canPublish` entitlement gates `/unpublish` symmetrically.

**Publish-only preconditions** (not enforced on unpublish):
- An Author ID must be chosen — blocked while the account's   handle is still the auto-generated placeholder (GitHub-derived   handles count as chosen). Set an Author ID in   Settings → Account Profile to unblock   (`code: namespace_unset`).
- On editions with safety analysis enabled, the skill must have   a non-null `safetyGrade` (`code: analysis_pending`).
- The safety grade must not be `F` (`code: safety_grade_too_low`).

**Idempotency.** Optional `Idempotency-Key` header: retries with the same key + same body return the cached response with `Idempotent-Replayed: true`; retries with a different body return 422 `idempotency_key_reused`.

Rate-limit bucket: `cli-crud` (60/min per key).

## Path parameters

- `owner` string, required
- `name` string, required

## Headers

- `Idempotency-Key` string: Client-supplied idempotency key (max 255 chars). Retries with the same key + same request body return the cached response (with `Idempotent-Replayed: true`) instead of re-executing the handler. Retries with the same key + a different body return 422 `idempotency_key_reused`. Concurrent retries while the original is in-flight return 409 `idempotency_key_in_progress`. Cached responses live for 24 hours.

## Response `200`

Either the skill was published (`action: "published"`) or it was already global (`action: "unchanged"`). `skill` carries the full post-transition SyncSkill so the client can update its local cache. On Idempotency-Key replay the same body is returned with `Idempotent-Replayed: true`.

- LibraryPublishResponse: Response returned by `POST /api/v1/library/{owner}/{name}/publish` (#1449). `skill` carries the full SyncSkill — same shape as one entry from `GET /api/v1/library`'s `skills[]` array — so clients can update their local cache in one round-trip.
  - `action` 'published' | 'unchanged', required: Outcome of a publish call. `published` = visibility transitioned from `private` to `global`. `unchanged` = the skill was already global; no transition happened (idempotent retry).
  - `skill` SyncSkill, required: Per-skill shape shared by bulk library sync and single-skill GET. Content is byte-identical across endpoints; the optional `governance` field rides only the manifest/scoped read paths.
    - `owner` string, required
    - `ownerName` string, required
    - `name` string, required
    - `version` string, nullable, required
    - `description` string, required
    - `keywords` string[], required: Single-word keywords extracted from the skill's `triggers` (stop-words removed, lowercased) for client-side indexing.
    - `updatedAt` string, date-time, required
    - `etag` string, required: Per-skill ETag of the form `"owner/name@<timestamp>"`. Used by clients for conditional re-fetching; the bulk sync response also sets an HTTP-level `ETag` covering the whole response.
    - `contextSignals` ContextSignals, nullable, required: Context-signals object attached to a skill by the analysis pipeline. Open-shaped — clients should pass through to UI without strict validation. Null when analysis has not yet run.
    - `files` SyncFile[], required
      - `path` string, required: File path relative to the skill root.
      - `content` string, required: Inlined file contents. UTF-8 when `encoding: utf-8`; base64 of the raw bytes when `encoding: base64`.
      - `sha256` string, required: Hex-encoded SHA-256 of the raw file bytes. Empty string for the synthesized SKILL.md entry (clients use the version-level `etag` for freshness there).
      - `contentType` string, required: MIME type. Text content types are inlined as UTF-8; everything else as base64.
      - `size` integer, required: File size in bytes (pre-base64-encoding).
      - `encoding` 'utf-8' | 'base64', required: How `content` is encoded. `utf-8` for text types, `base64` for binary.
    - `filesIncomplete` boolean, required: True when one or more blob-backed files failed to inline and were dropped from `files`. When true, the route SUPPRESSES the response-level HTTP `ETag` header and sets `Cache-Control: private, no-store` so clients re-fetch on the next sync instead of caching an incomplete response. On the push (POST) response this field is almost always `false` — files were just uploaded successfully, so the inline-fetch rarely fails. A `true` value here indicates a transient blob store hiccup between upload and response build; the next GET will return the complete state.
    - `governance` SyncSkillGovernance: Per-skill delivery governance carried ONLY on the non-recording read paths — the `?manifest=1` whole-library and skillset-scoped reads (`skillrepo list`). Absent on the byte-identical whole-library sync wire (D2) and the skillset-scoped sync wire, and absent entirely when the recall feature is disabled.
      - `status` 'latest' | 'serving' | 'pinned', required: Delivery status in the shared version-status vocabulary: `latest` — the served version is the publisher's head (healthy); `serving` — a recall rolled the account back to an earlier version (the served version is below head and diverted); `pinned` — a deliberate hold behind head (not a recall).
      - `head` string, nullable, required: The publisher's current head version label, for context (e.g. "vX available"). Null when the skill has no published head.
      - `recalledVersion` string, nullable: Present only when `status` is `serving`: the version that WOULD serve absent the recall — the one recalled and rolled back FROM. The version actually served is this skill's own `version`.

## Other responses

- `400`: Request validation failure. `code: idempotency_key_too_long` when the Idempotency-Key header exceeds 255 characters.
- `401`: Missing or invalid authentication. For routes that require `bearerAccessKey` or `bearerCatalogKey`.
- `403`: Authenticated, but the key lacks the required scope or the account is suspended. Use `code` to distinguish: `insufficient_scope`, `account_suspended`, `plan_limit`.
- `404`: Skill does not exist in the caller's account, OR the caller is publishing against another account's skill. Both cases return the same 404 by design — info-hiding per the same rule as `GET /api/v1/library/{owner}/{name}`. `code` is `skill_not_found`.
- `409`: Concurrent Idempotency-Key replay while the first request is still in-flight (`code: idempotency_key_in_progress`). Retry after a short backoff.
- `422`: Publish blocked by a product-rule precondition (NOT a permission failure — that's 403). `code` distinguishes the blocker. **Product-rule codes** (handler-emitted): - `namespace_unset` — the account's publisher handle is still the auto-generated placeholder (no Author ID ever chosen); set one in Settings before publishing. - `analysis_pending` — safety analysis not complete (editions with `skillAnalysis` enabled only). - `safety_grade_too_low` — grade is F. **Middleware-emitted code:** - `idempotency_key_reused` — replay with the same `Idempotency-Key` carried a different body than the original request.
- `429`: Rate limit exceeded on the route's bucket. Includes `X-RateLimit-*` headers indicating the limit, remaining, and reset time.

---

[API](https://skmtc.dev/skillrepo/apis/skillrepo-api.md) · [All operations](https://skmtc.dev/skillrepo/apis/skillrepo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/skillrepo/apis/skillrepo-api/revisions/b62bac246918?raw)
