Skip to content

Skill Server

SkillServer is a self-hosted skill registry - a private NuGet feed or npm registry, but for SKILL.md files and netclaw sub-agents. Host it behind your firewall, publish proprietary skills and sub-agents, and every netclaw instance on the network syncs from it automatically.

Source code and releases: github.com/netclaw-dev/skill-server

It implements two open standards, plus a native feed for netclaw-aware clients:

Any agent that speaks the RFC can consume skills from your server, and netclaw additionally reads the native manifest for sub-agents and archive artifacts.

  • Stores versioned skills and sub-agents in content-addressable blob storage (SHA-256)
  • Serves an RFC discovery index and a native manifest that agents poll on an interval
  • Packages skills with bundled resources as deterministic archives
  • Ships a web gallery for browsing skills and sub-agents
  • Requires API key auth on writes. Reads are open, so agents fetch without credentials
  • Runs as a single container with no external dependencies, just SQLite and the filesystem

Pull the image:

Terminal window
docker pull ghcr.io/netclaw-dev/skillserver:latest

Available for linux/amd64 and linux/arm64.

name: skillserver
services:
# The server runs as a non-root user (uid 1654). Docker creates the named
# volume's mount point owned by root, so this one-shot service fixes ownership
# before the server starts - otherwise it can't create the SQLite database.
init-data:
image: ghcr.io/netclaw-dev/skillserver:latest
user: "0:0"
volumes:
- skill-data:/data
entrypoint: ["sh", "-c", "chown -R 1654:1654 /data"]
restart: "no"
skill-server:
image: ghcr.io/netclaw-dev/skillserver:latest
depends_on:
init-data:
condition: service_completed_successfully
ports:
- "8080:8080"
volumes:
- skill-data:/data
environment:
- SKILLSERVER__DATAPATH=/data
- SKILLSERVER__BASEURL=http://localhost:8080
- SKILLSERVER__APIKEY=${SKILLSERVER_APIKEY:-}
- ASPNETCORE_URLS=http://+:8080
volumes:
skill-data:

Start it:

Terminal window
export SKILLSERVER_APIKEY="sk-$(openssl rand -base64 32)"
echo "Save this key: $SKILLSERVER_APIKEY"
docker compose up -d

Verify it’s running:

Terminal window
curl http://localhost:8080/health

The bootstrap API key is hashed and stored on first startup. Save the raw value, because it can’t be recovered from the server.

All configuration is via environment variables:

VariableDefaultDescription
SKILLSERVER__DATAPATH./dataSQLite database + blob storage directory
SKILLSERVER__BASEURLhttp://localhost:8080Base URL for absolute URLs in discovery responses
SKILLSERVER__APIKEY(none)Bootstrap API key, seeded on first run if no keys exist in DB
ASPNETCORE_URLShttp://+:8080Listen address and port

