Skillctl usage
Round-trip CLI for your personal agent skills library — install, push, detect, fork.
npx -y skills add umanio-agency/skillctl --skill skillctl-usageAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
How to drive the `skillctl` CLI non-interactively. Load PROACTIVELY when the user asks to install, push, or contribute Claude skills, or mentions a "skills library" / "skills repo". Covers every command's flag surface, exit codes, and end-to-end recipes so an agent can run `skillctl` without a TTY.
SKILL.md
39.5 KB, as published. Nobody here has run it
skillctl-usage
skillctl is a Rust CLI that manages a personal Claude skills library across projects. This skill is the agent-facing reference: it documents how to drive every command without prompts, so any agent (Claude Code or otherwise) can use it as a tool.
If you're a human reading this, the same flags work in interactive mode — they pre-fill choices and skip the relevant prompts.
How non-interactive mode is selected
skillctl auto-detects whether stdin and stdout are TTYs. When called from a script, agent, or pipe, it switches to non-interactive mode automatically. You can also force it explicitly with the global --no-interaction flag, or with --json (which implies it).
In non-interactive mode, every decision must come from a flag. If a required input is missing, the command exits with a clear error rather than silently falling back to a prompt.
Structured output: --json
The global --json flag suppresses the human-readable cliclack output and emits a single JSON object on stdout at the end of the command. --json implies --no-interaction. Errors continue to go to stderr.
Per-command top-level shape:
// init
{"command":"init","library":{"url":"…","cache_path":"…"}}
// list
{"command":"list","library":"…","skills":[{"name":"…","path":"…","description":"…|null","tags":["…"]}]}
// add
{"command":"add","destination":"…|null","results":[{"name":"…","status":"installed|skipped|aborted","…":"…"}],"summary":{"installed":N,"skipped":N,"aborted":N}}
// push
{"command":"push","results":[{"name":"…","status":"pushed|forked|promoted|pr_opened|skipped","operation":"update|fork|pr","pr_url":"…","branch":"…","new_name":"…","library":"…","…":"…"}],"commit":{"sha":"…","message":"…"}|null,"summary":{"pushed":N,"forked":N,"promoted":N,"pr_opened":N,"skipped":N}}
// pull
{"command":"pull","results":[{"name":"…","status":"pulled|skipped","fork_local":"…|null","fork_local_path":"…","source_sha":"…"}],"summary":{"pulled":N,"forked_locally":N,"skipped":N}}
// detect
{"command":"detect","target":"…|null","results":[{"name":"…","status":"added|skipped","library_path":"…","local_path":"…","source_sha":"…"}],"commit":{"sha":"…","message":"…"}|null,"summary":{"added":N,"skipped":N}}
// remove
{"command":"remove","results":[{"name":"…","status":"removed|failed","path":"…","removed_folder":true|false,"removed_entry":true|false,"reason":"…"}],"summary":{"removed":N,"failed":N}}
Stable rules:
- The
commandfield always matches the subcommand name. results[]only contains skills that were acted on (installed, pushed, forked, pulled, added, or explicitly skipped). Skills that were silently no-ops (e.g. unchanged onpush) are not inresults[]. To enumerate everything, useskillctl list --json.summarytotals exactly equal the count of correspondingstatusvalues inresults[].commitisnullwhen no commit was made (nothing to apply, or library-only-read commands likepull/list).
One-time setup: link a library
skillctl init https://github.com/<owner>/<repo>
- Clones the library repo into a platform-appropriate cache.
- Persists the URL in a global config file as the default library (named
personal, accesswrite). - Re-running
initagainst the same URL refreshes the cache; against a different URL re-points the default library. - Any git host works — GitHub, GitLab, or self-hosted — over HTTPS or SSH (e.g.
git@host:owner/repo.git). Cleartexthttp://is refused.
Multiple libraries
skillctl can track more than one library. A library carries an access level — read (consume only), write (direct commit), or pr (branch + PR/MR) — and exactly one is the default. The personal library from init is just the default.
skillctl library add <name> <url> [--access read|write|pr] [--default]
skillctl library list
skillctl library remove <name>
skillctl library set-default <name>
library addclones the repo immediately (fail-fast on a bad URL/credentials). Added libraries default to--access readso you can't push to them by accident. The nameallis reserved.- The default library is what every command acts on when you don't say otherwise.
--from <name>(onlist/add) reads from another library; non-default reads are treated as untrusted third-party content (see the audit notes below). pullandpushboth follow each skill's provenance — a skill installed from any configured library is refreshed from / written back to that library (a run may touch several). A skill whose recorded provenance is no longer configured is listed and skipped (runskillctl library addto restore it).pushto awrite-access library commits directly; to apr-access library it pushes askillctl/<slug>branch and opens a PR (gh) or MR (glab), returning the URL. A skill from aread-access source can't be written back — promote it into a writable library withpush --to <lib>(see the push section). Opening a PR/MR uses your existinggh/glabauth — no token is stored; unsupported hosts get a "push done, open it manually" message.- Each repository may be configured at most once:
skillctl library addrefuses a URL that resolves to an already-configured repo (same repo under two access levels would make a skill's write target depend on config order). - Interactive only: running
skillctl addin a terminal with more than one library configured (orskillctl add --from all) opens a picker with a tab per library (←/→ to switch, opens on the default); selections accumulate across tabs into one install. Agents/non-interactive runs use the flags above instead.
Commands
skillctl list — read-only
skillctl list
skillctl list --from <name> # list a specific library
skillctl list --from all # list every configured library
skillctl list --tag <tag> [--tag <tag> …] [--all-tags]
Refreshes the library cache (best-effort git fetch) and prints every skill with its name, any frontmatter tags in […], and a one-line description.
| Flag | Purpose |
|---|---|
--from <name> | List a named library instead of the default. --from all spans every configured library, grouped by library (each section shows the library name, access, and URL). |
--tag <tag> | Filter to skills carrying this tag. Repeatable; default semantics is union (any of the given tags). |
--all-tags | Switch to intersection (skill must carry every requested tag). Requires --tag. |
Under --json, a single-library list emits { "command": "list", "library": <url>, "skills": [...] }; --from all emits { "command": "list", "from": "all", "libraries": [ { "name", "url", "access", "default", "skills": [...] } ] }.
skillctl add — install skills from the library into a project
skillctl add --skill <name> [--skill <name> …] --dest <path>
skillctl add --all --dest <path>
skillctl add --tag <tag> [--tag <tag> …] [--all-tags] --dest <path>
skillctl add --from <name> --skill <name> --dest <path> # install from one named library
skillctl add --from all --tag <tag> --dest <path> # install matching skills from every library
| Flag | Purpose | Required in non-interactive |
|---|---|---|
--from <name|url> | Install from a named library instead of the default. A git URL or github:owner/repo / gitlab:owner/repo shorthand installs ad-hoc from a remote source that isn't a configured library (see below). --from all installs matching skills from every configured library in one run (non-interactive: requires a selection — --all/--skill/--tag). Installing from any non-default source forces the content audit on. | No |
--save-as <name> | Only with an ad-hoc --from <url>: also register the source as a read-access library under this name, so skillctl pull can track it later. Ignored when --from names an already-configured library. | No |
--skill <name> | Install a specific skill (repeatable). Mutually exclusive with --all and --tag. | Yes, unless --all or --tag |
--all | Install every skill in the library. Mutually exclusive with --skill and --tag. | Yes, unless --skill or --tag |
--tag <tag> | Install every skill carrying this tag (repeatable). Default semantics is union (any of the given tags). Mutually exclusive with --skill and --all. | Yes, unless --skill or --all |
--all-tags | Switch tag matching from union to intersection (skill must carry every requested tag). Requires --tag. | No |
--dest <path> | Project-relative destination folder (e.g. .claude/skills). The folder is created if missing. | Yes |
--on-conflict <overwrite|skip|abort> | Strategy when a destination skill folder already exists. | Yes if any conflict is encountered |
--no-audit | Skip the content security audit of skills before installing. | No |
--fail-on <info|warning|critical> | Refuse the whole batch (install nothing, exit 5) if any selected skill's content audit reaches this severity. Without it the audit is warn-only. | No |
Before anything is copied, add runs a content security audit (see skillctl audit) on each selected skill. By default it is warn-only (findings are logged, the install proceeds); under --json each installed skill's result carries an "audit_verdict" field (safe/caution/warning/dangerous) so a non-interactive caller still sees the signal. Pass --fail-on <severity> to block, or --no-audit to skip the scan entirely. The same gate covers pull (incoming library content) and detect (local content before publishing) — see their sections.
Interactive triage (TTY only). In an interactive terminal on the warn-only path (no --fail-on), if any skill is flagged (verdict ≥ warning) add/pull/detect present a batch-triage menu before applying: Decide for each (walk each flagged skill — include / skip / view findings), Proceed with all, or Cancel everything (nothing is applied, exits 0). Non-flagged skills always proceed. This never triggers non-interactively (agents/--json use --fail-on/--no-audit instead), so it does not affect scripted runs.
When installing from a non-default library (--from <name> where <name> isn't the default, or --from all while any non-default library is configured), the content is untrusted third-party material, so the audit is mandatory: --no-audit is refused (exit 2). It is still warn-only unless you add --fail-on. Installs from the default library are unaffected.
Ad-hoc remote install (--from <url>): skillctl add --from github:owner/repo --skill foo --dest .claude/skills clones the repo into the cache, audits its content (mandatory — --no-audit refused), and installs the selected skills with their remote-URL provenance. By default the source stays ephemeral — recorded in .skills.toml by URL but not added to config.toml, so pull/push skip it (it's a one-shot install). To keep tracking it, pass --save-as <name> (or accept the interactive "keep as a library?" offer) and it's registered as a read library. If the URL already matches a configured library, --from <url> simply installs from that library. Accepts the github:/gitlab: shorthand and full https:///git@/ssh:// URLs; installs from the default branch HEAD.
With --from all, the default library is installed first and keeps the skill's bare name; if another library offers a skill whose name (or destination folder) is already taken, that install is suffixed -<library> (e.g. deploy from personal + deploy-team from team), so both land with distinct names, folders, and provenance. The JSON results[] entries carry a library field naming the source.
Each installed skill is recorded in .skills.toml at the project root with the source path inside the library, the library commit SHA at install time, the local destination, an RFC3339 timestamp, and the provenance (library name + library_url) it was installed from.
Tags are read from the SKILL.md frontmatter. Both inline and block forms work:
---
name: claude-api
description: Build and tune Claude API apps with prompt caching.
tags: [api, claude, caching]
---
---
name: claude-api
tags:
- api
- claude
- caching
---
A bare scalar tags: foo is accepted and treated as a single-tag list. Tag flags on push and pull read tags from each skill's local SKILL.md (the user's current view), so retagging locally takes effect on the next run without needing to push or pull first.
description: accepts both a single-line value and YAML block scalars:
description: |
Multi-line literal description.
Newlines are preserved.
description: >
Multi-line folded description that
joins lines with spaces, useful for
one long sentence wrapped in source.
skillctl push — propagate local edits back to the library
skillctl push --skill <name> [--skill <name> …]
skillctl push --all
skillctl push --tag <tag> [--tag <tag> …] [--all-tags]
| Flag | Purpose |
|---|---|
--to <name> | Promotion mode. Publish the selected skills into this writable library (rewriting their provenance) instead of pushing each back to its own. Use it to contribute a skill installed from a read-only source into your own/team library. On a path collision in the target, --on-divergence applies. |
--skill <name> | Push only specific skills by name (repeatable). Mutually exclusive with --all/--tag. |
--all | Push every skill that has pushable changes. Mutually exclusive with --skill/--tag. |
--tag <tag> | Push every pushable skill whose local SKILL.md carries this tag. Repeatable; default semantics is union (any of). Mutually exclusive with --skill/--all. |
--all-tags | Switch tag matching to intersection (skill must carry every requested tag). Requires --tag. |
--on-divergence <overwrite|skip|fork> | Strategy for divergent (and library-missing) skills. Default when omitted: skip with a warning. |
--fork-suffix <suffix> | Required when --on-divergence fork is used non-interactively. New name = <original>-<suffix>. |
--message <text> | Override the auto-generated commit message. For a pr-access library it is also the PR/MR description. |
--pr-title <title> | Title for the PR/MR opened against a pr-access library (default: auto-generated). Ignored for write libraries. |
--yes | Skip the interactive PR/MR confirmation (open it without prompting). Always implied in non-interactive mode. |
--propagate | After the push succeeds, fan each pushed update out to every other project on disk that installed it from the same library (same engine as skillctl propagate). Mutually exclusive with --to. |
--root <path> | Directory to scan for install sites when --propagate is set (repeatable). Requires --propagate; falls back to [propagate] roots in config.toml when omitted. |
For each pushable skill, skillctl push runs a content diff (via git blob hashes) and applies the chosen strategy. push follows provenance: each skill is written back to the library it was installed from.
- For a
writelibrary: commits and pushes to its default branch, one commit per library (a run touching several makes several commits); each pushed.skills.tomlentry'ssource_shais rewritten to that library's new HEAD. In--json,commitis the single commit when exactly onewritelibrary was pushed, otherwisenull(each result carries itssource_sha). - For a
prlibrary: pushes askillctl/<slug>branch and opens a PR (gh) / MR (glab); the result carries"status":"pr_opened","pr_url", and"branch", and the URL is shown in the outro..skills.tomlis not changed (the skill isn't merged yet). Interactive runs show an editable title + confirm;--yes/non-interactive open it directly.
Skills from read libraries, and skills whose provenance is no longer configured, are listed and skipped (see the access notes above).
Promotion (push --to <writable-library>): publishes the selected skills' local content into <library> (regardless of where they came from) and rewrites their .skills.toml provenance to it — the way to contribute a skill installed from a read-only source. Each skill lands at its current source_path in the target; if that path is already taken there, the --on-divergence policy decides — overwrite (replace the target's version), fork (add as a new skill under <name>-<--fork-suffix>, renaming the local folder), or skip (default non-interactively; interactive shows a three-way prompt). The target must be write-access (read is refused; pr promotion isn't supported yet). JSON results carry "status":"promoted" (+ "new_name" when forked).
Fork (creating a new library skill from local edits) is supported non-interactively via --on-divergence fork --fork-suffix <s>: every divergent (or library-missing) skill is forked under the name <original>-<suffix>.
Propagate-on-push (push --propagate): once the push has committed and pushed, each just-pushed update is fanned out to every other install site under the scan roots — a one-step "push here, update everywhere". Only round-trip updates propagate; forks and --to promotions never do. The project you pushed from is skipped (it's already at the new HEAD). Scan roots come from --root (repeatable) or, when omitted, [propagate] roots in config.toml; if neither supplies a root the command fails fast (before pushing anything). With --json, a "propagated" block is added alongside results — { "results": [ { "project", "skill", "status": "updated"|"skipped"|… } ], "summary": { "updated", "would_update", "skipped" } } — and only when --propagate was passed.
skillctl pull — refresh installed skills from the library
skillctl pull --skill <name> [--skill <name> …]
skillctl pull --all
skillctl pull --tag <tag> [--tag <tag> …] [--all-tags]
| Flag | Purpose |
|---|---|
--skill <name> | Pull only specific skills by name (repeatable). Mutually exclusive with --all/--tag. |
--all | Pull every skill that has library updates available. Mutually exclusive with --skill/--tag. |
--tag <tag> | Pull every pullable skill whose local SKILL.md carries this tag. Repeatable; default semantics is union. Mutually exclusive with --skill/--all. |
--all-tags | Switch tag matching to intersection. Requires --tag. |
--on-divergence <overwrite|skip|fork> | Strategy for divergent skills. fork here means fork-locally (rename the local copy under a new name, then pull the library version into the original destination). Default when omitted: skip. |
--fork-suffix <suffix> | Required when --on-divergence fork is used non-interactively. New local name = <original>-<suffix>. |
--no-audit | Skip the content security audit of the incoming library content. Refused (exit 2) for skills whose provenance is a non-default (third-party) library. |
--fail-on <info|warning|critical> | Refuse the whole batch (pull nothing, exit 5) if any incoming skill's content audit reaches this severity. Without it the audit is warn-only. |
For each pullable skill, skillctl pull runs the same blob-SHA classification as push (in reverse direction): pullable = LibraryAhead (library moved, local hasn't) or BothDiverged. Library content overwrites local; the project's .skills.toml source_sha is rewritten to the current library HEAD. No git operations on the project side — the project repo is untouched, and the user can review/commit the resulting file changes via their own workflow. pull follows provenance: each skill refreshes from the library it was installed from (a run may touch several library caches), and its source_sha is rewritten to that library's HEAD. A skill whose recorded provenance is no longer a configured library is listed and skipped.
Fork-locally (preserving your local edits under a new name while pulling the library version into the original location) is supported non-interactively via --on-divergence fork --fork-suffix <s>: each divergent skill's local folder is renamed to <original>-<suffix>, then the library version drops into the original destination.
Content audit on pull. Before applying, pull audits the incoming library version of each skill about to land (the same scan as skillctl audit). It is warn-only by default; under --json each pulled result carries an "audit_verdict" field (safe/caution/warning/dangerous). --fail-on <severity> refuses the whole batch (nothing is written, exit 5); --no-audit skips the scan, but is refused (exit 2) for any skill whose provenance is a non-default (third-party) library — third-party updates can never be silenced.
skillctl detect — find new local skills and add them to the library
skillctl detect --skill <name> [--skill <name> …] --target <library-path>
skillctl detect --all --target <library-path>
skillctl detect --tag <tag> [--tag <tag> …] [--all-tags] --target <library-path>
| Flag | Purpose | Required in non-interactive |
|---|---|---|
--skill <name> | Add a specific detected skill by name (repeatable). Mutually exclusive with --all/--tag. | Yes, unless --all or --tag |
--all | Add every detected new skill. Mutually exclusive with --skill/--tag. | Yes, unless --skill or --tag |
--tag <tag> | Add every newly detected skill carrying this tag (repeatable). Default semantics is union. Mutually exclusive with --skill/--all. | Yes, unless --skill or --all |
--all-tags | Switch tag matching to intersection. Requires --tag. | No |
--to <name> | Writable library to add the skills to. Defaults to the sole write-access library; required when several are configured. Refused for read/pr libraries. | Yes, when more than one writable library is configured |
--target <path> | Library-relative folder where the new skills should land. Use . for the library root (flat-layout libraries), or e.g. skills / .claude/skills for a subfolder. | Yes |
--no-audit | Skip the content security audit of the local skills before adding them to the library. | No |
--fail-on <info|warning|critical> | Refuse the whole batch (add nothing, exit 5) if any skill's content audit reaches this severity. Without it the audit is warn-only. | No |
skillctl detect walks the current directory for SKILL.md files, drops anything already declared in .skills.toml, copies the leftovers into the chosen library's cache under <target>/<skill-folder-name>, single-commits with a add skill(s): … message, pushes, and appends the new entries to .skills.toml with that library's provenance. The target library is the sole writable library by default; with several, pass --to <name> (non-interactive) or pick from the Select (interactive). read/pr libraries cannot be detect targets (the latter pending the PR/MR flow).
Content audit on detect. Before publishing, detect audits the local content of each new skill — a pre-publish check for secrets or dangerous patterns headed for a (possibly shared) library. It is warn-only by default; under --json each added result carries an "audit_verdict" field. --fail-on <severity> refuses the whole batch (nothing is copied, committed, or pushed; exit 5); --no-audit skips the scan.
skillctl remove — remove skills from the current project
skillctl remove --skill <name> [--skill <name> …]
skillctl remove --all
| Flag | Purpose | Required in non-interactive |
|---|---|---|
--skill <name> | Remove a specific skill by name (repeatable). Mutually exclusive with --all. Errors if the name is unknown or ambiguous (two skills share it). | Yes, unless --all |
--all | Remove every removable skill found in the project. Mutually exclusive with --skill. | Yes, unless --skill |
skillctl remove is project-only — it never touches the library or git. It walks the current directory for skill folders (respecting .gitignore, skipping node_modules/target) and cross-references .skills.toml, presenting three kinds of removable skill:
- installed via skillctl — folder present and tracked in
.skills.toml. Removing it deletes the folder and drops the entry. - created locally, not tracked — folder present but absent from
.skills.toml. Removing it deletes the folder only. - orphan — a
.skills.tomlentry whose folder is already gone. Removing it drops the stale entry only (nothing to delete on disk).
In each results[] item, removed_folder and removed_entry report which of the two actions actually happened. .skills.toml is only rewritten when at least one tracked entry is dropped. In an interactive TTY, a confirmation prompt is shown before anything is deleted; in non-interactive/--json mode the explicit --skill/--all flags are the authorisation. A symlinked destination is never followed — it is treated as "no folder on disk" so removal can only ever drop its manifest entry, never delete through the link.
skillctl audit — scan skill content for dangerous patterns
skillctl audit # scan every skill in the project
skillctl audit --skill <name> # scan only this skill (repeatable)
skillctl audit --fail-on warning # exit 5 if any finding reaches the threshold
skillctl --json audit
| Flag | Purpose | Required in non-interactive |
|---|---|---|
--skill <name> | Audit only this skill by name (repeatable). Mutually exclusive with --all. Errors if the name is unknown. | No |
--all | Audit every skill found in the project (the default behaviour). Mutually exclusive with --skill. | No |
--fail-on <info|warning|critical> | Exit with code 5 if any finding reaches this severity. Without it, audit always exits 0. | No |
audit is read-only — it scans the SKILL.md and any bundled files of each skill discovered in the current project and reports a per-skill verdict (safe / caution / warning / dangerous). Categories: credentials (embedded keys/tokens — critical), obfuscation (long base64 / hex-escape blobs — warning), shell (rm -rf, curl|sh — warning/info), dynamic-code (eval( — info), and prompt-injection (instruction-override / conceal-from-user / exfiltration phrasings — warning). It is a heuristic advisory aid, not a guarantee. The same scan gates skillctl add (see above). The --json shape is { "command": "audit", "skills": [ { "name", "verdict", "findings": [ { "severity", "category", "label", "file", "line", "snippet" } ] } ], "summary": { "scanned", "worst_severity" } }.
skillctl tag — edit a skill's tags
skillctl tag add <tag> [<tag> …] --skill <name> # add tag(s)
skillctl tag remove <tag> [<tag> …] --skill <name> # remove tag(s)
skillctl --json tag add code-review --skill my-skill
| Flag / arg | Purpose |
|---|---|
<tag>… | One or more tags to add/remove (positional, required). Each must be a simple token — no , [ ] " ' or whitespace beyond a plain space. |
--skill <name> | The skill to edit, by name. Must exist in the current project; errors if unknown or if two skills share the name. |
skillctl tag rewrites the tags: line in the named skill's SKILL.md frontmatter — project-local, no git or network (like remove). add unions the new tags onto the existing set (de-duplicated); remove drops them; removing the last tag deletes the tags: field. Everything else in the file (other frontmatter keys, the body, line endings) is preserved, and the write is atomic. Because push/pull/list read tags from the local SKILL.md, a retag takes effect immediately and propagates to the library on the next push. The --json shape is { "command": "tag", "skill": "…", "tags": ["…"], "changed": true|false }.
skillctl create — scaffold a new local skill
skillctl create <name> --dest <path> [--description <text>] [--tag <tag> …]
skillctl create video-cutter --dest .claude/skills --description "Cut and trim videos." --tag video
skillctl --json create my-skill --dest .claude/skills
| Flag / arg | Purpose | Required in non-interactive |
|---|---|---|
<name> | Name of the new skill — also its folder name. A simple token (no /, \, ./.., or control characters). | Yes (positional) |
--dest <path> | Parent folder the skill is created under, relative to the project root (e.g. .claude/skills). The skill lands at <dest>/<name>/SKILL.md. Interactive mode picks from detected skills/ folders or the presets. | Yes |
--description <text> | One-line description for the frontmatter (what the skill does / when to use it). A TODO: placeholder is written if omitted. | No |
--tag <tag> | Tag added to the frontmatter tags: array. Repeatable. | No |
skillctl create writes a template SKILL.md — frontmatter (name, description, and an inline tags: […] array when tags are given) plus a body skeleton (# <name>, ## Instructions, ## Examples) — that skillctl's own parser reads back, so the new skill is immediately visible to detect/add/audit. It is project-local: no library, git, network, or .skills.toml entry (a created skill is untracked until skillctl detect publishes it to a library). Refuses to overwrite an existing folder (exit 3). The --json shape is { "command": "create", "name": "…", "path": "…", "created": true }.
skillctl propagate — push a library update into every project that has it
skillctl propagate <skill> [<skill> …] --root <path> [--root <path> …] [--from <library>] [--dry-run]
skillctl --json propagate my-skill --root ~/Code
skillctl propagate my-skill # scan roots taken from [propagate] roots in config.toml
| Flag / arg | Purpose | Required |
|---|---|---|
<skill>… | Skill name(s) to propagate, as recorded in installers' .skills.toml. | Yes (positional) |
--root <path> | Directory to scan for .skills.toml install sites (repeatable). Nested node_modules/target/.git and .gitignored paths are skipped. | Falls back to [propagate] roots in config.toml; errors (exit 2) if neither is set |
--from <library> | Library whose current version is propagated (defaults to the default library). Only sites whose provenance matches this library are touched. | No |
--dry-run | Report which projects would update without writing anything. | No |
skillctl propagate makes a library's current version of a skill live in every other project that installed it — "fix once, updated everywhere" — without visiting each project by hand. It discovers install sites by scanning the roots for .skills.toml (so it works even for projects you moved or freshly cloned — there is no central registry to keep in sync). Roots come from --root (repeatable) or, when omitted, the configured [propagate] roots (see below); if neither supplies a root it errors (exit 2). For each discovered site whose .skills.toml has an entry matching the skill and the library's provenance (library_url), it runs the same classification as pull: a site cleanly behind the library is updated (its skill folder is replaced with the library HEAD version and its source_sha rewritten); a site with local edits is skipped and reported (never clobbered — go pull/push there); an up-to-date site is a noop; a site installed from a different library is left untouched. No git operations on the project side. The library cache is fetched first so HEAD is current. The --json shape is { "command": "propagate", "skills": […], "library": "…", "dry_run": bool, "results": [ { "project", "skill", "status": "updated"|"up-to-date"|"skipped"|"would-update"|"failed", … } ], "summary": { "updated", "would_update", "skipped", "up_to_date" } }.
Configured scan roots. Add a [propagate] section to config.toml to make --root optional for both propagate and push --propagate:
[propagate]
roots = ["~/Code", "/srv/projects"]
The section is omitted from the file while empty, so a config that never propagates stays byte-clean. --root on the command line overrides the configured roots for that run.
To propagate in the same step as a push, use push --propagate (see the push section) — it pushes your local edits, then fans the new version out to every other install site under the scan roots.
Skill identity
A "skill" is any folder containing a file literally named SKILL.md. The skill's name comes from the YAML frontmatter name: field at the top of SKILL.md; if absent, the folder name is used. All --skill <name> flags match against this resolved name.
Exit codes
0— success, including "nothing to do" outcomes (no changes to push, no new skills detected, no skills to install, etc.).1— generic / unexpected error.2— configuration error: no library configured, library cache missing, malformed URL, missing required flag in non-interactive mode (e.g.--dest,--skill,--target), invalid skill name, malformed.skills.toml.3— conflict: a destination already exists with no--on-conflictpolicy in non-interactive mode, a fork target collides in the library, or a local fork target collides.4— git error:git clone/fetch/commit/push/hash-object/ls-treefailed (auth, network, missing user identity, etc.).5— content-audit threshold exceeded: a--fail-on <severity>bar was hit and nothing was written —addrefused to install,pullrefused to apply,detectrefused to add, orauditfound a finding at or above the threshold.
Agents should branch on exit code first, then optionally inspect stderr for context. Stdout in --json mode is always either a single JSON object (success or partial success) or empty (early failure before output is built).
Output
skillctl prints a tree-style human log to stdout in interactive/default mode (intro line, per-skill log::* lines, outro summary). In --json mode, that human log is suppressed and a single structured JSON object lands on stdout instead — see "Structured output: --json" above for shapes.
Errors always go to stderr regardless of mode.
Interactive prompt (multi-select with live filter)
When a multi-select prompt opens (in add, push, pull, detect, remove without flags or --all and a TTY), the prompt has a live filter:
- Type any character — appends to the filter; the list filters in real time on the skill name (substring, case-insensitive).
- Backspace — edits the filter (or clears an active tag filter, see below, when the text is empty).
- ↑ / ↓ — navigates the filtered list.
- Space or Tab — acts on the focused row: toggles a skill, or runs a tag meta-action (below).
- Enter — confirms the prompt with all currently-selected items.
- Esc or Ctrl+C — cancels (Esc first clears an active tag filter, if any).
The filter searches skill names only (so Space stays available as a toggle). The hint/description column is shown next to each row but not searched. A windowed view shows up to ~12 items at a time with ↑ N more above / ↓ N more below indicators when needed.
Tag meta-actions (add/pull/detect/push). When the typed query matches a tag carried by the candidates, two actionable rows appear above the skill matches: ▸ tag:<name> — filter to N skill(s) (narrows the list to that tag; Esc/Backspace exits the tag filter) and ▸ tag:<name> — select all N (picks every skill carrying it, then shows them). Activate a meta-row with Space/Tab. This is the interactive equivalent of the --tag/--all-tags flags — same set, chosen visually. (The tabbed cross-library picker and the push --to/remove pickers don't surface tag meta-rows.)
Agents driving the CLI never see this prompt — --json and non-TTY contexts suppress all interactive UI.
Recipes
Install a fresh project's skills
skillctl init [email protected]:me/skills.git
skillctl add --skill claude-api --skill review --dest .claude/skills
Bulk-install everything
skillctl add --all --dest .claude/skills --on-conflict skip
Add a team library and install from it
skillctl library add team https://gitlab.com/acme/ai-config --access read
skillctl list --from all # browse what's where
skillctl add --from team --skill deploy --dest .claude/skills # audit is mandatory here
Bulk-install every skill carrying a tag
skillctl add --tag api --dest .claude/skills
Install only skills tagged with both code-review AND gitlab
skillctl add --tag code-review --tag gitlab --all-tags --dest .claude/skills
List every skill tagged meta
skillctl list --tag meta
Push every local edit, defaulting to skip on conflicts
skillctl push --all
Force-push everything, including diverged skills
skillctl push --all --on-divergence overwrite
Fork every diverged skill under a <name>-custom library entry
skillctl push --all --on-divergence fork --fork-suffix custom
Pull every available library update
skillctl pull --all
Force-pull everything, even where local has unrelated edits
skillctl pull --all --on-divergence overwrite
Pull updates while keeping your local edits as <name>-local
skillctl pull --all --on-divergence fork --fork-suffix local
Contribute every new local skill back to the library (library root)
skillctl detect --all --target .
Contribute new local skills to a skills/ subfolder of the library
skillctl detect --all --target skills
Push specific skills with a custom message
skillctl push --skill review --skill security-review --message "polish: tighter reviewer prompts"
Remove specific skills from a project
skillctl remove --skill claude-api --skill review
Remove every skill from a project (folders + manifest entries)
skillctl remove --all
Failure modes worth checking before invoking
- No library configured. Calls fail with
no library configured — run skillctl init <github-url> first. Runskillctl init(or check for an existing library URL via inspecting~/.config/skills-cli/config.tomlon Linux or the equivalent under~/Library/Application Support/dev.umanio-agency.skills-cli/on macOS). - Library cache deleted. Same fix: re-run
skillctl initwith the URL. - Push without
user.name/user.emailconfigured globally. Git itself errors out — the message is forwarded verbatim. Fix:git config --global user.name …/user.email …. --targetfordetectmust be relative to the library root. Absolute paths are rejected with a clear message.--skill <name>with a name not in the library / not detected. Fails fast.
Constraints to remember
skillctldoes not handle merges. When local and library both moved past the recordedsource_sha, the operator chooses one side; there is no automatic three-way merge.skillctl pushalways produces one commit per run, regardless of how many skills are touched.- Forking is now supported non-interactively via
--on-divergence fork --fork-suffix <s>— the suffix is appended to each forked skill's name. Without--fork-suffix, fork stays interactive (each fork prompts for a name).