Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

zskills

A package manager for Claude Code skills. Declarative install, multi-marketplace, written in Rust. Manages both Claude Code plugins (via marketplaces, settings.jsonenabledPlugins) and Agent Skills (the older raw-SKILL.md format under ~/.claude/skills/) from a single manifest.

Think brew bundle for Claude Code: a skills.toml declares intent, your ~/.claude/settings.json and installed_plugins.json get reconciled atomically. Works with any marketplace tap (zot24-skills, claude-plugins-official, cloudflare, custom) and any GitHub repo that exposes an Agent Skill under skills/<name>/SKILL.md. npm-distributed skill bundles (like get-shit-done-cc) are supported via an npm = "<pkg>" declaration.

Install

cargo install --git https://github.com/zot24/zskills

Requires git and (for npm-sourced skills) npm on $PATH.

Quick start

Create ~/.config/zskills/skills.toml:

# Claude Code plugins (marketplace-based)
[[skills]]
name = "umbrel-app"
marketplace = "zot24-skills"

[[skills]]
name = "cloudflare"
marketplace = "cloudflare"

# Agent Skills from a GitHub repo
[[agent_skills]]
source = "jakubkrehel/make-interfaces-feel-better"

# Agent Skills from an npm package (with glob ownership)
[[agent_skills]]
npm = "get-shit-done-cc"
claims = ["gsd-*"]

Then:

zskills marketplace add zot24/skills    # register the marketplace
zskills sync                            # apply the manifest
zskills upgrade                         # refresh everything from origin
zskills list                            # see what's installed
zskills doctor                          # reconcile disk ↔ inventory ↔ settings

Commands

zskills list [-v]                       # what's installed; agent skills grouped by source
zskills install <name>                  # add a plugin to enabledPlugins
zskills remove  <name>                  # disable + drop inventory (keep bytes)
zskills purge   <name>                  # also delete bytes
zskills enable  <name> / disable <name> # flip the flag only
zskills sync [--file f.toml] [--prune]  # apply declarative manifest
zskills upgrade [<name>...]             # ONE command: refresh everything
zskills update [<name>...]              # refresh marketplace caches (plugins only)
zskills doctor [--fix]                  # reconcile disk ↔ inventory ↔ settings
zskills scan [path]                     # find project-scope skills across a tree
zskills migrate <project>               # promote project skills to user scope
zskills migrate-skill <name>            # promote ONE skill across every project
zskills migrate-all <dir>               # interactive sweep
zskills marketplace add|remove|list|update

Full reference: Commands. Workflows and recipes: Use cases. How it works internally: Architecture. Stuck? Troubleshooting.

Why

Existing skill managers are JavaScript shims with per-skill Node cold-start, no atomic write semantics, no notion of upgrade-from-origin for marketplaces shipped as tarballs, and no way to take ownership of skill bundles installed via other tooling. zskills is a single static binary that wraps Claude Code’s existing plugin substrate, atomically — preserving every unknown field in your settings.json (hooks, permissions, env, MCP servers), tracking ownership via inventory tags + glob claims, and reconciling all three sources of truth in one pass.

Source

github.com/zot24/zskills · MIT license · v0.5+

Commands reference

Full reference for every zskills subcommand. Flags are shown with their defaults. Run zskills <cmd> --help for the up-to-date help.

Conventions

  • <name> accepts the unqualified skill name (e.g. servarr) when unambiguous, or name@marketplace (e.g. servarr@zot24-skills) when multiple marketplaces declare the same skill. This matches Claude Code’s own syntax.
  • Most commands print colored output. Set NO_COLOR=1 to disable, or pipe through cat if you need plain text.
  • CLAUDE_HOME=/custom/path overrides ~/.claude for testing.
  • XDG_CONFIG_HOME and XDG_CACHE_HOME are respected for manifest/cache locations.

list

What’s currently installed, with each item’s enabled/disabled/orphaned status. Covers both Claude Code plugins and Agent Skills.

zskills list [--json]
FlagDefaultDescription
--jsonoffEmit a machine-readable JSON document for scripting

The non-JSON output groups results into four plugin buckets (active, installed-but-disabled, enabled-but-not-installed, installed-from-missing-marketplace) plus two Agent Skill buckets (managed by zskills, on-disk-but-untracked).

install

Flip a plugin’s enabledPlugins entry on. Claude Code fetches bytes on next start (or run /plugin install <name> inside Claude Code to materialize them immediately).

zskills install <name>...

Accepts multiple names. Resolves unqualified names against your registered marketplaces; errors on ambiguity.

remove / purge