All state lives in SKILLSERVER__DATAPATH. Back up that volume and you have everything.

  • Put a reverse proxy (Caddy, nginx, Traefik) in front for TLS
  • Set SKILLSERVER__BASEURL to your public URL (e.g., https://skills.internal.example.com) so discovery responses have correct absolute URLs
  • Mount /data to persistent storage. If the volume is lost, you’ll re-publish everything

Reads are open. Writes (publish, delete, key management) require a Bearer token.

Set SKILLSERVER__APIKEY before the first run. The server hashes it and stores it as the “bootstrap” key. Once any key exists in the database, this environment variable is ignored on later starts.

Terminal window
curl -X POST http://localhost:8080/api/v1/api-keys \
-H "Authorization: Bearer sk-your-bootstrap-key" \
-H "Content-Type: application/json" \
-d '{"label": "ci-deploy"}'

The response contains the raw key once. Store it in a secret manager or password vault. The skillserver api-key commands do the same thing without hand-writing curl.

Terminal window
# List (never shows raw keys)
curl http://localhost:8080/api/v1/api-keys \
-H "Authorization: Bearer sk-your-key"
# Revoke
curl -X DELETE http://localhost:8080/api/v1/api-keys/2 \
-H "Authorization: Bearer sk-your-key"

You can’t delete the last remaining key.

sk-{random} (256 bits of entropy, base64url-encoded). Stored as SHA-256 hashes, compared in constant time. Raw keys never touch disk.

The skillserver CLI handles publishing and management from your terminal or CI, and that page has the full command and flag reference. The essentials:

Terminal window
# Publish one skill, or every skill directory under a parent
skillserver publish ./my-skill
skillserver publish-all ./skills
# Publish sub-agents (version is required - it's not read from frontmatter)
skillserver publish-subagent ./release-notes-writer.md --version 1.0.0
skillserver publish-subagents ./subagents

Publishing a version that already exists is skipped, so publish-all and publish-subagents are idempotent and safe to run on every CI push. A skill with a references/, scripts/, or assets/ folder is packaged as an archive that preserves relative paths and executable bits. A bare SKILL.md publishes as a lightweight skill-md artifact.

The server exposes two discovery feeds:

FeedPathContents
RFC index/.well-known/agent-skills/index.jsonSkills only, per the Cloudflare RFC
Native manifest/manifest.jsonSkills, sub-agents, and archives, with API version negotiation

The netclaw daemon reads the native manifest, so it picks up sub-agents and archive resources the RFC feed can’t express. See Native Manifest for the manifest tree and version negotiation, and Skill Feeds for how the daemon syncs from it.

Add a skill server as a feed source through netclaw config → Skill Sources (select + Add skill server and enter the base URL), or by adding it to the SkillFeeds.Feeds array in ~/.netclaw/config/netclaw.json:

{ "SkillFeeds": { "Feeds": [ { "Name": "my-server", "Url": "http://skills.internal.example.com", "Enabled": true } ] } }

The daemon syncs on a periodic interval. Skills land in ~/.netclaw/skills/.server-feeds/ and sub-agents in ~/.netclaw/agents/.server-feeds/, both read-only. See Skill Feeds for sync intervals, authentication, and selective sync options.

Reads are open. Endpoints marked auth require an Authorization: Bearer <key> header.

MethodPathDescription
GET/healthHealth check
GET/api/v1/infoServer info and capabilities
GET/.well-known/agent-skills/index.jsonRFC-compliant skill discovery index
GET/manifest.jsonNative manifest root (version negotiation)
GET/skills/v1/index.jsonNative skill collection (paginated)
GET/skills/v1/{name}/versions/{version}.jsonNative skill version entry
GET/subagents/v1/index.jsonNative sub-agent collection (paginated)
GET/subagents/v1/{name}/versions/{version}.jsonNative sub-agent version entry
MethodPathDescription
GET/api/v1/skillsList all (?q=, ?skip=, ?take= supported)
GET/api/v1/skills/{name}All versions of a skill
GET/api/v1/skills/{name}/latestLatest version metadata
GET/api/v1/skills/{name}/{version}Specific version metadata
GET/api/v1/skills/{name}/{version}/SKILL.mdDownload SKILL.md
GET/api/v1/skills/{name}/{version}/archive.zipDownload the packaged archive
GET/api/v1/skills/{name}/{version}/resourcesList bundled resources (path, digest, mode)
GET/api/v1/skills/{name}/{version}/{*path}Download a single resource file
POST/api/v1/skills/check-updatesBatch update check
POST/api/v1/skillsauth Upload a new version (multipart/form-data)
DELETE/api/v1/skills/{name}/{version}auth Delete a version
MethodPathDescription
GET/api/v1/subagentsList all sub-agents
GET/api/v1/subagents/{name}All versions of a sub-agent
GET/api/v1/subagents/{name}/{version}Specific version metadata
GET/api/v1/subagents/{name}/{version}/agent.mdDownload the sub-agent definition
POST/api/v1/subagentsauth Upload a new version
DELETE/api/v1/subagents/{name}/{version}auth Delete a version
MethodPathDescription
GET/api/v1/blobs/sha256/{digest}Download a blob by digest
POST/api/v1/api-keysauth Create a key (returns raw key once)
GET/api/v1/api-keysauth List keys (hashed, never shows raw)
DELETE/api/v1/api-keys/{id}auth Revoke a key

The server also serves a browser UI: /, /skills, and /subagents return the gallery SPA rather than JSON.