Package: skillx-sh | Binary: skillx | Version: 0.8.0 (skillx add is on main, not yet released) | Node: >=22
Config file: managed by conf: ~/.config/skillx-nodejs/config.json on Linux, ~/Library/Preferences/skillx-nodejs/config.json on macOS; skillx config show prints the values
Priority: env var > config file > default
| Key | Env Var | Default | Description |
|---|---|---|---|
apiKey |
SKILLX_API_KEY |
— | API key for authenticated requests |
baseUrl |
— | https://skillx.sh |
API server URL |
deviceId |
— | auto-generated UUID | Anonymous tracking ID |
Search for skills in the SkillX marketplace.
Arguments:
<query>(required) — Search query string
Options:
| Flag | Description | Default |
|---|---|---|
-u, --use |
Auto-pick top result and show full details | false |
-c, --compatible <runtime> |
Only skills that declare support for this runtime; prints the filter report (unknown is not support) |
— |
--json |
Machine-readable envelope | false |
Examples:
skillx search "code review"
skillx search "database migration"
skillx search "ui ux" --use
skillx search "lint" --compatible agentkit --jsonOutput: Table with SKILL, CATEGORY, RATING, DESCRIPTION columns.
Smart skill lookup and display with multiple identifier formats.
Arguments:
<identifier>(required) — Skill identifier in any format:slug— Direct slug lookup (e.g.,awesome-skill)author/skill— Two-part identifier (e.g.,duy/skill-creator)org/repo/skill— Three-part GitHub reference (e.g.,duy/skillx-repo/skill-creator)"keywords"— Multi-word search query (e.g.,"data validation")
Options:
| Flag | Description | Default |
|---|---|---|
-r, --raw |
Output raw content only (for piping to AI agents) | false |
-s, --search |
Force search mode regardless of identifier format | false |
--exact |
Resolve the identifier only as a catalog slug; never search or register | false |
--include-refs |
Include references in output | false |
--include-scripts |
Include scripts in output | false |
--json |
Envelope with the resolution (kind: skill or repo scan) and its notes |
false |
When nothing matches, use (and search --use, find after a pick) fails with not_found
and exits 3 in both modes; the human message goes to stderr.
Resolution Logic:
- Spaces in query → search mode
x/yformat → lookup slugx-y; fallback scan GitHub repox/y/zformat → lookup slugx-z; fallback register from GitHub subfolder- Single word → slug lookup; fallback to search on 404
- Multi-word or
--search→ search, auto-pick top result
Exact lookup (--exact): the identifier is taken verbatim as a catalog slug and fetched from
/api/skills/<slug>. Nothing above applies: no search, no GitHub register or scan. A miss fails with
not_found (exit 3). The server also answers an old slug (an alias) with its renamed skill; under
--exact that answer is refused with not_found too, message <slug> is an alias of <canonical>; rerun with the canonical slug and details: { requested, canonical }, before any output or install
count. Use it when a specific slug was approved: taken verbatim, a miss cannot load a different skill.
An identifier outside the slug charset ^[a-z0-9][a-z0-9._-]*$ (uppercase, a slash, whitespace, .,
.., empty), or --exact together with --search, fails with usage_error (exit 2).
skillx use duy-skill-creator --exact --rawRisk Warnings:
- Safe — no warning (default)
- Caution — yellow banner with advisory message
- Danger — red banner with suspicious pattern warning
Raw Output Format:
--- BEGIN EXTERNAL SKILL CONTENT (untrusted, slug: <slug>, risk: <label>) ---
<skill content>
--- REFERENCES --- (if --include-refs)
[type] title - url/filename
--- SCRIPTS --- (if --include-scripts)
name: command (url)
--- END EXTERNAL SKILL CONTENT ---
slug is the catalog slug that was actually resolved; compare it with the slug you asked for.
Any character outside the slug charset is printed as ?, so a server value cannot forge the marker.
Examples:
skillx use awesome-skill
skillx use duy/skill-creator
skillx use duy/skillx-repo/skill-creator
skillx use "data validation"
skillx use awesome-skill --raw
skillx use awesome-skill --raw --include-refs --include-scripts
skillx use "testing" --searchShow a skill's metadata, requirements, declared compatibility, and evidence.
<identifier> is a slug, author/skill, or org/repo/skill.
| Flag | Description |
|---|---|
--target <runtime[@version]> |
Also answer for one runtime |
--json |
Machine-readable envelope |
Gate on compatibility. Exit code is the answer: 0 when the target may be acted on
(declared or verified), 4 when it may not (unknown, unsupported, blocked),
3 when the skill does not exist, 2 for invalid input. --json emits an envelope.
skillx check find-skills --target agentkit@1.4.0 && echo okInteractive search — browse results and select a skill.
Arguments:
<query>(required) — Search query string
Options:
| Flag | Description |
|---|---|
--json |
Emit the hits as an envelope ({ query, results, count }) without prompting |
Flow:
- Displays numbered results:
[1],[2],[3]... - Shows name, category, rating, description per result
- Prompts:
Select a skill [1-N] or press Enter to cancel: - Shows the selected skill exactly as
skillx use <slug>would
If stdin closes before a selection (no terminal, empty pipe), find fails with usage_error
(exit 2); use --json in scripts.
Examples:
skillx find "testing"
skillx find "api integration" --jsonImport (register) skills from a GitHub repo into the SkillX marketplace
(POST /api/skills/register). This was skillx publish [repo];
skillx publish <owner/repo|github-url> still runs it, prints a deprecation
warning on stderr, and will be removed.
Arguments:
[repo](optional) —owner/repoor a GitHub URL. Auto-detects from git remote if omitted.
Options:
| Flag | Description | Default |
|---|---|---|
-p, --path <path> |
Specific skill subfolder path | — |
-s, --scan |
Scan entire repo for all SKILL.md files | false |
--dry-run |
Preview what would be imported without calling API | false |
--json |
Envelope with the request (dry run) or the register response | false |
Resolution Logic:
- No flags → auto-detect (root SKILL.md or scan all)
--path→ register specific skill folder--scan→ find all SKILL.md files in repo--dry-run→ show plan without executing
Requires: API key configured.
Examples:
skillx import # auto-detect from git remote
skillx import owner/repo # explicit repo
skillx import owner/repo --path .claude/skills/my-skill
skillx import owner/repo --scan
skillx import --dry-runInstall skills straight from GitHub, an https git URL or a local directory.
add is a thin front end to the installer: skills land in the
same runtime skills directory (--runtime, default claude-code) with the same
receipts, install lock, conflicts and backups as skillx install. Unlike a
registry release, a skill added this way is not scanned or signed; review
it before use. A skill read from git is pinned in skillx.lock.json by commit
and tree digest (see Lock, update and sync).
Sources:
| Source | Meaning |
|---|---|
owner/repo |
A GitHub repository (default branch) |
owner/repo/skill |
One skill of it, by name or directory name (same as --skill skill) |
github:owner/repo[/path][@ref] |
A directory of the repository, at a branch, a tag or a full commit id |
https://github.com/owner/repo[/tree/<ref>[/<path>]] |
Read as github: (a permalink /tree/<commit>/... included) |
https://host/repo.git |
Any https git repository (default branch) |
./dir, ../dir, an absolute path |
A local directory |
Only https is accepted. http, ssh, git@host: (scp style), file://,
ext:: and other :: transport helpers, a URL with a query or fragment, and
anything starting with - are refused as usage_error; git is also run with
every other protocol disabled. A path or ref with .., ., empty, \ or :
segments is refused. @publisher/package, kit: and collection: belong to
skillx install; @agentkit/* is reserved.
A ref is a branch or tag name, or a full commit id (40 hex digits, or 64 for a
SHA-256 repository; case does not matter). A short commit id reads as a branch
name, since git cannot fetch one. A branch or tag is read with a shallow
clone --branch; a commit is fetched on its own (git init, then
git fetch --depth 1 <url> <commit>), which the server must allow (GitHub and
most hosts do; a host that does not answers not_found or network_error, so
name a branch or tag instead). A host can serve any commit of its object
store, and on GitHub that includes commits pushed only to a fork, so the commit
is read only when a branch or tag of the named repository contains it: the
lock's recorded branch or tag is checked first (on sync --locked), then the
default branch, then every branch and tag, fetching commits only where the
host supports it (--filter=tree:0). A commit no branch or tag has, or a commit read that is not
the one named, is an integrity_error.
Options:
| Flag | Description | Default |
|---|---|---|
-s, --skill <names...> |
Skills to install, by front matter name or directory name (* for all) |
ask; the only skill; all with -y |
-l, --list |
List the skills in the source; install nothing | false |
-y, --yes |
Do not ask (no selection prompt, no confirmation); required to install in human mode when stdin is not a terminal | false |
--force |
Replace a skill added before, or install over a directory nothing owns (edited files are backed up, unknown files kept) | false |
--runtime <id> |
claude-code or codex; several comma-separated (claude-code,codex) or the option repeated install the same read into each |
claude-code |
--project |
Install into the current directory's project skills directory (.claude/skills for claude-code, .agents/skills for codex) instead of the user's |
false |
--lock <file> |
Packages lock that pins skills read from git | skillx.lock.json |
--no-lock |
Write no lock file | |
--json |
Envelope with status (listed, installed or cancelled); never asks, so several skills need --skill. installed carries the first runtime's runtime, scope, skillsDir, planDigest, applied and recovered at the top level, every runtime under targets, and lockPath (null for a local source or --no-lock) |
false |
How a skill is found and read. A skill is a directory with a regular
SKILL.md whose front matter (between a first --- line and the next ---
line) is plain YAML with a non-empty name and description. The front matter
is parsed as data only (YAML 1.2 core schema): a ---js/---javascript fence
is not front matter, and tags or duplicate keys reject the file, so nothing in
a source ever runs. When the source (or github: path) is itself a skill, it
is the only one; otherwise every skill up to 6 directories deep counts,
outside .git, other VCS and cache directories and node_modules, and a
skill's own subdirectories are its content. Skills that cannot be installed are
listed under skipped with the reason.
- Names.
nameis the installed directory name and is never rewritten: it must be a valid alias (up to 64 lowercase letters, digits,.,_,-, not ending in., not a Windows device name). Names starting withak-orak:are reserved for AgentKit and refused. Naming a skipped skill with--skillis ausage_error; two chosen skills with one name are ausage_error. - Content. Each chosen skill is checked with the release tree rules
(
agentkit.skill-tree/v1): a symlink, junction, hardlink, git submodule or other special entry, an unsafe or colliding path, or a tree over the release limits (4,096 files, 32 MiB per file, 256 MiB in all) refuses the skill asincompatible. Links are never followed or copied, neither while searching nor while reading. - Git. The clone is shallow, single-branch and without a checkout, into a
temporary directory removed afterwards: files are read from git objects, so
no repository file is written, and no hook, filter or LFS download runs. git
runs without a shell, with
--before the URL and all credential prompts off (core.askPass=,GIT_TERMINAL_PROMPT=0,GCM_INTERACTIVE=never), so a private repository works only with credentials git already has (a credential helper, or a token in the URL). - Credentials. A token in the URL is used to clone and nothing else: it is
removed from the recorded source and masked (
https://***@host) in every message and envelope.
Where it is recorded. Each added skill is its own installer source,
add:<name>, recorded with the source it came from (no credentials): its
receipt's package is the skill's origin (for example
github:owner/repo/skills/pdf) and version the commit read, or local.
skillx list shows it, and skillx uninstall add:<name> removes it with
ownership proof (and drops it from the lock).
Runtimes and scope. With several runtimes, the source is read once and
every runtime is planned before any is written: a refusal or conflict in one
installs nothing anywhere. A failure while applying a later runtime leaves the
earlier ones installed (and pinned) and is reported with them
(details.installed, details.lockPath); run the same command again. --project (also accepted by install, update, uninstall, list
and sync) installs into the current directory, so run it from the project
root; that project keeps its own receipts, owners and install lock under
$SKILLX_HOME/projects/<digest of the real path>, apart from the user-scope
install of the same runtime; the project gets the skills and the runtime's
.skillx-staging directory beside them (.claude/.skillx-staging,
.agents/.skillx-staging). A .claude, .agents, skills or staging
directory of the project that is a symlink or junction is a conflict, since
writes through it would leave the project. Pass --project again to list,
update or uninstall them.
A skill read from git is written to the packages lock (--lock, default
skillx.lock.json in the current directory; --no-lock skips it) as an
added entry: the repository URL without credentials, the skill's
directory, the branch or tag to follow (ref; the commit itself when one was
named; absent for the default branch), the commit read and the skill's
agentkit.skill-tree/v1 tree digest. A local directory is never locked: it has
no portable source. Adding into a lock of another scope (a kit or collection)
is a usage_error before anything is cloned, as with install.
skillx updateupdates added skills with the registry sources, or only the ones named (skillx update add:pdf; name them apart from registry sources). Each is re-read from its recorded source (a branch or tag is followed, a commit stays where it is, a local directory is read again) and the skill in the directory it was installed from replaces it (when it moved, the only skill of its name; two of that name elsewhere is aconflict); local edits are aconflictunless--force. A skill no longer in its source isnot_found. A source that fails does not stop the others: they are installed and locked, then the command fails with every failed source indetails.failures(the message anddetails.updatedname the owners that were updated). A lock that already pins the skill gets its new commit and digest;updatenever starts a lock. Adding a local copy over a skill that target had from git drops that skill's lock entry, since it no longer comes from the pinned source; any other local add leaves the lock alone.skillx sync --lockedrestores added skills with the packages: each locked commit is fetched, and the skill's directory there must hash to the locked tree digest, elseintegrity_errorand nothing is installed.- A private repository needs credentials git already has (a credential helper): the lock and the recorded source never keep a token from the URL.
- Older CLIs (before this release) refuse a lock with
addedentries as invalid; upgrade every machine that syncs it.
Overwrites. Nothing installed is replaced without --force: adding a name
that add installed before from a different tree is a conflict, and so is a
directory no source owns (with --force its unknown files are kept). A skill
that another source (a registry package, kit or collection) installed at
other content is a conflict even with --force; the same content just adds
add:<name> as a second owner.
Exit codes. 0 installed, listed, or cancelled by an empty answer at a
prompt; 2 usage_error (bad source, refused transport or path, reserved or
invalid name, several skills under --json without --skill, or stdin not a
terminal without --yes or --json: pass --yes, with --skill to choose,
since human mode cannot ask); 3 not_found (no skill,
no such --skill, path, repository or branch); 4 incompatible (a link,
special file or limit in a chosen skill); 1 integrity_error (git returned
another commit than the one named); 5 auth_error (git asked for
credentials); 6 network_error (clone failed or timed out after 120 s);
8 conflict (see Overwrites). Nothing is installed when any chosen skill
fails.
Examples:
skillx add anthropics/skills --list
skillx add anthropics/skills/pdf
skillx add github:acme/agent-skills/skills@v1.2 --skill lint review --yes
skillx add https://github.com/acme/agent-skills/tree/<commit>/skills/lint --yes --runtime claude-code,codex
skillx add acme/agent-skills/lint --project --yes
skillx update add:lint
skillx sync --locked
skillx add https://git.example.com/team/skills.git --skill '*' --json
skillx add ./my-skills --force
skillx uninstall add:pdfThese commands work on immutable, digest-pinned releases. Like every other
command they take --json and follow
JSON Output and Exit Codes; 2 covers invalid
input and a package the registry would reject. skillx release pack and
skillx release publish remain as aliases of skillx pack and
skillx publish <dir>.
Checks a package without uploading: packs it exactly like pack (so a tree
the registry would reject fails here), requires a root SKILL.md with YAML
frontmatter carrying name and description (for --kind skill), and checks
the optional --name, --release-version, --kind and --channel. Lists
every problem in details.problems and exits 2.
skillx validate skills/my-skill --name @acme/my-skill --release-version 1.2.0Packs a package from git at an exact commit into a deterministic tar and
prints its releaseDigest (agentkit.skill-tree/v1). Only committed content
is packed: the tree comes from git objects, never the working tree, so mtimes,
umask, core.autocrlf, core.fileMode and uncommitted edits do not change
the digest (uncommitted edits are reported as a warning). Modes are normalized
to 100644/100755; symlinks and submodules are rejected; OS noise files
(e.g. .DS_Store) are excluded and listed. Packing the same commit twice gives
the same releaseDigest, artifactDigest and bytes.
<source>: a package directory (packsHEAD, path from the repo root) or a git ref used with--path.
| Flag | Description |
|---|---|
-o, --out <file> |
Output tar file (required) |
-p, --path <dir> |
Package directory inside the repository, for a git ref |
--json |
Print the result envelope |
skillx pack skills/my-skill -o my-skill.tar
skillx pack v1.2.0 --path skills/my-skill -o my-skill.tarPacks like pack, then uploads to
POST /api/v1/packages/@publisher/slug/releases. Needs an API key scoped to
releases:publish:@publisher (SKILLX_API_KEY or the config), or --oidc in
GitHub Actions (trusted publishing). Releases are immutable: re-publishing
identical content is a no-op, different content for an existing version is
rejected. The command fails if the registry reports a releaseDigest (or, for
a new release, an artifactDigest) other than the one packed locally.
A source that is an existing path, or any use of --name /
--release-version, is a release. Only a GitHub URL or an owner/repo that is
not a local path (or no source at all) runs the deprecated import (see
skillx import). The import is always public, so --visibility there is a
usage_error (exit 2) and nothing is sent.
| Flag | Description | Default |
|---|---|---|
-n, --name <@publisher/slug> |
Package name (required) | — |
--release-version <semver> |
Release version (required) | — |
-p, --path <dir> |
Package directory, for a git ref | — |
--channel <channel> |
stable, beta or alpha |
stable |
--kind <kind> |
skill, hook-pack or bundle |
skill |
--repo <https-url> |
Provenance repository URL | origin remote (https form) |
--script <path> |
Declare a packaged script (repeatable) | — |
--visibility <visibility> |
public, unlisted (readable by anyone who knows the name, never listed) or private. Omitted: a new package is private (a note goes to stderr, never stdout) and an existing package keeps its visibility. A different visibility for an existing package is refused: api_error, exit 1, error.details.code release.visibility_mismatch (like release.version_exists); the error's hint names the next step: change it first with skillx visibility set (a key may only make a package less visible) or in the web dashboard (/dashboard), or publish without --visibility. Publishing to a deleted package is refused with package.deleted |
— |
--kit-origin <origin> |
Publish an @agentkit/* kit release (remote-signed or local-dev), signed as skillx.release/v2; needs a releases:publish:@agentkit key; the slug omits ak- (@agentkit/plan). Installing v2 releases needs skillx-sh 0.7.0 or later |
— |
--registry <url> |
Registry base URL; https only (http only for localhost) |
configured base URL |
--idempotency-key <key> |
Sends Idempotency-Key: a retry with the same key and bytes replays the first answer; other bytes are refused |
— |
--oidc |
Exchange the GitHub Actions OIDC token for a 15 minute publish token (needs permissions: id-token: write and a trusted publisher) |
— |
--oidc-audience <aud> |
Audience requested for the OIDC token | skillx |
--json |
Print the result envelope | — |
SKILLX_API_KEY=sx_... skillx publish skills/my-skill --name @acme/my-skill --release-version 1.2.0 --visibility public
skillx publish HEAD --path skills/my-skill --name @acme/my-skill --release-version 1.2.0 --oidc --idempotency-key "$GITHUB_RUN_ID"CI: .github/workflows/publish-release.yml (manual dispatch or
workflow_call, never on pull requests) runs the same command with a pinned
CLI version, in the skillx-publish environment, with its
SKILLX_PUBLISH_KEY secret. Its optional visibility input (public or
private) passes --visibility and needs a cli-version that has the flag;
left empty, no flag is sent, so a package it creates is private.
.github/workflows/publish-agentkit-foundation.yml
publishes an AgentKit foundation skill from bestagentkits/agentkit at a
pinned commit with OIDC (fallback SKILLX_AGENTKIT_PUBLISH_KEY) and
--visibility public: the kit
directory kits/<kit>/skills/ak-<x> becomes @bestagentkits/<x> at the
kit's kit.yaml version (see the deployment guide for why @agentkit/* stays
reserved). Deploy the web app before running either workflow with
--visibility: a registry without visibility support refuses the member with
400 release.invalid_metadata and stores nothing (see the deployment guide).
Moves a published release to another channel (like an npm dist-tag): only the
current channel changes; bytes, digests and the signed manifest stay as
published. A publish credential (scoped key or --oidc) may only promote
(alpha → beta → stable); the package owner's account key may also
demote. Every attempt is audited.
| Flag | Description | Default |
|---|---|---|
--to <channel> |
Target channel (required) | — |
--from <channel> |
Expected current channel; the move fails with state_conflict if it changed |
current |
--reason <text> |
Reason stored in the audit log | — |
--registry <url> |
Registry base URL | configured base URL |
--oidc, --oidc-audience |
As for publish |
— |
--json |
Print the result envelope | — |
skillx promote @acme/my-skill@1.2.0 --from beta --to stable --reason "soak passed"Shows or changes who can read a published package: public, unlisted
(readable by anyone who knows the name, never listed) or private.
skillx visibility get <@publisher/slug>reads it fromGET .../releases(visibilitymember), with the API key when one is set, so the owner can read a private package.skillx visibility set <@publisher/slug> <visibility>posts the compare-and-setPOST /api/v1/packages/@publisher/slug/visibility({from, to}, audited; see the API reference).fromis--fromwhen given, else read first withget. When the target equalsfrom, the CLI confirms the stored visibility (also when--fromwas given) and sends no write (data.changed: false); a stored value that differs isapi_errorwitherror.details.codevisibility_conflict.
It needs the owner's unscoped account API key (SKILLX_API_KEY or
skillx config set-key); a publish key (releases:publish:@handle) or any
other scoped key is refused (auth_error, exit 5). A key may only make a
package less visible (public to unlisted, anything to private). Making it
more visible (out of private, or to public) needs a signed-in approval in
the web dashboard: the registry answers 403 visibility.approval_required
(auth_error, exit 5) and the hint names <registry>/dashboard. A
concurrent change is 409 visibility_conflict (api_error, exit 1; the hint
says to re-read with get), and an unknown package, or one you do not own, is
not_found (exit 3).
| Flag | Description | Default |
|---|---|---|
--from <visibility> |
set only: expected current visibility; the change fails with visibility_conflict if it differs |
read with get |
--registry <url> |
Registry base URL | configured base URL |
--json |
Print the result envelope: get {identity, visibility}, set {identity, from, to, visibility, changed, changedAt?} |
— |
skillx visibility get @acme/my-skill
skillx visibility set @acme/my-skill private # narrowing works with an account key
skillx visibility set @acme/my-skill public --json # widening: auth_error, hint names /dashboardLists the skills and personal kits you own or are entitled to
(GET /api/v1/library, see the API reference). Needs an API
key with library:read (or an unscoped key). Without --cursor every page is
read and merged, so data.nextCursor is null; with --cursor only that page
is returned together with its nextCursor.
| Flag | Description | Default |
|---|---|---|
--kind <kind> |
skill or kit |
both |
--query <q> |
Name fragment, at most 64 characters | — |
--limit <n> |
Page size, 1 to 100 | 50 |
--cursor <cursor> |
Read only the page after this cursor | follow all pages |
--json |
Print the result envelope: data is { items, nextCursor, kits } |
— |
Invalid flags are usage_error (exit 2) before any request. data.kits is
{ "status": "unavailable", "code": "kit.api_disabled" } while the deployment
has personal kits off.
Personal kit authoring against the personal kits API.
<kit> is @handle/name. Reads need library:read or kits:write:@<handle>,
writes kits:write:@<handle> (or an unscoped key). There is no review command:
reviewing a proposal and activating a public revision need your signed-in web
session, and an API key gets kit.interactive_approval_required.
| Command | Purpose |
|---|---|
kit show <kit> |
Summary: generation, effectiveRevision |
kit revisions <kit> |
Revision list |
kit revision <kit> <id> |
One revision with its document |
kit push <kit> --document <file> --if-match <generation> |
Store a draft revision (skillx.personal-kit/v1 JSON). --if-match takes 3, g3 or "g3" (0 creates the kit); --idempotency-key overrides the key, which defaults to a digest of kit, generation and document so a retry replays |
kit activate <kit> <revisionId> --digest <digest> |
Make a revision effective. --digest (required, exit 2 without it) is the exact revision digest you approve; --if-match is optional (default: the current generation) |
kit rollback <kit> <revisionId> --digest <digest> |
Re-activate an earlier effective revision; same flags as activate |
kit lock <kit> [--revision <id>] |
The lock of the effective (or named) revision |
Every subcommand takes --json. Failures keep the server's machine-readable
code in error.details.code next to error.details.status:
| HTTP | error.code |
Exit | Typical details.code |
|---|---|---|---|
401, 403 |
auth_error |
5 |
invalid_api_key, auth.scope_missing, kit.publisher_not_owned, kit.interactive_approval_required |
404 |
not_found |
3 |
not_found (unknown kit, and a kit of another account), kit.api_disabled (deployment without personal kits) |
412 (stale If-Match), 409 kit.generation_conflict |
conflict |
8 |
kit.generation_conflict — nothing was written; run kit show, rebase your document and retry |
429 |
rate_limited |
7 |
— |
| other | api_error |
1 |
e.g. kit.approval_required, kit.revision_exists, kit.locked_incompatible |
Report skill usage outcome (requires API key).
Arguments:
<slug>(required) — Skill slug identifier<outcome>(required) — One of:success,failure,partial
Options:
| Flag | Description | Default |
|---|---|---|
-m, --model <model> |
AI model used (e.g., claude-sonnet-4) |
— |
-d, --duration <ms> |
Execution duration in milliseconds | — |
--json |
Machine-readable envelope | false |
Requires: API key configured. An invalid outcome or duration is a usage error (exit 2)
checked before any request; a missing key is auth_error (exit 5).
Examples:
skillx report my-skill success
skillx report my-skill failure --model claude-sonnet-4 --duration 5000
skillx report awesome-skill partial -m gpt-4 -d 2500Manage CLI configuration.
All three subcommands accept --json. config show --json reports the key only as
apiKey: { configured, source, masked }; the plaintext key is never printed. Under
set-key --json the prompt goes to stderr, so stdout stays one envelope. The key may be
piped on stdin; an empty or closed stdin is a usage_error (exit 2).
Set API key interactively.
skillx config set-key
# Prompts: Enter your API key:
# Get your key from: https://skillx.sh/settings/apiSet custom API base URL.
skillx config set-url https://staging.skillx.shDisplay current configuration (API key masked).
skillx config show
# Output:
# Base URL: https://skillx.sh
# API Key: sk_prod_****..._xxxx
# (loaded from the conf config file, e.g. ~/.config/skillx-nodejs/config.json)install, update, uninstall, list, sync and mcp install signed,
pinned releases into an agent runtime's skills directory. Bytes come only from
the release read API; nothing is written until every file is verified.
| Ref | Meaning |
|---|---|
kit:@handle/kit |
The active revision of a personal kit (overlays applied client side) |
kit:@handle/kit#rev_<uuid> |
That exact kit revision |
collection:col_<uuid>[#n] |
Revision n of a collection (default latest) |
@publisher/package[@version] ... |
Explicit packages (newest stable release when no version) |
@publisher/package@version#sha256:<hex> |
That version, only if its release digest is still the pinned one; a different digest is an integrity_error and nothing is installed |
| Id | Skills directory | With --project |
|---|---|---|
claude-code (default) |
$CLAUDE_CONFIG_DIR/skills, else ~/.claude/skills |
./.claude/skills |
codex |
$CODEX_HOME/skills, else ~/.codex/skills |
./.agents/skills |
~ is USERPROFILE on Windows and HOME elsewhere. Installer state lives in
$SKILLX_HOME (default ~/.skillx); a --project install keeps its own under
$SKILLX_HOME/projects/<first 16 hex of the SHA-256 of the project's real path>, so
its ref-counts never mix with the user-scope install. Trusted keys are always
the user's.
skillx install <refs...> [--runtime id] [--project] [--dry-run] [--force] [--lock file | --no-lock] [--json]
skillx update [refs...] [--runtime id] [--project] [--force] [--lock file | --no-lock] [--json]
skillx uninstall <refs...> [--runtime id] [--project] [--lock file | --no-lock] [--json]
skillx list [--runtime id] [--project] [--json]
skillx sync --locked [--lock file | --from kit:@h/k#rev] [--runtime id] [--project] [--dry-run] [--force] [--json]
skillx mcp [--registry url]All accept --registry <url> (default: the configured base URL). The API key
is sent only over https; plain http is refused except for localhost,
127.0.0.1, [::1] and *.localhost.
- install resolves the source to exact versions and digests, plans, then
fetches, verifies and applies. It writes
skillx.lock.jsonin the current directory unless--no-lock. A lock has one scope:packages, or one kit or collection. Package installs merge into a packages lock (by alias); a kit or collection install replaces its own lock. Installing into a lock of another scope is ausage_error(pass--lock other.jsonor--no-lock). Nothing in a release is ever run: files the release marks executable are written with mode 755 and listed on stderr (--json:executableon each changed skill) after the install.--dry-runpreviews the files that would be written; the executable files and declared permissions to review before installing come from the MCPprepare_installanswer (trust). - update re-resolves installed sources (all, or those named) and applies the
difference, then refreshes the lock file when its scope was updated. With
nothing installed it prints
Nothing to update.(--json: emptyoutcomes). Skills installed byskillx add(add:<name>) are re-read from their own source (Lock, update and sync). - uninstall drops a kit, a collection, one or more packages, or skills
installed by
skillx add(add:<name>, named on their own); each explicit package is its own source (packages:@scope/name), whether it was installed alone, with others, or bysync --lockedfrom a packages lock. Each package is dropped from a packages lock file as soon as it is uninstalled, so a run that stops part way leaves the lock in step. Uninstalling a kit or a collection does not rewrite its lock file: the lock still describes that revision and can restore it withsync --locked. A skill another source still needs stays. Only regular files whose SHA-256 still matches the receipt are deleted; edited and unknown files are kept and reported. Links are never followed: a file reached through a symlink or junction is kept, and a skill directory that is itself a link is left alone. - sync --locked restores exactly what a lock pins, including the
addedskills of a packages lock at their locked commit, and refuses drift: a kit revision whose re-resolved lock digest differs, a collection revision whose content digest differs or whose members are not exactly the locked packages (at a member's pinned version or digest), a release whose digest differs, or local edits (unless--force). - --force replaces locally edited skills (the old files are copied to
~/.skillx/backups/<runtime>/<alias>/<stamp>) and installs into unowned directories, keeping unknown files. It never overrides a version conflict between two sources sharing a skill, and never installs over a skill path that is a link or a file (unowned_directory). A symlink or junction inside a skill directory is kept while nothing is rewritten (reinstalling the same version,sync --locked, another source adding itself as owner), but an install or update that would rewrite the directory is acontains_linkconflict, also with--force: links are never copied or followed, so remove or replace them first. - Aliases match
^[a-z0-9]([a-z0-9._-]{0,62}[a-z0-9_-])?$(no trailing.) and are never a Windows device name (con,prn,aux,nul,com1-com9,lpt1-lpt9, with or without an extension).
- The manifest signature is checked against trusted keys;
then package identity, version and
releaseDigest. - Every file's size and SHA-256 must match the manifest (blob cache:
~/.skillx/cache/blobs). - Kit overlays must match their
overlayDigests; the composed tree must hash tocompositionDigest. skillx.jsonis fetched and checked first: acompatibleblock that blocks or omits the runtime, or askillx.jsonthat is not a JSON object, refuses the install (incompatible) before other files are downloaded. Every manifest and overlay path must be relative, with no..,., empty,\or:segment; anything else is anintegrity_error.- Apply per skill: stage in
<runtime dir>/.skillx-staging/, then re-check the target against the receipt. A file created or edited since planning is aconflict(local_modified) and nothing is swapped. Write a journal (~/.skillx/journal/<runtime>/<alias>.json), move the old directory aside, rename stage into place, write the receipt, then retire the old directory: any file in it that neither the receipt, the new tree nor the backup accounts for (it appeared after the re-check) is copied to~/.skillx/backups/<runtime>/<alias>/<stamp>and reported asdisplacedbefore the directory is deleted; a link found there is removed itself (its target path is saved as<path>.link), never followed. A rename that Windows refuses (EPERM/EBUSY) is retried with backoff, then rolled back to the old tree and reported as aconflict. If something else has taken the skill path in the meantime, the old tree is not deleted: it is moved to~/.skillx/backups/<runtime>/<alias>/<stamp>-previous-treeand listed in the error'sparkeddetails. The move is a rename; across volumes (or when the rename stays refused) regular files are copied instead and links are recorded as<path>.link, never followed. An old tree left in staging without a journal is parked the same way. Each run first recovers interrupted journals (roll back while the stage exists, roll forward once it is in place) and dropsrequiredByowners that~/.skillx/owners/<runtime>.jsonno longer records, removing (with ownership proof) a skill left with none, so a killed run converges on rerun. A missing owners file counts as "no owners": every receipted skill is then removed on the next run, keeping edited and unknown files. A corrupt owners file stops the run instead. One run at a time perSKILLX_HOME:install.lockholds the pid, is created atomically, and is taken over only when that process is gone.
Verification fails closed: with no trusted key every install is an
integrity_error. Keys (raw 32-byte Ed25519, base64) are merged from:
- keys embedded in the CLI: the production registry key
skillx-reg-993cd14a011f7b76(since 0.7.1); ~/.skillx/trusted-keys.json:{"keys": {"<keyId>": "<base64>"}};SKILLX_TRUSTED_KEYS=<keyId>=<base64>[,...].
GET /api/v1/keys lists the registry's public keys; confirm a key through a
second channel before trusting it.
Schema and validator: packages/contracts/src/install-lock.ts. It names what
to install, never where: no timestamps, machine paths or secrets.
{
"schema": "skillx.lock/v1",
"resolver": "skillx.install-resolver/1",
"source": { "kind": "kit", "kit": "@alpha/my-kit", "revisionId": "rev_…", "revisionNumber": 3, "revisionDigest": "sha256:…" },
"runtimes": { "claude-code": ">=1.0.0" },
"packages": [
{ "alias": "demo", "package": "@alpha/demo", "version": "1.0.0",
"releaseDigest": "sha256:…", "compositionDigest": "sha256:…", "overlayDigests": ["sha256:…"] }
]
}source.kind is kit, collection (collectionId, revisionNumber,
revisionDigest) or packages. A packages lock may also hold added, the
skills from skillx add read from git (omitted when
there is none; schema in packages/contracts/src/install-lock-added.ts):
"added": [
{ "alias": "pdf", "repository": "https://github.com/acme/skills.git", "path": "skills/pdf",
"ref": "v1.2", "commit": "<40 or 64 hex>", "treeDigest": "sha256:…" }
]An alias is either a package or an added skill, never both. Packages and added skills are sorted by alias; the lock digest
is the SHA-256 of its canonical JSON. The lock is scanned for secrets and
absolute paths (findSyncLeaks) before it is written and when it is read.
~/.skillx/receipts/<runtime>/<alias>.json (skillx.install-receipt/v1):
{
"schema": "skillx.install-receipt/v1",
"runtime": "claude-code", "alias": "demo", "package": "@alpha/demo", "version": "1.0.0",
"releaseDigest": "sha256:…", "compositionDigest": "sha256:…", "overlayDigests": [],
"files": [{ "path": "SKILL.md", "sha256": "…", "mode": "100644" }],
"requiredBy": ["kit:@alpha/my-kit", "packages:@alpha/demo"],
"installedAt": "2026-10-19T12:00:00.000Z"
}files is the ownership proof. requiredBy is the ref-count: a skill is
removed only when its last owner uninstalls. ~/.skillx/owners/<runtime>.json
(skillx.install-owners/v1) maps each owner key to its ref and current lock.
Receipts stay local; they are never uploaded.
Newline-delimited JSON-RPC 2.0 on stdio (initialize, ping, tools/list,
tools/call). Tools:
| Tool | Arguments | Effect |
|---|---|---|
inspect |
runtime? |
Read-only: skills directory, installed skills, sources |
plan |
source or lock, runtime? |
Read-only: the plan and its planDigest |
apply |
as plan, plus confirm: true and planDigest (both required) |
Applies the plan the caller saw; usage_error without either, conflict when the fresh plan differs from planDigest |
force is not accepted over MCP (usage_error); replacing local edits needs
the CLI's --force.
Installer commands use the shared envelope and the one exit-code table in
JSON Output and Exit Codes. The codes they add
are 1 integrity_error (verification failed, nothing written), 4
incompatible (the release refuses the runtime) and 8 conflict (local
edits, an unowned directory, a link inside a skill, a shared version, drift, a
stale plan, or a path in use). The kit commands reuse 8 conflict for a
write based on a stale generation; nothing is written.
| Command | Endpoint | Method | Auth Required |
|---|---|---|---|
search |
/api/search |
POST | No |
use |
/api/skills/{slug} |
GET | No |
use (register) |
/api/skills/register |
POST | Yes |
use (install) |
/api/skills/{slug}/install |
POST | No |
find |
/api/search |
POST | No |
find (detail) |
/api/skills/{slug} |
GET | No |
import (and deprecated publish <owner/repo>) |
/api/skills/register |
POST | Yes |
report |
/api/report |
POST | Yes |
publish <dir> (release publish) |
/api/v1/packages/@{publisher}/{slug}/releases |
POST | Yes (scoped key or --oidc) |
publish --oidc, promote --oidc |
/api/v1/oidc/github/token |
POST | GitHub Actions OIDC token |
promote |
/api/v1/packages/@{publisher}/{slug}/releases/{version}/promote |
POST | Yes (scoped key, OIDC, or owner key) |
| installer (fetch) | /api/v1/packages/@{publisher}/{slug}/releases, /api/v1/releases/{digest}/manifest, /api/v1/releases/{digest}/files/* |
GET | No |
| installer (kit) | /api/v1/kits/@{handle}/{kit}/lock[?revision=] |
GET | Yes (owner) |
| installer (collection) | /api/v1/collections/{id}/revisions/{n|latest} |
GET | Private: owner |
Auth method: Authorization: Bearer <API_KEY> header.
Every command, the release and installer commands included, accepts --json and then
writes exactly one envelope to stdout; spinners and prompts never reach stdout in that mode.
The one exception is skillx mcp, which speaks MCP on stdio and has no single result to
wrap.
{ "ok": true, "data": { "...": "command-specific payload" } }
{ "ok": false, "error": { "code": "not_found", "message": "...", "hint": "...", "details": {} } }hint and details appear only when there is something concrete to say. The exit code
carries the same answer, so a script can branch without parsing either form:
| Exit | error.code |
Meaning |
|---|---|---|
0 |
— | Success (check: the target may be acted on) |
1 |
api_error, unexpected_error, integrity_error |
The API failed without a better class, a bug, or a release failed verification (nothing written) |
2 |
usage_error |
Invalid arguments or options, unknown command, missing argument |
3 |
not_found |
The skill, package, release, kit or collection does not exist |
4 |
incompatible |
The target may not act on the skill. check answers it on a success envelope (data.actionable: false); the installer fails with incompatible |
5 |
auth_error |
API key missing or rejected (401/403); the hint names skillx config set-key |
6 |
network_error |
The API could not be reached |
7 |
rate_limited |
The API answered 429; retry later |
8 |
conflict |
The installer would overwrite something it does not own (see Exit codes), or a kit write used a stale generation (412) |
For check, exit 4 is an answer, not a failure, so its envelope is a success. Only flags before --
count: skillx check -- --json treats --json as an operand.
Parse failures follow the same table: with --json on the command line, an unknown option
prints a usage_error envelope and exits 2. --help, help <command> and --version exit
0; a bare skillx or a group without its subcommand (skillx config) prints help and exits
2. The
implementation lives in output.ts and
run-command.ts; a command wraps its work in
runCommand and throws CliError for failures it classifies itself.
Use --raw flag for machine-readable output:
# Pipe skill content to AI agent
skillx use my-skill --raw --include-refs --include-scripts
# Search and auto-pick top result
skillx search "code review" --useRaw output uses boundary markers for safe parsing:
--- BEGIN EXTERNAL SKILL CONTENT (untrusted, slug: my-skill, risk: safe) ---
...content...
--- END EXTERNAL SKILL CONTENT ---