skilld

Sell access to your Claude skills — never the files.

CI Release License: Apache-2.0 Python 3.11+

Your skill runs on your machine. Customers get an API key you can revoke the moment payments stop.

Get started in 60 seconds Deploy to a $5 VM in ~20 minutes

📚 Table of contents 📚

💡 Why skilld exists 💡

You've built a Claude Skill that saves you serious time — or makes you real money. Someone finds out and offers to pay you monthly to use it.

Here's the trap: a skill is just a folder of files. Today, "letting someone use it" means sending them the files — and a sent skill is theirs forever. The prompts, the bundled know-how, all of it. The day they stop paying, you have no recourse. Your intellectual property walked out the door with the first payment.

skilld removes the trap. Your skill stays on a machine you control. Your customer gets a URL and an API key instead of files. They call the API, the skill runs on your box, and they receive only the result. If the payments stop, you rotate the key — and they're holding nothing.

                     WITHOUT skilld                    WITH skilld
  you send      →    the .skill files            →    a URL + API key
  they own      →    your IP, forever            →    nothing but results
  they stop paying → they keep using it          →    you revoke the key

What that takes technically, skilld handles:

  • 🔒 The skill never leaves your box — clients see run results, never SKILL.md, prompts, or bundled files
  • 🔑 Access you control — bearer-token auth on every request; revoke or rotate the moment payments stop
  • ⚙️ Two execution backends — a fast one-shot Messages API call for prompt-only skills, or a full Agent SDK run (bundled files, tools, multi-step reasoning) for real Agent Skills
  • Sync or async — your customer gets the result in one request with ?wait=, or submits and polls
  • 🧾 Append-only event log — every run's tokens and cost are recorded, so you always know what serving a customer costs you; crashes recover, telemetry is never lost
  • 📦 Ephemeral sandboxes — each agent run executes in a throwaway workspace, created and deleted per run
  • 🚀 One-command deploy — Docker Compose with automatic HTTPS, runs on any $5 VM (or your own PC to start)

🧠 How it works 🧠

flowchart LR
    C[Client] -->|HTTPS + bearer token| P[Caddy<br/>auto-TLS, deploy only]
    P --> A[API server]
    A -->|append run.created| L[(SQLite<br/>event log)]
    W[Worker] -->|claim + lease| L
    W --> M[Messages backend<br/>one-shot API call]
    W --> G[Agent backend<br/>Claude Agent SDK in an<br/>ephemeral workspace]
    M --> AN[Anthropic API]
    G --> AN