remove is apt-style: disable in enabledPlugins and drop the inventory entry, but leave bytes on disk so re-enabling is instant. purge does the same plus deletes the bytes from ~/.claude/plugins/cache/.

zskills remove <name>...
zskills purge  <name>...

enable / disable

Flip a plugin’s enabledPlugins flag without (un)installing.

zskills enable  <name>...
zskills disable <name>...

enable is a no-op if the plugin isn’t installed (Claude Code will install it on next start). disable keeps bytes and inventory; use purge to wipe completely.

sync (headline command)

Apply a declarative skills.toml manifest. Diffs intent against current state, then atomically writes the necessary settings.json and inventory changes.

zskills sync [--file <path>] [--dry-run] [--prune]
FlagDefaultDescription
--file <path>$XDG_CONFIG_HOME/zskills/skills.toml (then ~/.config/zskills/skills.toml)Path to skills.toml. ./skills.toml is NOT auto-loaded — pass --file ./skills.toml to use a project-local manifest. (This caused destructive surprises in v0.5; the v0.5.1 default is safer.)
--dry-runoffPrint the plan; do not write
--pruneoffAllow destructive removals. Without --prune, agent skills present on disk but absent from the manifest are reported as skip and left untouched. With --prune, their bytes are deleted from ~/.claude/skills/.

What sync does:

  1. For each [[skills]] entry: resolve name@marketplace, write to enabledPlugins. Entries currently enabled but not in the manifest get flipped off.
  2. For each [[agent_skills]] entry: if source is present, clone/pull and copy skills/<name>/ to ~/.claude/skills/. If npm is present, run npm install -g <pkg> (or install_cmd), then claim all matching claims globs. If neither is present (just name), register the existing on-disk skill in inventory without fetching anything.
  3. Agent skills tracked in inventory but missing from the manifest are reported. With --prune they’re deleted; without, they’re skipped.

Sync is idempotent. Run it on every fresh machine to reproduce your global state from a single file.

If a ./skills.toml exists in CWD when you run sync without --file, zskills prints a yellow warning telling you it’s being ignored — pass --file ./skills.toml if that’s actually what you wanted.

upgrade

The one command for refreshing everything zskills manages — marketplaces, git agent skills, and npm agent skills.

zskills upgrade [<name>...]
Source kindWhat upgrade does
Plugins (marketplace-based)For each registered marketplace tap, git pull --ff-only if it’s a git working tree; otherwise fetch the GitHub archive tarball from the source recorded in known_marketplaces.json and atomically swap the tree. Claude Code picks up new plugin versions on next start.
Git agent skills (source = "owner/repo")git pull the cached source clone + re-copy bytes
npm agent skills (npm = "pkg")Run npm install -g <pkg> (or install_cmd), then re-apply the claims glob to retag inventory

Pass specific names to upgrade just those; empty = upgrade everything. The name filter matches against the manifest’s npm, source, or name fields.

Agent skills manifest schema

The [[agent_skills]] table supports four orthogonal fields, used in combination:

# Git-sourced single skill
[[agent_skills]]
source = "jakubkrehel/make-interfaces-feel-better"

# Git-sourced multi-skill repo, install just one
[[agent_skills]]
source = "owner/multi-skill-repo"
name = "specific-skill"

# npm-distributed package
[[agent_skills]]
npm = "get-shit-done-cc"
claims = ["gsd-*"]              # glob patterns; every match in ~/.claude/skills/ is owned by this entry

# npm with custom installer command
[[agent_skills]]
npm = "some-tool"
install_cmd = "npx some-tool setup"

# Local-only (just tracked, never refreshed from a remote)
[[agent_skills]]
name = "my-internal-tool"
FieldPurpose
sourceGit source: owner/repo (GitHub) or full git URL. sync/upgrade clone/pull and copy.
npmnpm package name. sync/upgrade runs npm install -g <pkg>.
install_cmdCustom installer command — overrides the default npm install -g. Used for packages with their own setup CLI.
nameOptional. For source entries, pick a single skill out of a multi-skill repo. For local-only entries, required — names the on-disk skill to track.
claimsGlob patterns (e.g., ["gsd-*"]) matched against ~/.claude/skills/. After install, every match is tagged with this entry’s source. Used for npm packages whose installer touches pre-existing directories — so the diff-after-install discovers nothing, but claims retroactively claims ownership.

update

Refresh every registered marketplace’s git cache (or just the named one).

zskills update [<marketplace-name>]

Runs git pull --ff-only against each marketplace clone. Claude Code reads the cache on next start to detect new versions.

doctor

Reconcile disk ↔ inventory ↔ settings. Reports drift in three categories:

  1. Plugins enabled in enabledPlugins but not present in installed_plugins.json (broken references — Claude Code will fetch on next start).
  2. Plugins in inventory whose marketplace tap is no longer registered.
  3. Agent skills tracked in inventory but missing from ~/.claude/skills/ on disk.
zskills doctor [--fix]

With --fix, dangling references are removed (settings.json entries pointing nowhere, orphaned inventory entries). --fix never deletes installed bytes — that’s what purge is for.

scan

Walk a directory tree looking for project-scope skills.

zskills scan [<path>] [--depth N] [--json]
FlagDefaultDescription
<path>.Tree to walk
--depth N6Maximum directory recursion depth
--jsonoffMachine-readable output

Detects two patterns:

  • .claude/settings.json or .claude/settings.local.json with enabledPlugins / extraKnownMarketplaces (project-scope plugin enables)
  • .claude/skills/<name>/SKILL.md (project-scope Agent Skills)

The default depth of 6 catches both patterns from a ~/Desktop/code-style parent directory (Agent Skills are at depth 5 from the project root).

migrate

Promote ONE project’s enabled plugins and project-scope Agent Skills to user scope.

zskills migrate <project> [--remove-from-project] [--dry-run]

Reads <project>/.claude/settings.json (or settings.local.json) and <project>/.claude/skills/<name>/. Writes promoted plugin enables into ~/.claude/settings.json’s enabledPlugins and copies Agent Skill directories into ~/.claude/skills/.

--remove-from-project clears enabledPlugins, extraKnownMarketplaces, and .claude/skills/ from the project after a successful promote.

migrate-skill

Promote ONE Agent Skill that appears in many projects across the tree, in a single operation.

zskills migrate-skill <name> [--root <dir>] [--source <ref>]
                              [--remove-from-all] [--dry-run]
FlagDefaultDescription
--root <dir>.Tree to search
--source <ref>none (local-only)If set, install from upstream (owner/repo or git URL) instead of copying canonical
--remove-from-alloffDelete the skill’s .claude/skills/<name>/ from every matched project
--dry-runoffPrint the plan; do not write

For each matched project, the skill’s directory is hashed and compared. If content diverges, the first project (alphabetical) wins as canonical and a warning is printed showing which projects have which hash. The promoted skill gets a [[agent_skills]] entry appended to your skills.toml so the migration is reproducible.

migrate-all

Interactive sweep across a tree.

zskills migrate-all <dir> [--threshold N] [--yes] [--dry-run]
FlagDefaultDescription
<dir>requiredTree to walk
--threshold N2Only consider skills appearing in ≥N projects
--yes, -yoffSkip prompts; accept defaults (no source, keep project copies)
--dry-runoffPrint planned action per skill; do not write

For each duplicated skill above the threshold, prompts:

  1. “Promote ‘’ to user scope? [Y/n]”
  2. “Upstream source [owner/repo, URL, or blank for local-only]:”
  3. “Remove project copies from N project(s)? [y/N]”

Calls migrate-skill under the hood for each accepted prompt.

marketplace

Tap management — register, list, refresh, and remove Claude Code marketplaces.

zskills marketplace add <owner/repo | git-url>
zskills marketplace remove <name>
zskills marketplace list [--json]
zskills marketplace update [<name>]

add clones the marketplace repo into ~/.claude/plugins/marketplaces/<name>/ and writes both known_marketplaces.json and settings.json’s extraKnownMarketplaces. Mirrors what /plugin marketplace add does inside Claude Code.

Use cases

Pragmatic recipes for common workflows. Each section assumes zskills is installed and on PATH.

1. Bootstrap a fresh machine

cargo install --git https://github.com/zot24/zskills

# Copy your manifest from the old machine (or your dotfiles repo)
mkdir -p ~/.config/zskills
scp old-machine:~/.config/zskills/skills.toml ~/.config/zskills/

# Add the marketplaces named in the manifest
zskills marketplace add zot24/skills
zskills marketplace add cloudflare/skills
# (claude-plugins-official is registered automatically by Claude Code)

# Apply
zskills sync

After sync, restart Claude Code so it fetches the bytes for the flagged plugins. zskills doctor should report clean.

2. Add a new skill globally

Two options. Use whichever matches how you think.

Imperative — flip on now, document later:

zskills install firecrawl@zot24-skills

Declarative — edit the manifest, run sync:

# ~/.config/zskills/skills.toml
[[skills]]
name = "firecrawl"
marketplace = "zot24-skills"
zskills sync

The declarative path is reproducible across machines; the imperative path is what you reach for in a one-off shell.

3. Mirror an Agent Skill from a GitHub repo

[[agent_skills]]
source = "jakubkrehel/make-interfaces-feel-better"
zskills sync

sync clones (or pulls) github.com/jakubkrehel/make-interfaces-feel-better.git into $XDG_CACHE_HOME/zskills/agent-skills/, then copies every directory under skills/<name>/SKILL.md into ~/.claude/skills/<name>/. To pin just one skill out of a multi-skill repo, add name = "specific-skill".

4. Centralize duplicate skills scattered across projects

You have the same performance-tracking-skill directory under .claude/skills/ in 8 projects, and you want it at user scope so every project gets it for free.

Inspect first:

zskills scan ~/Desktop/code | grep -A1 'Skill → projects'

Promote one specifically (preview, then apply):

zskills migrate-skill performance-tracking-skill --root ~/Desktop/code --dry-run
zskills migrate-skill performance-tracking-skill --root ~/Desktop/code --remove-from-all

migrate-skill hashes each project’s copy; if content has diverged it warns and picks the first as canonical so you can stop and inspect manually. The skill ends up at ~/.claude/skills/performance-tracking-skill/, an entry is appended to your skills.toml, and (with --remove-from-all) every project’s copy is deleted.

5. Interactive sweep across many projects

For a one-shot cleanup of dozens of duplicated skills:

zskills migrate-all ~/Desktop/code --threshold 3

Walks the tree, groups by skill name, only considers skills in ≥3 projects, then prompts per skill. For each accepted prompt it asks for an upstream source (or blank for local-only) and whether to clean project copies.

For batch-promotion without interactivity (accepts defaults: no source, keep project copies):

zskills migrate-all ~/Desktop/code --threshold 5 -y

6. Track a project-scope skill you can’t move yet

Sometimes a skill is genuinely project-specific (e.g., the project’s own ops runbook). You don’t want it at user scope, but you do want it tracked. Currently the manifest is user-scope-only. Two reasonable patterns:

Pattern A: Keep the project copy in version control. Check .claude/skills/<name>/ into the project’s git repo. Don’t add it to ~/.config/zskills/skills.toml. The project carries its own skill; teammates get it on git clone.

Pattern B: Put a project-scope skills.toml in the project root. zskills sync (run from inside the project) auto-discovers ./skills.toml before falling back to ~/.config/zskills/. So a project can carry its own intent for what should be globally enabled when working on it.

7. Adopt a multi-skill npm package (e.g., get-shit-done-cc)

Some skill bundles ship as npm packages whose post-install hook writes many skill directories under ~/.claude/skills/. zskills owns them via a npm + claims declaration:

[[agent_skills]]
npm = "get-shit-done-cc"
claims = ["gsd-*"]               # any name matching this glob is owned by this entry

After zskills sync, all matching ~/.claude/skills/gsd-*/ directories are tagged source: "npm:get-shit-done-cc" in inventory. zskills list groups them under one line:

✓ get-shit-done-cc (66 skills)  ← npm
    gsd-add-tests, gsd-ai-integration-phase, … [-v to list all 66]

zskills upgrade will run npm update -g get-shit-done-cc and re-claim, keeping the bundle current.

If the npm package needs a custom setup command (some packages have a separate CLI to actually place files), use install_cmd:

[[agent_skills]]
npm = "some-tool"
install_cmd = "npx some-tool install"
claims = ["sometool-*"]

8. One command, refresh everything

zskills upgrade

That single command:

  • git pull (or tarball fetch) every marketplace tap, so Claude Code sees the latest plugin versions
  • git pull every git-sourced agent skill and re-copy bytes
  • npm update -g every npm-sourced agent skill (and re-claim via claims globs)

Pass names to limit scope: zskills upgrade get-shit-done-cc zot24-skills.

9. Diagnose drift

zskills doctor

If something’s amiss:

  • “enabled but NOT installed (broken)” — Claude Code knows about the plugin via enabledPlugins, but the bytes aren’t on disk. Restart Claude Code (it’ll install on startup), or run /plugin install <name>@<mp> inside Claude Code. If you don’t want it anymore, zskills doctor --fix removes the flag.
  • “installed from a marketplace that’s no longer registered” — you marketplace remove-d a tap but plugins from it are still in the inventory. Run zskills purge <name> to clean.
  • “agent skill tracked in inventory but missing on disk” — someone deleted ~/.claude/skills/<name>/ manually. zskills doctor --fix removes the stale inventory entry, or re-run sync to reinstall.

10. Reproduce someone else’s setup

Ask them for their skills.toml. Drop it in ~/.config/zskills/skills.toml. Register any marketplaces they use (zskills marketplace add owner/repo). Run zskills sync. Done.

11. Promote project skills + remove the project’s .claude/skills/

zskills migrate ~/Desktop/code/some-project --remove-from-project

Moves both enabledPlugins entries from the project’s .claude/settings.json (or settings.local.json) AND every directory under .claude/skills/ into user scope, then clears the project copies. Useful when you’ve decided “these are clearly global tools — they shouldn’t be vendored per-project.”

12. Vendor a global skill INTO a project (rare; manual)

zskills doesn’t push this direction yet (user-scope → project-scope). If you need a particular project to pin an exact version of a skill that diverges from the global one, copy ~/.claude/skills/<name>/ into the project’s .claude/skills/<name>/ and commit it. Claude Code resolves project scope before user scope, so the project’s pinned copy wins.

Architecture

How zskills models the three sources of truth in your Claude Code install and reconciles between them.

The three states

StateLives atAuthoritative for
Intentskills.tomlWhat you want installed and enabled
Inventory~/.claude/plugins/installed_plugins.json + ~/.claude/skills/.zskills.jsonWhat exists on disk
Activation~/.claude/settings.jsonenabledPluginsWhat’s currently running in a Claude Code session

The first is what zskills writes from. The second and third are what Claude Code reads. zskills’ job is to keep all three consistent.

Two ecosystems

Claude Code has two parallel skill systems that we manage from one manifest:

Claude Code plugins

  • Distributed via marketplaces (Git repos with a .claude-plugin/marketplace.json)
  • Installed under ~/.claude/plugins/cache/<marketplace>/<name>/<version>/
  • Activation toggle in settings.jsonenabledPlugins
  • Inventory: ~/.claude/plugins/installed_plugins.json
  • Qualified name: <plugin>@<marketplace> (matches Claude Code’s syntax)

Agent Skills (the older raw-SKILL.md format)

  • No marketplace — direct from any Git repo with skills/<name>/SKILL.md
  • Installed under ~/.claude/skills/<name>/
  • No “enabled” flag — files-on-disk is the activation
  • Inventory: ~/.claude/skills/.zskills.json (we own this; Claude Code doesn’t write it)

Both kinds are declared in skills.toml:

[[skills]]                          # plugin
name = "umbrel-app"
marketplace = "zot24-skills"

[[agent_skills]]                    # agent skill from a repo
source = "owner/repo"

[[agent_skills]]                    # local-only agent skill
name = "my-internal-tool"           # name required; source omitted = no remote refresh

Atomic JSON writes

Claude Code’s settings.json and installed_plugins.json carry more than the keys zskills cares about: hooks, permissions, env vars, MCP servers, etc. Losing those on round-trip would be catastrophic. So every write goes through:

  1. Read full document as serde_json::Map<String, Value> (preserves unknown keys).
  2. Mutate only the keys zskills owns (enabledPlugins, extraKnownMarketplaces, plugins.*).
  3. Serialize the entire map back to a temp file in the same directory.
  4. std::fs::rename to the target path (atomic on Unix).

Same idea for skills.toml writes: we use toml_edit::DocumentMut so existing comments, blank lines, and table ordering survive. An append-only writer (manifest::append_agent_skill) checks for duplicates by exact (source, name) match before inserting.

How sync reconciles

            ┌─────────────────┐
            │   skills.toml   │  ← you edit this
            └────────┬────────┘
                     │
                     ▼
            ┌─────────────────┐
            │     Manifest    │  parsed model
            └────────┬────────┘
                     │
        ┌────────────┴────────────┐
        │ resolve name@marketplace │
        │ via known_marketplaces  │
        └────────┬────────────────┘
                 ▼
        ┌────────────────┐    ┌─────────────────────┐
        │ desired_plugins│    │ desired_agent_skills│
        └───────┬────────┘    └──────────┬──────────┘
                │                        │
                ▼                        ▼
       ┌────────────────┐       ┌────────────────┐
       │ current_plugins│       │ current_agent  │  (from inventory + disk)
       │ from settings  │       │ skills         │
       └────────┬───────┘       └────────┬───────┘
                │                        │
                ▼                        ▼
              DIFF                     DIFF
                │                        │
       ┌────────┴───────┐       ┌────────┴────────┐
       │ enable / disable│       │ install / remove│
       │ via settings.json│      │ via git + copy  │
       └─────────────────┘       └─────────────────┘

sync is a single atomic apply: it computes the full plan first, prints it, and (unless --dry-run) applies the diff. The settings.json write happens once at the end, not per-skill.

Doctor’s three reconciliations

┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  settings    │◀──▶│   inventory  │◀──▶│     disk     │
│ enabledPlugins│    │  json files  │    │  ~/.claude/  │
└──────────────┘    └──────────────┘    └──────────────┘
       │                    │                   │
       └────────────────────┴───────────────────┘
                            │
                         doctor

Three failure modes:

  1. Settings says enabled, inventory says nothing — broken reference. Claude Code’s startup install will fix this on next launch, OR doctor --fix removes the flag.
  2. Inventory says installed, marketplace gone — orphan from a marketplace remove. doctor reports; purge cleans.
  3. Agent skill inventory entry, no bytes on disk — someone rm -rf’d the skill manually. doctor --fix drops the inventory entry; sync would reinstall from manifest.

Doctor never deletes plugin bytes. That’s purge’s job — a deliberate, explicit operation.

Marketplace update strategies

Marketplaces installed via Claude Code can be either git working trees or unpacked tarballs (the latter is how claude-plugins-official ships, with a .gcs-sha cache marker file). zskills upgrade handles both:

                        ┌─ is .git/ present?
                        │
                  ┌─────┴──────┐
                yes            no
                  │            │
            git pull         resolve source from known_marketplaces.json
                                  │
                          ┌───────┴────────┐
                  github source        other
                          │                │
            fetch archive tarball       error / skip
            from GitHub HEAD branch
            (falls back to main, master)
                          │
            extract to sibling staging dir
                          │
            atomic-ish rename swap (backup → rename → cleanup)

The tarball path uses reqwest blocking + flate2 + tar. Result: every marketplace recorded in known_marketplaces.json is updatable from one command regardless of how Claude Code originally installed it.

Ownership tracking for agent skills

Agent skill inventory entries carry a source field that’s typed by prefix:

Inventory sourceMeansRefresh via
owner/repo (or git URL)Git-cloned sourcegit pull cached clone + re-copy
npm:<pkg>npm-installednpm install -g <pkg>
localLocal-only, never refreshed(manual)

The manifest entry’s claims glob list bridges the gap when an npm package overwrites files in-place: after the install command runs, every ~/.claude/skills/<name>/ directory matching any glob in claims is tagged with the entry’s source. This is the only way to claim “I own these 66 pre-existing directories” without re-installing fresh.

sync uses the same ownership signals (source match + npm tag + claims glob) when deciding whether a skill not in the desired set should be removed — preventing accidental deletion of skills owned by a [[agent_skills]] entry.

Why git is shelled out

zskills runs git clone --depth 1 and git pull --ff-only via std::process::Command instead of linking libgit2. Reasons:

  • Reuses your existing credential helpers (SSH keys, gh CLI, OS keychain) for free.
  • Smaller binary (no libgit2 dependency).
  • Sparse-checkout and partial-clone work correctly without bespoke handling.
  • Errors come back as plain git output, which is what users already know how to read.

The trade-off is one process spawn per fetch, which is fine: marketplace updates and agent-skill installs are rare events.

Path resolution

~/.claude/                              ($CLAUDE_HOME)
├── settings.json                       activation
├── plugins/
│   ├── installed_plugins.json          plugin inventory
│   ├── known_marketplaces.json         tap registry
│   ├── marketplaces/<name>/            tap clones
│   └── cache/<mp>/<name>/<v>/          plugin bytes
└── skills/
    ├── .zskills.json                   agent skill inventory
    └── <name>/SKILL.md                 agent skill bytes

$XDG_CACHE_HOME/zskills/agent-skills/<owner>-<repo>/  source clones
$XDG_CONFIG_HOME/zskills/skills.toml                  manifest (fallback)
./skills.toml                                         manifest (project-scope wins)

CLAUDE_HOME env var overrides ~/.claude. XDG_CACHE_HOME and XDG_CONFIG_HOME are respected. On macOS, zskills uses ~/.config/zskills/ for the manifest by default (matching cargo/starship/atuin) rather than the platform’s ~/Library/Application Support/ location.

Troubleshooting

“no skills.toml found”

Error: no skills.toml found (looked in ./ and ~/.config/zskills/)

zskills looks for the manifest in this order:

  1. $PWD/skills.toml (if you’re inside a project that vendors a manifest)
  2. $XDG_CONFIG_HOME/zskills/skills.toml
  3. ~/.config/zskills/skills.toml
  4. The platform default from dirs::config_dir() (~/Library/Application Support/zskills/skills.toml on macOS)

Fix: create the file at ~/.config/zskills/skills.toml, or pass --file <path> explicitly.

“skill X is ambiguous — qualify with @marketplace”

Two registered marketplaces both expose a skill with the same name. zskills can’t pick one. Qualify it:

zskills install firecrawl@zot24-skills        # instead of just firecrawl

“enabled but NOT installed (broken)”

doctor is flagging an enabledPlugins entry that has no corresponding inventory record. Three legitimate causes:

  1. You just ran sync and haven’t restarted Claude Code yet. This is the normal post-sync state. Restart Claude Code; it’ll fetch the bytes on startup. Doctor will go clean.
  2. The plugin was removed from its marketplace upstream. Pick a replacement or zskills disable <name> to silence the warning.
  3. The marketplace tap was unregistered (marketplace remove). zskills doctor --fix will drop the orphan reference from enabledPlugins.

Sync deleted agent skills I didn’t expect to lose

A v0.5 incident: running zskills sync inside a repo that ships its own skills.toml (like the zot24/skills marketplace) destructively re-applied that manifest against your user-scope state. v0.5.1+ defaults prevent this:

  1. ./skills.toml no longer auto-loads. Sync without --file uses only ~/.config/zskills/skills.toml. If ./skills.toml exists, sync prints a yellow warning pointing at it.
  2. sync never deletes bytes by default. Removal of agent skills no longer in the manifest requires --prune. Without it, they’re reported as skip and left intact.

If you lost a skill before these defaults, check whether the skill was committed in any project under your tree:

find ~/Desktop/code -path '*/.claude/skills/<name>/SKILL.md' 2>/dev/null
# Or via git history (any project that had it)
for p in ~/Desktop/code/*; do
  [ -d "$p/.git" ] || continue
  sha=$(git -C "$p" log --all --oneline -- ".claude/skills/<name>/SKILL.md" | head -1 | awk '{print $1}')
  [ -n "$sha" ] && echo "$p: $sha"
done

Then restore via git checkout <sha> -- .claude/skills/<name>/ in the relevant project and cp -R to ~/.claude/skills/.

Sync wants to disable plugins I want to keep

If your skills.toml doesn’t list a plugin, sync will flip it off because the manifest is declarative — it represents your complete intent. Two ways to handle:

A) Add the plugin to your manifest so sync stops touching it:

[[skills]]
name = "rust-analyzer-lsp"
marketplace = "claude-plugins-official"

B) Edit ~/.claude/settings.json to remove the unwanted enabledPlugins entry, so it no longer needs an enable=false flip on every sync.

(A sync --no-prune flag is on the roadmap for users who prefer additive-only behavior; for now, declare everything you want.)

migrate-skill says “content differs across projects”

Different projects have edited their copy of the same-named skill, so the bytes are no longer identical. zskills hashes each project’s SKILL.md tree and groups by hash:

! content differs across projects — using the first as canonical:
  [4e483861]  7 project(s)
    /path/to/project-a
    /path/to/project-b
    ...
  [5f9d37fb]  1 project(s)
    /path/to/project-z

The first project (alphabetical) wins as canonical. If you want a different project’s version to win, either:

  • Re-run migrate-skill from inside that project first, OR
  • Manually copy the desired version to ~/.claude/skills/<name>/ before migrate-skill (which will detect it as “already at user scope” and overwrite with canonical only if you proceed — so cancel first).

A --canonical <project-path> flag is reasonable for v0.4 if this becomes common pain.

npm agent skill says “no new skills discovered”

Some npm packages place their skill files via a separate setup CLI (e.g., npx <pkg> install), not via npm’s own postinstall hook. If npm install -g <pkg> alone doesn’t write to ~/.claude/skills/, the diff-before-after returns empty and zskills sees nothing to claim.

Two fixes:

  1. Add a claims glob so zskills retroactively claims pre-existing directories that match:
    [[agent_skills]]
    npm = "get-shit-done-cc"
    claims = ["gsd-*"]
    
  2. Set install_cmd to whatever the package’s actual installer is:
    [[agent_skills]]
    npm = "some-tool"
    install_cmd = "npx some-tool install"
    

If you’re not sure where a package writes its skills, run it once manually, then check ~/.claude/skills/ and pick a claims pattern that covers them.

sync clones repeatedly / is slow on Agent Skills

The first sync clones every [[agent_skills]] source repo into ~/.cache/zskills/agent-skills/. Subsequent syncs do git pull --ff-only against the cache — fast. If you’re seeing repeated full clones, check that the cache directory exists and is writable:

ls -la ~/.cache/zskills/agent-skills/

You can wipe and rebuild the cache safely; it’s reproducible:

rm -rf ~/.cache/zskills
zskills sync

Plugin bytes seem stale / not reflecting upstream

Marketplace caches need an explicit refresh:

zskills marketplace update              # all marketplaces
zskills marketplace update zot24-skills # one

Restart Claude Code afterward so it picks up the new versions.

Manifest entries vanished after I edited skills.toml manually

zskills writes via toml_edit and never deletes user content. If entries are gone, check:

  • Did you save the file?
  • Are you editing the right one? zskills sync --dry-run prints the manifest path at the top.
  • Did git checkout or another tool overwrite it?

“agent skill in inventory, missing on disk”

You deleted ~/.claude/skills/<name>/ manually. Two ways to recover:

# Re-fetch from upstream (if there's a source in the manifest)
zskills sync

# Or just drop the inventory entry
zskills doctor --fix

Cargo install fails with “edition2024 not stabilized”

You’re on Rust < 1.85. zskills requires Rust 1.85 or newer (transitive idna_adapter dep). Update:

rustup update stable
cargo install --git https://github.com/zot24/zskills --force

How do I uninstall zskills entirely?

cargo uninstall zskills
rm -rf ~/.cache/zskills
# Manifest stays — it's just a config file. Delete if you don't want it:
rm ~/.config/zskills/skills.toml

# Agent skill inventory stays too — Claude Code itself doesn't read it, but if
# you reinstall zskills later it'll resume from this state. Delete if you want
# a clean slate:
rm ~/.claude/skills/.zskills.json

Plugins and Agent Skills installed via zskills remain in ~/.claude/ — they’re managed by Claude Code’s own machinery, not zskills. Uninstall those via Claude Code’s /plugin uninstall or by deleting the relevant directories.

Changelog

All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. Releases from this point forward are managed by release-please based on Conventional Commits.

0.5.0 (2026-05-13)

Features

  • tarball update for non-git marketplaces (db6e370)
  • v0.5 — upgrade command, npm sources, grouped list (fb37468)

Bug Fixes

  • claims field + quiet git output + skip non-git marketplaces (b424711)

0.4.0 (2026-05-12)

Features

  • initial v0.1 — package manager for Claude Code skills (c03fcea)
  • v0.2 — Agent Skills support (~/.claude/skills/) (fcd7773)
  • v0.3 — migrate-skill, migrate-all, optional source (d4144d9)

Bug Fixes

  • manifest: use XDG ~/.config across platforms, not platform default (25e9b10)

Documentation

  • release-please + CHANGELOG + docs/ folder (bdb002c)

0.3.0 - 2026-05-12

Features

  • migrate-skill: promote ONE agent skill across every project under a tree. Hashes each project’s copy to detect content divergence, picks the first as canonical, copies to user scope, optionally removes from all projects, appends a [[agent_skills]] entry to the manifest.
  • migrate-all: interactive sweep over a tree. Groups by skill name, sorts by occurrence count, prompts per skill (promote? source? remove from projects?). --threshold N filters; -y/--yes accepts defaults.
  • Optional source on [[agent_skills]] entries. A name-only entry declares a local-only skill: tracked in inventory but not refreshed from a remote by sync.
  • Manifest writes preserve formatting: append uses toml_edit::DocumentMut so existing comments/structure in skills.toml survive round-trip.

Internal

  • Added dialoguer for interactive prompts.
  • 13/13 integration tests passing, including new coverage for migrate-skill.

0.2.0 - 2026-05-12

Features

  • Agent Skills support (raw SKILL.md format under ~/.claude/skills/). New [[agent_skills]] manifest section with source (owner/repo or git URL) and optional name.
  • Source repos cached at $XDG_CACHE_HOME/zskills/agent-skills/<owner>-<repo>/.
  • Own inventory at ~/.claude/skills/.zskills.json (since Claude Code’s installed_plugins.json doesn’t cover Agent Skills).
  • sync applies both [[skills]] and [[agent_skills]] in a single pass.
  • list shows plugins AND agent skills; flags untracked agent skills.
  • doctor detects orphans across all three states (settings, inventory, disk).
  • scan walks .claude/skills/<name>/SKILL.md directories at project scope (default depth bumped 4 → 6).
  • migrate also promotes .claude/skills/ directories to user scope.

0.1.0 - 2026-05-12

Initial release — package manager for Claude Code plugins.

Features

  • Commands: list, install, remove, purge, enable, disable, sync, update, doctor, scan, migrate, marketplace add|remove|list|update.
  • Atomic JSON round-trip preserves all unknown fields in ~/.claude/settings.json (hooks, permissions, env, etc.).
  • Multi-marketplace support with name@marketplace qualification matching Claude Code’s syntax.
  • Declarative skills.toml manifest auto-discovered from CWD or ~/.config/zskills/.
  • Scan + migrate for promoting project-scope skills to user scope.
  • Git shelled out (no libgit2 bundling); rustls TLS; single static binary.
  • 8 integration tests using assert_cmd + tempfile-isolated CLAUDE_HOME.