zskills
A package manager for Claude Code skills. Declarative install, multi-marketplace, written in Rust. Manages both Claude Code plugins (via marketplaces, settings.json → enabledPlugins) 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, orname@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=1to disable, or pipe throughcatif you need plain text. CLAUDE_HOME=/custom/pathoverrides~/.claudefor testing.XDG_CONFIG_HOMEandXDG_CACHE_HOMEare 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]
| Flag | Default | Description |
|---|---|---|
--json | off | Emit 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]
| Flag | Default | Description |
|---|---|---|
--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-run | off | Print the plan; do not write |
--prune | off | Allow 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:
- For each
[[skills]]entry: resolvename@marketplace, write toenabledPlugins. Entries currently enabled but not in the manifest get flipped off. - For each
[[agent_skills]]entry: ifsourceis present, clone/pull and copyskills/<name>/to~/.claude/skills/. Ifnpmis present, runnpm install -g <pkg>(orinstall_cmd), then claim all matchingclaimsglobs. If neither is present (justname), register the existing on-disk skill in inventory without fetching anything. - Agent skills tracked in inventory but missing from the manifest are reported. With
--prunethey’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 kind | What 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"
| Field | Purpose |
|---|---|
source | Git source: owner/repo (GitHub) or full git URL. sync/upgrade clone/pull and copy. |
npm | npm package name. sync/upgrade runs npm install -g <pkg>. |
install_cmd | Custom installer command — overrides the default npm install -g. Used for packages with their own setup CLI. |
name | Optional. 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. |
claims | Glob 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:
- Plugins enabled in
enabledPluginsbut not present ininstalled_plugins.json(broken references — Claude Code will fetch on next start). - Plugins in inventory whose marketplace tap is no longer registered.
- 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]
| Flag | Default | Description |
|---|---|---|
<path> | . | Tree to walk |
--depth N | 6 | Maximum directory recursion depth |
--json | off | Machine-readable output |
Detects two patterns:
.claude/settings.jsonor.claude/settings.local.jsonwithenabledPlugins/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]
| Flag | Default | Description |
|---|---|---|
--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-all | off | Delete the skill’s .claude/skills/<name>/ from every matched project |
--dry-run | off | Print 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]
| Flag | Default | Description |
|---|---|---|
<dir> | required | Tree to walk |
--threshold N | 2 | Only consider skills appearing in ≥N projects |
--yes, -y | off | Skip prompts; accept defaults (no source, keep project copies) |
--dry-run | off | Print planned action per skill; do not write |
For each duplicated skill above the threshold, prompts:
- “Promote ‘
’ to user scope? [Y/n]” - “Upstream source [owner/repo, URL, or blank for local-only]:”
- “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 versionsgit pullevery git-sourced agent skill and re-copy bytesnpm update -gevery npm-sourced agent skill (and re-claim viaclaimsglobs)
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 --fixremoves 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. Runzskills purge <name>to clean. - “agent skill tracked in inventory but missing on disk” — someone deleted
~/.claude/skills/<name>/manually.zskills doctor --fixremoves the stale inventory entry, or re-runsyncto 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
| State | Lives at | Authoritative for |
|---|---|---|
| Intent | skills.toml | What you want installed and enabled |
| Inventory | ~/.claude/plugins/installed_plugins.json + ~/.claude/skills/.zskills.json | What exists on disk |
| Activation | ~/.claude/settings.json → enabledPlugins | What’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.json→enabledPlugins - 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:
- Read full document as
serde_json::Map<String, Value>(preserves unknown keys). - Mutate only the keys zskills owns (
enabledPlugins,extraKnownMarketplaces,plugins.*). - Serialize the entire map back to a temp file in the same directory.
std::fs::renameto 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:
- Settings says enabled, inventory says nothing — broken reference. Claude Code’s startup install will fix this on next launch, OR
doctor --fixremoves the flag. - Inventory says installed, marketplace gone — orphan from a
marketplace remove.doctorreports;purgecleans. - Agent skill inventory entry, no bytes on disk — someone
rm -rf’d the skill manually.doctor --fixdrops the inventory entry;syncwould 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 source | Means | Refresh via |
|---|---|---|
owner/repo (or git URL) | Git-cloned source | git pull cached clone + re-copy |
npm:<pkg> | npm-installed | npm install -g <pkg> |
local | Local-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:
$PWD/skills.toml(if you’re inside a project that vendors a manifest)$XDG_CONFIG_HOME/zskills/skills.toml~/.config/zskills/skills.toml- The platform default from
dirs::config_dir()(~/Library/Application Support/zskills/skills.tomlon 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:
- You just ran
syncand 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. - The plugin was removed from its marketplace upstream. Pick a replacement or
zskills disable <name>to silence the warning. - The marketplace tap was unregistered (
marketplace remove).zskills doctor --fixwill drop the orphan reference fromenabledPlugins.
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:
./skills.tomlno longer auto-loads. Sync without--fileuses only~/.config/zskills/skills.toml. If./skills.tomlexists, sync prints a yellow warning pointing at it.syncnever deletes bytes by default. Removal of agent skills no longer in the manifest requires--prune. Without it, they’re reported asskipand 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-skillfrom inside that project first, OR - Manually copy the desired version to
~/.claude/skills/<name>/beforemigrate-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:
- Add a
claimsglob so zskills retroactively claims pre-existing directories that match:[[agent_skills]] npm = "get-shit-done-cc" claims = ["gsd-*"] - Set
install_cmdto 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-runprints the manifest path at the top. - Did
git checkoutor 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 Nfilters;-y/--yesaccepts defaults. - Optional source on
[[agent_skills]]entries. Aname-only entry declares a local-only skill: tracked in inventory but not refreshed from a remote bysync. - Manifest writes preserve formatting: append uses
toml_edit::DocumentMutso existing comments/structure inskills.tomlsurvive round-trip.
Internal
- Added
dialoguerfor 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.mdformat under~/.claude/skills/). New[[agent_skills]]manifest section withsource(owner/repo or git URL) and optionalname. - Source repos cached at
$XDG_CACHE_HOME/zskills/agent-skills/<owner>-<repo>/. - Own inventory at
~/.claude/skills/.zskills.json(since Claude Code’sinstalled_plugins.jsondoesn’t cover Agent Skills). syncapplies both[[skills]]and[[agent_skills]]in a single pass.listshows plugins AND agent skills; flags untracked agent skills.doctordetects orphans across all three states (settings, inventory, disk).scanwalks.claude/skills/<name>/SKILL.mddirectories at project scope (default depth bumped 4 → 6).migratealso 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@marketplacequalification matching Claude Code’s syntax. - Declarative
skills.tomlmanifest auto-discovered from CWD or~/.config/zskills/. - Scan + migrate for promoting project-scope skills to user scope.
- Git shelled out (no
libgit2bundling); rustls TLS; single static binary. - 8 integration tests using
assert_cmd+tempfile-isolatedCLAUDE_HOME.