skilld
Sell access to your Claude skills — never the files.
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
- How it works
- Quickstart
- API
- Writing skills
- Configuration
- Deploying
- Development
- Roadmap
- Contributing
- License
💡 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 status200. Capped server-side bySKILLD_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 toallowed_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_urlis accepted today, not yet called) - Human-in-the-loop:
POST /v1/runs/{id}/inputfor 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_usdenforcement) - 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.
No comments yet
Be the first to share your take.