API
Publishing
Publish a skill, harness or MCP server with reuseforge.json and rf publish or the API: manifest reference, versions, pricing, licensing, verification, install.
Publishing is two steps: create the item (its name, kind, price and license) and publish a version (immutable files plus a manifest). rf publish does both from a project directory; the API has one operation for each.
The five-minute path
cd my-skill
rf init --kind skill # scaffolds reuseforge.json (detects SKILL.md, package.json, pyproject.toml, .mcp.json)
rf publish --dry-run # validates, lists every file, shows the content hash and the idempotency key; sends nothing
rf publish # creates the item if missing, uploads version 0.1.0
rf publish --verify --wait # later releases: also request the platform's checks and wait for the resultNeeded: a token with items:write (to create the item) and items:publish (to publish versions); see authentication. rf publish scans for secrets and refuses to upload files that look like credentials (private key headers, rf_ tokens, .env files) unless you pass --allow-secrets, which you should almost never do. It never changes the price of an existing item and uses a deterministic Idempotency-Key, so repeating an interrupted publish cannot create a second version.
Item kinds
| Kind | What it is | Installs as (default) |
|---|---|---|
skill | An agent skill: a folder with SKILL.md and supporting files. | .claude/skills/<slug>/ for Claude Code |
prompt | A prompt or slash command. | .claude/commands/<slug>.md for Claude Code |
harness | An agent harness or workflow with its configuration. | ./reuseforge/<owner>/<slug>/, plus the commands hint from the manifest |
mcp_server | An MCP server you ship or point to. | A merged entry in .mcp.json (or the client's equivalent) |
repo, template | A project or starter. | Extracted into ./<slug>/ |
model, dataset | Metadata and small files; the heavy weights or data stay at the source. | ./reuseforge/<owner>/<slug>/ plus a display-only fetch command |
The manifest: reuseforge.json
A project describes what it publishes in reuseforge.json at its root. Only name, version and kind are required. Unknown keys are rejected. The schema is /docs/api/schemas/reuseforge.schema.json (JSON Schema 2020-12); point your editor at it with "$schema".
{
"$schema": "https://reuseforge.com/docs/api/schemas/reuseforge.schema.json",
"name": "acme/invoice-extractor",
"version": "1.4.0",
"kind": "skill",
"title": "Invoice extractor",
"summary": "Extracts vendor, totals and line items from PDF invoices into JSON.",
"license": "MIT",
"repository": "https://github.com/acme/invoice-extractor",
"capabilities": ["document-parsing"],
"tags": ["pdf", "invoices"],
"pricing": { "model": "free" },
"ignore": ["tests/fixtures/**"],
"preview": ["README.md", "SKILL.md"],
"dependencies": { "acme/pdf-text-layer": "^2.1.0" },
"changelog": "Handles multi-page invoices."
}An MCP server item adds an mcp section that tells installers how to start it:
{
"name": "acme/ledger-mcp",
"version": "0.3.0",
"kind": "mcp_server",
"license": "Apache-2.0",
"mcp": {
"transport": "stdio",
"command": "node",
"args": ["server.js"],
"env": { "LEDGER_API_KEY": "${LEDGER_API_KEY}" }
}
}env and headers values are ${VAR} placeholders only: an installer never writes a secret into your configuration. The full key reference follows, generated from the schema.
| Key | Type | Required | Description |
|---|---|---|---|
$schema | string | no | Editor hint. Ignored by the platform. |
name | string | yes | owner/slug, or just slug to publish under the logged-in account. Same grammar as the ItemRef schema in the OpenAPI document. (pattern ^(?:[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?/)?[a-z0-9][a-z0-9._-]{0,63}$; e.g. "acme/invoice-extractor") |
version | string | yes | Semantic version 2.0.0. A - suffix makes it a prerelease, which latest skips. Versions are immutable once published. (pattern ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$; e.g. "1.4.0") |
kind | string: repo, skill, harness, mcp_server, model, dataset, prompt, template | yes | Same values as the ItemKind schema in the OpenAPI document. |
title | string | no | (maxLength 120) |
summary | string | no | One line shown in search results. (maxLength 280) |
readme | string | no | Path of the README file inside the project (default README.md). Its text becomes the item's readme. (maxLength 512; default README.md) |
license | string | no | SPDX license expression, for example MIT or Apache-2.0 OR MIT. Use LicenseRef-Proprietary for paid closed items. (maxLength 64; e.g. "MIT") |
homepage | string | no | (maxLength 300) |
repository | string | no | Source repository URL, stored as the item's source. (maxLength 300) |
pricing | object | no | Informational for rf publish: the price lives on the item, not on the version. The CLI never changes a price implicitly; it warns when this differs from the item and rf items update --price is the explicit path. |
capabilities | array of string | no | Capability ids from the platform taxonomy, for example pdf.extract. (maxItems 12) |
tags | array of string | no | (maxItems 20) |
files | array of string | no | Include globs relative to the manifest directory (*, **, ?). Default: everything not ignored. (maxItems 100) |
ignore | array of string | no | Exclude globs, applied after files, on top of the built-in ignore list (.git, node_modules, .venv, __pycache__, .DS_Store, reuseforge.lock, .env*). |
preview | array of string | no | Globs of files readable without purchase (README, SKILL.md). |
executable | array of string | no | Globs (*, **, ?) of published files that get mode 0755 in the ZIP archive (get_version_archive); every other file is 0644. Setuid and setgid are never set. (maxItems 100) |
dependencies | object | no | Other ReuseForge items this one needs, as a map of owner/slug to semver range. (e.g. {"acme/pdf-text-layer": "^2.1.0"}) |
install | object | no | Per-target overrides of the default install recipe (see DESIGN.md section 13). Keys are install targets. |
mcp | object | no | |
test | object | no | What request_verification runs in the sandbox. The result becomes the item's attested tests_pass_rate. |
requires | object | no | What the consumer's environment must provide. Informational, shown before install. |
targets | array of generic|claude-code|cursor|codex|mcp-json | no | Install targets the author has tested. Default: all that apply to the kind. |
changelog | string | no | Release notes of this version. Alternatively a path to a CHANGELOG file via changelog_file. (maxLength 20000) |
changelog_file | string | no | Path of a markdown file whose text is used as the changelog when changelog is absent. (maxLength 512) |
pricing
| Key | Type | Required | Description |
|---|---|---|---|
model | string: free, fixed, pwyw | no | (default free) |
price_cents | integer | no | Integer US cents. Required when model is fixed. (minimum 0; maximum 100000) |
test
| Key | Type | Required | Description |
|---|---|---|---|
command | string | yes | (maxLength 500; e.g. "pytest -q") |
timeout | integer | no | Seconds. (minimum 1; maximum 900; default 120) |
runtime | string: python3.11, python3.12, node22, bash | no | Sandbox image. |
requires
| Key | Type | Required | Description |
|---|---|---|---|
python | string | no | (maxLength 64) |
node | string | no | (maxLength 64) |
env | array of string | no | Names (never values) of environment variables the item reads. |
tools | array of string | no | Executables expected on PATH, for example git. |
mcp
How to start or reach the MCP server this item ships (kind mcp_server). env and headers values must be ${VAR} placeholders, never secrets.
| Key | Type | Required | Description |
|---|---|---|---|
transport | string: stdio, http | yes | |
command | string | no | (maxLength 256) |
args | array of string | no | (maxItems 50) |
env | object | no | |
url | string | no | (maxLength 300) |
headers | object | no |
install.<target>
Keys of install are install targets (generic, claude-code, cursor, codex, mcp-json); each value may contain:
| Key | Type | Required | Description |
|---|---|---|---|
dest | string | no | Destination directory relative to the project root (or ~/...). Replaces the default for the kind. (maxLength 256) |
files | array of object | no | (maxItems 100) |
config_patches | array of object | no | (maxItems 10) |
commands | array of object | no | Display-only suggestions. Clients never run them without an explicit flag. (maxItems 10) |
The schema's text for test says the result becomes the item's attested pass rate. Today the hosted runner static-v1 does not run test.command; see verification. The key is recorded for runners that will.
Versions
- Semantic versions.
MAJOR.MINOR.PATCH, optionally with a prerelease suffix (1.5.0-rc.1).latestprefers stable releases and uses the highest prerelease only when an item has no stable version; ranges skip prereleases unless asked for. - Immutable. A version can never be overwritten; republishing the same number answers
409 already_exists. Fix a mistake by publishing a higher version and yanking the bad one. - Yanking.
update_versionwithyanked: trueand ayanked_reasonhides a version fromlatestand new resolution and warns installers, but never deletes content, so existing lockfiles still resolve. Onlychangelog,yankedandyanked_reasoncan change after publishing. - Limits. At most 100 files, 512 KiB per file and 2 MiB per version (the live numbers are in
GET /api/v1/). Paths are relative and/-separated, without.., a leading/, a backslash, NUL, case-only duplicates, and normalised to NFC. Binary files travel as base64. - What the CLI includes. Everything not ignored: the manifest's
files(include globs) andignore, plus a built-in ignore list (.git,node_modules,.venv,__pycache__,.DS_Store,reuseforge.lock,.env*). - Events. Publishing fires
item.version.published(anditem.publishedfor the first public version); see webhooks.
The content hash
content_hash is sha256: plus the hex SHA-256 of a text with one line per file, <sha256 hex of the file bytes> <path>\n (two spaces, exactly what sha256sum prints), sorted by path in UTF-8 byte order. Send it with publish_version and the server recomputes it, answering 422 integrity_mismatch if it differs. Installers verify it before writing a single file.
import hashlib
def content_hash(files): # files: {"SKILL.md": b"...", "src/run.py": b"..."}
lines = [f"{hashlib.sha256(data).hexdigest()} {path}\n" for path, data in sorted(files.items(), key=lambda kv: kv[0].encode("utf-8"))]
return "sha256:" + hashlib.sha256("".join(lines).encode("utf-8")).hexdigest()Both SDKs ship content_hash(files) / contentHash(files) and verify_bundle. Shared test vectors are in the repository under api/conformance/content-hash-vectors.json.
Publish with the API
curl
# 1. create the item (metadata only)
curl -X POST https://reuseforge.com/api/v1/items -H "Authorization: Bearer $REUSEFORGE_TOKEN" -H "Content-Type: application/json" \
-d '{"slug": "invoice-extractor", "kind": "skill", "title": "Invoice extractor", "summary": "Extracts totals from PDF invoices.", "license": "MIT"}'
# 2. publish a version; files carry their text (or base64 for binary files)
curl -X POST https://reuseforge.com/api/v1/items/acme/invoice-extractor/versions -H "Authorization: Bearer $REUSEFORGE_TOKEN" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"version": "1.4.0", "files": [{"path": "SKILL.md", "content": "# Invoice extractor\n", "encoding": "utf8"}], "changelog": "First release."}'Python
import reuseforge
client = reuseforge.Client()
client.create_item(slug="invoice-extractor", kind="skill", title="Invoice extractor", summary="Extracts totals from PDF invoices.", license="MIT")
files = [{"path": "SKILL.md", "content": "# Invoice extractor\n", "encoding": "utf8"}]
client.publish_version("acme/invoice-extractor", version="1.4.0", files=files, changelog="First release.")JavaScript
import { ReuseForge } from '@reuseforge/sdk';
const client = new ReuseForge();
await client.createItem({ slug: 'invoice-extractor', kind: 'skill', title: 'Invoice extractor', summary: 'Extracts totals from PDF invoices.', license: 'MIT' });
await client.publishVersion('acme/invoice-extractor', { version: '1.4.0', files: [{ path: 'SKILL.md', content: '# Invoice extractor\n', encoding: 'utf8' }], changelog: 'First release.' });CLI
rf publish # does both from the project directoryIn the API a version's dependencies is a list of {item, range} objects, while the manifest writes the same thing as a map of owner/slug to range. The exact body fields (files[].encoding is utf8 or base64, manifest, content_hash, changelog) are in the reference for publish_version and create_item. New items are draft until their first version is published, unless the item only points at an external source.
Pricing
pricing_model | Meaning |
|---|---|
free | No charge. Installs need no order. |
fixed | A price in integer cents (price_cents, up to 100000 in the manifest). Buyers pay into escrow. |
pwyw | Pay what you want. |
- The price belongs to the item, not to a version. The manifest's
pricingis informational:rf publishwarns when it differs from the item and never changes it. Change the price explicitly withrf items update --price 12(orupdate_item). - Paid items need a verified email and a payout address. Payout addresses can only be set in the web app, never through the API, so a stolen token cannot redirect your earnings.
- Escrow and fees. The buyer's money is held until they accept (or the acceptance window ends). The platform fee (
fee_bps, default 100 basis points, 1 percent) is deducted from your proceeds at release:seller_amount_cents = price_cents - fee_cents. The buyer pays exactly the price. - Previews.
previewglobs name files readable without buying (usually the README and theSKILL.mdfront matter). Everything else answers402 payment_requireduntil the buyer's order is delivered. - Entitlement. Access to a paid item starts when the order is delivered, before the money is released, and covers every version of the item.
The orders side is not enabled on reuseforge.com yet (see the availability table), so today paid listings are for the self-hosted node and for planning.
Licensing
license is an SPDX expression (MIT, Apache-2.0 OR MIT). Use LicenseRef-Proprietary for paid closed items. Buyers and the planner filter on it (the planner's constraints.allowed_licenses), and installers record it in reuseforge.lock. The verification run checks that a license is present. Say what the buyer may do in your README; the platform does not provide legal advice.
Verification
request_verification asks the platform to check a stored version. The hosted runner is static-v1: it runs inside the API, completes before the response is sent, and re-checks file hashes, limits, the manifest, the license, secrets, binaries, the SKILL.md front matter (for skills), the MCP configuration (for MCP servers) and the README. It does not execute your tests: tests_total and tests_passed count static checks, tests_pass_rate stays null, and planner signals are never changed by it. A sandbox runner that executes test.command and attests a real pass rate (the runner values platform, community and seller) does not exist yet. Nothing in the API presents static checks as test results.
rf verify request acme/invoice-extractor 1.4.0 # or publish with: rf publish --verify --wait
rf verify get vrf_...Subscribe to verification.completed (webhooks) instead of polling get_verification.
Install descriptors
get_install_descriptor(item, version, target) returns a recipe: which files go where, the config patches to merge, and display-only commands. The client fetches content with get_version_bundle and applies the recipe. The descriptor never contains secrets and never asks a client to run code.
Targets are generic, claude-code, cursor, codex and mcp-json. If you omit target the server chooses generic; the CLI detects .claude/, .cursor/ and .codex/ or AGENTS.md and otherwise asks.
| Kind | generic | claude-code | cursor | codex |
|---|---|---|---|---|
skill | ./reuseforge/<owner>/<slug>/ | .claude/skills/<slug>/ | .cursor/rules/<slug>.mdc generated from SKILL.md, other files in .cursor/reuseforge/<slug>/ | .codex/skills/<slug>/ plus a block in AGENTS.md |
prompt | ./reuseforge/<owner>/<slug>/ | .claude/commands/<slug>.md | .cursor/rules/<slug>.mdc | .codex/prompts/<slug>.md |
harness | ./reuseforge/<owner>/<slug>/ | same as generic | same | same |
mcp_server | a patch into .mcp.json | .mcp.json | .cursor/mcp.json | .codex/config.toml |
repo, template | ./<slug>/ | same | same | same |
model, dataset | ./reuseforge/<owner>/<slug>/ and a fetch command | same | same | same |
A manifest install.<target> entry overrides any cell. The claude-code layouts and .mcp.json follow documented conventions. The Cursor .mdc and Codex paths follow those tools' public conventions but were not verified against the running tools; they are isolated in the override mechanism so they can be corrected without an API change.
Installer safety rules (the CLI and both SDKs enforce them): destinations resolve inside the project root, with traversal and symlink escapes refused; a file that differs from the lock's recorded hash is never overwritten without --force; every file sha256 and the bundle content_hash are verified before anything is written; writes are atomic; setuid and setgid are stripped and modes capped at 0755. Where config files merge, objects merge deeply and a conflicting key stops the install (or is skipped without --force) instead of being overwritten.
Archives and plugin marketplaces for tools that do not speak this API are on Distribution formats.
Last updated: