agentsclimarketplace

Skillctl usage

Skill umanio-agency/skillctl/.claude/skills/skillctl-usage

Round-trip CLI for your personal agent skills library — install, push, detect, fork.

Install
npx -y skills add umanio-agency/skillctl --skill skillctl-usage

Assembled 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 command field 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 on push) are not in results[]. To enumerate everything, use skillctl list --json.
  • summary totals exactly equal the count of corresponding status values in results[].
  • commit is null when no commit was made (nothing to apply, or library-only-read commands like pull/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, access write).
  • Re-running init against 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). Cleartext http:// 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 add clones the repo immediately (fail-fast on a bad URL/credentials). Added libraries default to --access read so you can't push to them by accident. The name all is reserved.
  • The default library is what every command acts on when you don't say otherwise. --from <name> (on list/add) reads from another library; non-default reads are treated as untrusted third-party content (see the audit notes below).
  • pull and push both 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 (run skillctl library add to restore it).
  • push to a write-access library commits directly; to a pr-access library it pushes a skillctl/<slug> branch and opens a PR (gh) or MR (glab), returning the URL. A skill from a read-access source can't be written back — promote it into a writable library with push --to <lib> (see the push section). Opening a PR/MR uses your existing gh/glab auth — no token is stored; unsupported hosts get a "push done, open it manually" message.
  • Each repository may be configured at most once: skillctl library add refuses 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 add in a terminal with more than one library configured (or skillctl 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.

FlagPurpose
--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-tagsSwitch 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
FlagPurposeRequired 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
--allInstall 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-tagsSwitch 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-auditSkip 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]
FlagPurpose
--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.
--allPush 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-tagsSwitch 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.
--yesSkip the interactive PR/MR confirmation (open it without prompting). Always implied in non-interactive mode.
--propagateAfter 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 write library: commits and pushes to its default branch, one commit per library (a run touching several makes several commits); each pushed .skills.toml entry's source_sha is rewritten to that library's new HEAD. In --json, commit is the single commit when exactly one write library was pushed, otherwise null (each result carries its source_sha).
  • For a pr library: pushes a skillctl/<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.toml is 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]
FlagPurpose
--skill <name>Pull only specific skills by name (repeatable). Mutually exclusive with --all/--tag.
--allPull 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-tagsSwitch 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-auditSkip 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>
FlagPurposeRequired in non-interactive
--skill <name>Add a specific detected skill by name (repeatable). Mutually exclusive with --all/--tag.Yes, unless --all or --tag
--allAdd 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-tagsSwitch 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-auditSkip 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
FlagPurposeRequired 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
--allRemove 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.toml entry 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
FlagPurposeRequired in non-interactive
--skill <name>Audit only this skill by name (repeatable). Mutually exclusive with --all. Errors if the name is unknown.No
--allAudit 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 / argPurpose
<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 / argPurposeRequired 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 / argPurposeRequired
<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-runReport 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.
  • 2configuration 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.
  • 3conflict: a destination already exists with no --on-conflict policy in non-interactive mode, a fork target collides in the library, or a local fork target collides.
  • 4git error: git clone/fetch/commit/push/hash-object/ls-tree failed (auth, network, missing user identity, etc.).
  • 5content-audit threshold exceeded: a --fail-on <severity> bar was hit and nothing was written — add refused to install, pull refused to apply, detect refused to add, or audit found 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

  1. No library configured. Calls fail with no library configured — run skillctl init <github-url> first. Run skillctl init (or check for an existing library URL via inspecting ~/.config/skills-cli/config.toml on Linux or the equivalent under ~/Library/Application Support/dev.umanio-agency.skills-cli/ on macOS).
  2. Library cache deleted. Same fix: re-run skillctl init with the URL.
  3. Push without user.name/user.email configured globally. Git itself errors out — the message is forwarded verbatim. Fix: git config --global user.name … / user.email ….
  4. --target for detect must be relative to the library root. Absolute paths are rejected with a clear message.
  5. --skill <name> with a name not in the library / not detected. Fails fast.

Constraints to remember

  • skillctl does not handle merges. When local and library both moved past the recorded source_sha, the operator chooses one side; there is no automatic three-way merge.
  • skillctl push always 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).

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.