Publishing a Space App
Registry metadata, the first publish, shipping updates, and how installed apps know a new version exists.
Building a Space App ends with a zip that installs locally. This page gets that zip onto the registry and into users' update checks, using the senclaw hub commands. The registry's general rules — immutable versions, visibility, yanking — are in Publishing and apply to apps unchanged.
One hostname note up front: older CLI builds still default to hub-store.bacnd.com, the registry's previous name — the same server as senclaw.bacnd.com. Every command below passes --hub https://senclaw.bacnd.com explicitly so the URLs in the output are the canonical ones.
Sign in once
Publishing needs an account with a username — the username is your scope, and without one the API answers 403 no_handle — plus a token minted under API tokens with the publish scope. Store the token on the machine:
senclaw hub loginThe CLI reads it from stdin, so it never lands on a command line or in shell history, and writes it to ~/.senclaw/hub-token (mode 600). In CI, set the SENCLAW_HUB_TOKEN environment variable instead of using the file. Check who you are with:
senclaw hub whoamiRegistry metadata: senclaw-hub.json
senclaw hub init . --version 1.0.0This scaffolds senclaw-hub.json, the hub-side metadata — deliberately separate from the runtime manifest, because the two files have two different readers and merging them is how they drift. init starts the permissions narrow, derived from the manifest:
{
"version": "1.0.0",
"permissions": {
"network": ["127.0.0.1"],
"exec": ["./my-app"]
},
"updater": "none",
"platform": "darwin-arm64"
}Before the first publish, fill in by hand:
categoryandkeywords, so the package is findable in the store.repo_urlandhomepage_url, shown on the package page.permissions, truthfully. If the app calls out to the network, declare the domains. This is the security declaration users see before installing — see Permissions — and a false declaration is worse than none.
Dry-run, then publish
# Check everything (semver, description, artifact, size, version collisions) — uploads NOTHING
senclaw hub publish . --pack --dry-run --hub https://senclaw.bacnd.com
# Looks good? Publish for real
senclaw hub publish . --pack --hub https://senclaw.bacnd.com--packrunsscripts/pack.shbefore uploading; drop it if you just packed by hand.senclaw hub status .is the quick overview: local version, artifact, integrity, and the versions already on the hub.- Publish sends
kind=app, theidfrom the runtime manifest as the name, the manifest'sdescription, andREADME.mdas the package page. The scope is forced by the server to the token owner's handle — nobody can publish into someone else's namespace.
Success prints the slug, a URL of the form https://senclaw.bacnd.com/p/<scope>/my-app, and the SHA-512 integrity the hub computed itself from the bytes it stored — the client-side number is only a cross-check.
Then verify from the outside:
senclaw hub info <scope>/my-app --hub https://senclaw.bacnd.com
senclaw hub install <scope>/my-app --dry-run --hub https://senclaw.bacnd.com # download + verify, no installWhen publish fails
| What comes back | What it means | What to do |
|---|---|---|
HTTP 409 version_exists | That name@version already exists — published versions are immutable | senclaw hub bump . patch, then publish again |
| HTTP 401 | Bad or expired token | senclaw hub login with a fresh token |
HTTP 403 insufficient_scope | Token lacks the publish scope | Mint a new token with the publish scope |
HTTP 403 no_handle | The account has no username yet | Set a username on the hub first |
| Any other HTTP 403 | Not a maintainer of that package | The name is already taken in your scope by a package you cannot write to |
| HTTP 413, or "over the 20 MB limit" | Artifact too large | Shrink the zip: strip the binary, drop stray assets |
| "no artifact ..." | Nothing has been packed | Run with --pack, or run scripts/pack.sh first |
| "missing description" | The runtime manifest has an empty description | The hub requires one — add it to senclaw-manifest.json |
One caveat: the CLI's preflight checks version collisions under the senclaw scope. If your handle is anything else, the server's 409 is the verdict that counts — bump and move on.
Shipping an update
# 1. After the change, raise the version (patch, minor or major)
senclaw hub bump . patch # 1.0.0 -> 1.0.1
# 2. Pack and publish
senclaw hub publish . --pack --hub https://senclaw.bacnd.comThe rule: every shipped change is a new version. There is no publish-over-the-top. A broken version gets yanked on the hub — the registry refuses to install yanked versions — and a fixed one published after it.
What users run
senclaw hub install <scope>/my-app --hub https://senclaw.bacnd.com # first install
senclaw hub outdated # which apps have newer versions
senclaw hub update my-app # update one app
senclaw hub update --all # update everything with a newer versionThe web UI shows an update badge as well — the daemon exposes GET /api/space/apps/updates and POST /api/space/apps/{id}/update. Every download is verified against the registry's SHA-512 and size before install; a mismatch is a refusal.
Scope note: a bare name (senclaw hub install my-app) is read as senclaw/my-app. A package published under any other handle needs the full <handle>/my-app.
The provenance stamp
When the CLI installs from the hub, it passes the slug, version, hub and integrity along, and the daemon stamps them into the installed manifest as manifest.hub. That stamp is what makes updates work: outdated knows which package, and which version, to compare against.
A zip installed by hand through install-zip carries no stamp — if its id matches a hub package, it is permanently offered the latest version. Local dev apps (install.type = "local") and ids of the form space-app-<uuid> are skipped entirely.
Current limits (as of August 2026)
- One version, one artifact through the CLI and publish API. The server can already store a second platform on an existing version, but the endpoint and CLI do not expose it yet — for multi-platform apps, publish per-platform versions from each target machine for now.
- Uploads cap at 20 MB — the Worker holds the file in memory to hash it. A presigned upload path for larger artifacts is planned; app zips do not use it yet.
- The CLI's default hub is still
hub-store.bacnd.com— the same server — so pass--hubfor canonical URLs in the output. On the installing side, the daemon readsSENCLAW_HUB_URLfor marketplace and update checks.
The whole lifecycle, as a checklist
[ ] Own repo: Cargo.toml depends on app-space-sdk (git, pinned rev); .gitignore target/release/zip
[ ] main.rs: prefer the PORT the daemon assigns, bind SENCLAW_BIND_HOST (default 127.0.0.1),
/api/status returns 200, serve web_dist next to the binary
[ ] Fixed port no other app uses, declared exactly in the manifest
[ ] AI through the bridge: llm.request (handle finish=length, no temperature),
agent.run for jobs needing tools, knowledge.* for memory, usage.report only for direct provider calls
[ ] MCP: name <id>-mcp, GET /api/mcp/sse + POST /api/mcp/message, one consistent tool prefix
[ ] (optional) widgets[]: surfaces include "chat", params schema, textFallback,
HTML page at entryUrl + a skill teaching emit_widget kind "app"
[ ] External links go through POST /api/ui/open-url (openExternal)
[ ] senclaw-manifest.json: id / description / runtime / integration / bridge / mcp / skills / personas
[ ] scripts/pack.sh -> <id>-app.zip, flat layout, 20 MB max
[ ] Local test: install-zip into the daemon, /api/mcp-servers shows the MCP, logs clean
[ ] Hub: account + username + publish token -> senclaw hub login
[ ] senclaw hub init . -> fill in permissions / category / keywords / repo_url
[ ] senclaw hub publish . --pack --dry-run --hub https://senclaw.bacnd.com -> then publish for real
[ ] New release: bump -> publish. Users: outdated -> update