Publishing skills, plugins, workflows and kits

What to upload for each kind, what its manifest block carries, and the exact web and API calls that publish it.

Publishing covers the rules that hold for every kind: a published name@version is permanent, visibility is yours to change, and a yank hides a version from range resolution without deleting it. This page is the practical half for the four kinds that ship as text — skill, plugin, workflow and kit: what goes in the file, what the manifest carries, and the calls that publish it. Apps have a platform matrix and a page of their own, Publishing a Space App.

Before you start

You needWhere
An account with a usernameLog in, then pick one
A token, to publish from a scriptAPI tokens, scope publish
An artifact under 50 MBBoth the form and the API cap there

Your username is your scope: everything lands under you/<name>. The server takes that scope from the token's owner rather than from the request, so it is not a field you can set — and an account without a username has nothing to publish under, which comes back as 403 no_handle. A token is shown once, stored hashed, and sent as Authorization: Bearer snc_pat_.... The 50 MB ceiling is where the checksum is computed: inside a Worker that has to hold the whole file in memory while hashing it.

What to upload

KindWhat the file isInstalls to
skillSKILL.md itself, or a .tgz / .zip of the skill directory~/.senclaw/managed/skills
pluginA .tgz / .zip of the plugin directory, its manifest inside~/.senclaw/managed/plugins
workflowThe workflow .js — the one exporting const meta~/senclaw/workflows
kitThe kit .json — the manifest the daemon installs~/.senclaw/kits

Those paths are the agent's defaults, overridable there by MANAGED_SKILLS_DIR, MANAGED_PLUGINS_DIR, SENCLAW_WORKFLOWS_DIR and SENCLAW_KITS_DIR — a default, not a guarantee.

Names are lowercase letters, digits and inner hyphens, up to 64 characters. Versions are semver and required. The kind is fixed at the first publish: a later version naming a different kind is refused with kind_mismatch rather than quietly changing what you/thing means.

Every manifest field, with the constraints the server actually enforces, is in the manifest reference — generated from the schema that validates your upload, so it cannot drift from what is accepted. The sections below cover only what is specific to getting each kind out the door.

Skills

A skill is SKILL.md: frontmatter, then the instructions the agent reads. Upload that file on its own when it is the whole skill; upload a tarball of the directory when the skill ships references, scripts or examples beside it.

The manifest's skill block is that frontmatter carried through verbatim — the registry stores and displays it, the agent is what interprets it. The load-bearing one is whenToUse: it is what decides whether the skill is reached for at all, so a vague line there costs you every invocation the skill was written for. The web form does not ask for these fields; send them in the extra part when publishing from a script.

Plugins

A plugin is a directory — commands, subagents, hooks, MCP servers, sometimes skills and workflows — tarred up with its manifest inside.

Declare what it ships in plugin.components. It is a declaration, not a count taken from your tarball, and it exists so the package page can say what the plugin adds before anyone downloads it. Readers look at the hooks row first, because hooks run commands on their machine.

Set the repository URL. For a plugin that field is not decoration: the plugin index at /marketplace.json lists only plugins whose repo URL resolves to a git source, and skips the rest rather than emitting an entry that fails in someone's terminal. That index is the interoperability path for agents other than SenClaw — a client following it fetches from your repository, not from here, so those bytes never pass the checksum and signature checks. Installing is where that trade-off is spelled out.

Workflows

Upload the .js file that exports const meta. The manifest's workflow.entry names it inside the package, defaults to workflow.js, must end in .js, and may not contain .. — an entry that traverses upward would write outside the workflows directory on the machine installing it.

Mirror the script's own meta.whenToUse and meta.phases into the manifest. The point is display without execution: the package page can then show what the workflow does and which phases it runs, having never run it.

One thing to know before publishing: this kind models the script format, a .js file with a meta block. The SenClaw daemon also has a workflow runner of its own, and that one loads YAML-frontmatter .md DAG definitions from its workflows directory and ignores .js. Those arrive through kits, below — not through this kind.

Publishing from the web

Publish a package, pick the kind, and fill in one form:

  • Package — name, version, description; optionally repository URL, category and keywords.
  • File — the artifact.
  • Visibility — public, unlisted, or private. Publishing private takes you straight to the access page, where allowed emails are added one at a time.
  • Documentation — upload a README.md or write it in the box. Markdown, tables included; an uploaded file wins over the box.
  • Declared permissions — upload or paste permissions.json. The keys are network, filesystem, exec and env. It costs a minute, and it is what turns a future upgrade into a check instead of a hope — see Permissions.

Publishing from a script

One multipart/form-data call. Every accepted part is listed in the HTTP API reference.