The API server never executes skill code. Submitting a run appends a run.created event to an append-only event log; a worker claims runs from the log (with a lease, so a crashed worker's runs get picked up again), executes them through a backend, and appends the outcome. Run state is always derived from the event log — that's what makes crash recovery work today and lets workers move out of process later without changing the API.

🚀 Quickstart 🚀

Requirements: Python 3.11+ and an Anthropic API key. Windows, macOS, and Linux — no separate Claude Code install needed (the agent backend's SDK bundles it).

Run from source

git clone https://github.com/rokbenko/skilld && cd skilld
python -m venv .venv
.venv\Scripts\activate        # Windows — use `source .venv/bin/activate` elsewhere
pip install -e .

copy .env.example .env        # `cp` elsewhere
# edit .env: set API_KEY (any secret you choose) and ANTHROPIC_API_KEY

skilld                        # or: python -m skilld

Run with Docker

# after cloning and creating .env as above:
docker compose -f docker-compose.yml -f docker-compose.local.yml up

Either way the server is on http://127.0.0.1:8000, loading every skill folder under ./skills. Call it:

curl -X POST "http://127.0.0.1:8000/v1/runs?wait=60" \
  -H "Authorization: Bearer change-me" \
  -H "Content-Type: application/json" \
  -d '{"skill": "meeting-notes", "input": "Discussed Q3 launch. Ana owns pricing page, due Friday."}'
{
  "run_id": "run_...",
  "skill": "meeting-notes",
  "state": "succeeded",
  "result": "## Summary\n...",
  "usage": {"input_tokens": 210, "output_tokens": 96, "cost_usd": 0.0035}
}

Two example skills ship in ./skills: meeting-notes (messages backend) and changelog-writer (agent backend — it reads a bundled style guide during the run, which a plain prompt can't do).

🔌 API 🔌

All /v1 endpoints require Authorization: Bearer <API_KEY>.

Method Path Purpose
POST /v1/runs[?wait=N] Submit a run; optionally wait for the result
GET /v1/runs/{run_id} Poll a run's state and result
GET /v1/skills List loaded skills
GET /healthz Liveness probe (unauthenticated)

POST /v1/runs[?wait=N]

{"skill": "meeting-notes", "input": "...", "max_tokens": 1024}
  • skill (required) — a loaded skill's name.
  • input (required) — string or JSON object; handed to the skill as the request.
  • max_tokens (optional) — per-run output cap; can only lower the skill's own cap (messages backend).
  • wait (optional query param, seconds) — block until the run finishes and get the full result with status 200. Capped server-side by SKILLD_MAX_WAIT_SECONDS.

Without wait (or wait=0) you get 202 immediately:

{"run_id": "run_9f2..."}

With wait, a finished run returns 200 with the full payload (same shape as GET /v1/runs/{id}). If the run is not done before the deadline, you get 202 and fall back to polling:

{"run_id": "run_9f2...", "state": "running"}

A failed run is still a successful retrieval (200 when waited, with the error inline):

{
  "run_id": "run_9f2...",
  "skill": "meeting-notes",
  "state": "failed",
  "usage": {"input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0},
  "error": {"message": "run exceeded timeout of 600s", "type": "timeout"}
}

GET /v1/runs/{run_id}

Always includes run_id, skill, state (queued | running | succeeded | failed | canceled), timestamps, and aggregated usage. Terminal runs add result (succeeded) or error: {message, type} (failed).

GET /v1/skills

[{"name": "meeting-notes", "description": "Summarize raw meeting notes..."}]

GET /healthz

Unauthenticated liveness probe: {"status": "ok", "version": "0.1.0"}. A 200 means the store is open, skills are loaded, and the worker is running — used by the Docker healthcheck and uptime monitors.

🧩 Writing skills 🧩

A skill is a folder under SKILLD_SKILLS_DIR containing a SKILL.md — the standard Agent Skills format. A minimal complete example:

---
name: tone-polisher
description: Rewrite text in a friendly, concise support-team voice.
backend: messages
max_tokens: 1024
---

You rewrite the user's text in a friendly, concise tone suitable for
customer support replies. Preserve all factual content. Output only the
rewritten text.

skilld reads a few extra frontmatter keys beyond the standard ones:

Key Backend Meaning
name, description both Required. Runs are addressed by name.
backend messages (default) or agent.
model both Override the default model.
max_tokens messages Output-token cap for the run.
timeout_seconds both Per-run wall-clock timeout.
allowed_tools agent Tools the agent may use without prompting. Default: [Skill, Read, Glob, Grep] (read-only). Add Bash, Write, WebFetch, ... only if the skill needs them.
max_turns agent Cap on agent-loop turns (the agent backend's budget knob).

Unknown keys are ignored, so existing Agent Skills load as-is. Note: the standard allowed-tools frontmatter key has no effect when running through the Agent SDK — use skilld's allowed_tools.

Choosing a backend

  • messages — the SKILL.md body becomes the system prompt for a single Anthropic Messages API call. Fast, cheap, fully sandboxed by construction. Right for prompt-only skills.
  • agent — a real Agent Skill run via the Claude Agent SDK. The skill folder (including bundled scripts and reference files) is copied into a fresh per-run workspace; the agent runs with that workspace as its working directory, restricted to allowed_tools, and the workspace is deleted when the run ends. Right for skills that read bundled files, use tools, or need multi-step reasoning.

🔧 Configuration 🔧

Set via environment or a .env file in the working directory (see .env.example).

Variable Default Meaning
API_KEY — (required) Bearer token protecting this server.
ANTHROPIC_API_KEY — (required) Used by both backends.
SKILLD_HOST / SKILLD_PORT 127.0.0.1 / 8000 Server bind address.
SKILLD_SKILLS_DIR ./skills Directory scanned for skill folders.
SKILLD_DB_PATH ./skilld.db SQLite database path.
SKILLD_DEFAULT_MODEL claude-opus-4-8 Model when a skill doesn't specify one.
SKILLD_DEFAULT_MAX_TOKENS 4096 Messages-backend output cap.
SKILLD_DEFAULT_TIMEOUT_SECONDS 600 Per-run timeout.
SKILLD_WORKSPACES_DIR <temp>/skilld-workspaces Where per-run agent workspaces live.
SKILLD_DEFAULT_MAX_TURNS 16 Agent-backend turn cap.
SKILLD_KEEP_FAILED_WORKSPACES 0 Keep a failed run's workspace for debugging.
SKILLD_MAX_WAIT_SECONDS 120 Server-side cap on ?wait=N.
SKILLD_DOMAIN Docker deploy only: domain Caddy serves with auto-TLS.

🌍 Deploying 🌍

skilld ships as a generic public image — ghcr.io/rokbenko/skilld — with a compose file that puts it behind Caddy for automatic HTTPS. Your private skills, keys, and database are mounted at runtime and never baked into the image. The happy path, on any ~$5 VM:

# 1. provision an Ubuntu VM, point a DNS A record at it, install Docker
# 2. lay out /opt/skilld: docker-compose.yml, Caddyfile, .env, and your skills/
# 3.
docker compose up -d

Full step-by-step guide — provisioning, DNS, backups (the event log is SQLite in WAL mode; there's a safe online method), updates, and a security checklist: docs/deploy.md.

Design choices that make this portable and safe:

  • No cloud-hosted workspace. Agent runs execute in ephemeral directories inside the container, created and deleted per run — no external process can modify, delete, or observe skill files mid-run.
  • State is one SQLite file in a Docker volume. When you outgrow one process, the event-log/claim design lets workers move out of process and the store move to Postgres without changing the API.
  • Skills never leave the box. Clients only ever see run results.

🧪 Development 🧪

pip install -e ".[dev]"
ruff check .
pytest

🧭 Roadmap 🧭

  • Event-sourced run store with crash recovery
  • Messages backend (prompt-only skills)
  • Agent backend (real Agent Skills via the Claude Agent SDK)
  • Synchronous wait (?wait=N)
  • Container packaging + VM deployment with auto-HTTPS
  • SSE streaming of run events (the event log already records them)
  • Webhooks on run completion (webhook_url is accepted today, not yet called)
  • Human-in-the-loop: POST /v1/runs/{id}/input for skills that need to ask a question
  • POST /v1/runs/{id}/cancel
  • Idempotency keys and input dedup (store support exists, API wiring pending)
  • Cost budgets (max_cost_usd enforcement)
  • Per-client API keys and usage metering
  • Multi-worker execution and a Postgres store

🤝 Contributing 🤝

Issues and PRs welcome — see CONTRIBUTING.md for dev setup and expectations.

📄 License 📄

Apache-2.0 — see LICENSE.