# skill — frontmatter goes in `extra`, which the form has no field for
curl -X POST https://senclaw.bacnd.com/api/v1/publish \
  -H "Authorization: Bearer $SENCLAW_TOKEN" \
  -F kind=skill \
  -F name=commit-writer \
  -F version=1.0.0 \
  -F 'description=Writes conventional commits from a staged diff' \
  -F 'extra={"skill":{"whenToUse":"When the user asks for a commit message"}}' \
  -F 'readme=<README.md' \
  -F 'permissions=<permissions.json' \
  -F file=@commit-writer.tgz
# plugin — repoUrl is what puts it in /marketplace.json
curl -X POST https://senclaw.bacnd.com/api/v1/publish \
  -H "Authorization: Bearer $SENCLAW_TOKEN" \
  -F kind=plugin \
  -F name=review-kit \
  -F version=2.1.0 \
  -F 'description=Review commands, two subagents and a pre-commit hook' \
  -F repoUrl=https://github.com/you/review-kit \
  -F 'extra={"plugin":{"components":{"commands":4,"agents":2,"hooks":true}}}' \
  -F file=@review-kit.tgz
# workflow — mirror the script's own meta block
curl -X POST https://senclaw.bacnd.com/api/v1/publish \
  -H "Authorization: Bearer $SENCLAW_TOKEN" \
  -F kind=workflow \
  -F name=triage \
  -F version=0.3.0 \
  -F 'description=Fans out over failing tests and verifies each fix' \
  -F 'extra={"workflow":{"entry":"triage.js","whenToUse":"When CI is red across several suites"}}' \
  -F file=@triage.js
# kit — installs counts are what the package page shows before download
curl -X POST https://senclaw.bacnd.com/api/v1/publish \
  -H "Authorization: Bearer $SENCLAW_TOKEN" \
  -F kind=kit \
  -F name=daily-report \
  -F version=1.1.0 \
  -F 'description=A reporter agent, its skill, and a 09:00 cron job' \
  -F 'extra={"kit":{"entry":"kit.json","manifestVersion":2,"installs":{"agents":1,"skills":1,"jobs":1}}}' \
  -F file=@kit.json

Note the < in readme=<README.md: curl reads the file into a normal text field, which is what the endpoint expects. @ would attach it as a file part instead, and the README would be silently ignored.

A 201 comes back with the slug, the version, the SHA-512 integrity the server computed from the bytes it received — never one you supply — and the package URL.

Kits

A kit is one JSON manifest that installs a whole working setup in a single call: agents, skills, workflows, hooks and scheduled jobs together. Upload that file. kit.entry names it inside the package and defaults to kit.json, under the same two rules as a workflow entry: it must end in .json, and a .. segment in it is refused.

What the kit file declares is not repeated in the manifest, with one exception. kit.installs carries counts: how many agents, skills, workflows, hooks and jobs installing it creates. That is the same trade plugin.components makes — enough for the package page to say what a kit does to a machine before anyone downloads it, without a second copy of the lists that could disagree with the file itself. The two counts readers weigh are hooks, which fire on tool use, and jobs, which run on a cron from the moment they are installed.

kit.manifestVersion is worth filling in: a daemon refuses a manifest newer than the version it understands, so publishing it lets a reader see whether their build can install the kit at all.

The install itself belongs to the daemon, which owns ordering, the never-overwrite rule and the removal ledger: POST /api/kits/preview validates and reports counts without installing anything, POST /api/kits/install installs and returns a per-item report, GET /api/kits lists what is installed, and DELETE /api/kits/:id removes exactly what that kit created.

Today that is a manual step — download the kit from its package page and post it to your own daemon:

curl -X POST http://127.0.0.1:18788/api/kits/install \
  -H 'Content-Type: application/json' -d @kit.json

After it is live

  • The package page is /p/you/<name>. A stable version moves the latest dist-tag; a prerelease moves beta instead. Neither moves backwards, so republishing an older version cannot drag a tag back with it.
  • Plugins appear in /marketplace.json within about a minute — it is cached for 60 seconds and revalidated, so a yank takes effect just as quickly.
  • Downloads are counted once per artifact served.
  • What you can still change — deprecate, yank, visibility, removal — is in Publishing.

When a publish is rejected

ResponseMeansFix
401 unauthorizedNo token, or not a valid oneMint one under API tokens
403 insufficient_scopeThe token is valid but has no publish scopeMint a new one; scopes are fixed at creation
403 no_handleThe account has no username, so there is no scope to publish underPick one, then retry
400 invalid_manifestA manifest field failed validation; the message names the field and the ruleCompare against the manifest reference
409 version_existsThat name@version is already publishedPublish a new version — the bytes behind a version never change
400 kind_mismatchThe package already exists as another kindPublish the kind it already is, or choose another name
403 not_maintainerThe package belongs to someone elseAsk its owner to add you as a maintainer
413 too_largeOver 50 MBTrim the archive; large app binaries publish through the CLI, where bytes go straight to storage
400 bad_permissionsThe permissions JSON did not parse or did not validateSee Permissions for the accepted shape