skills-syncer
v0.9.0
Published
Vendor Claude Code skills and subagents from any catalog repo into your repo, with a lockfile and content hashing. Run via npx.
Maintainers
Readme
skills-syncer
Vendor Claude Code skills and the subagents they use from one catalog into many repos — real file copies, recorded by a lockfile. One source of truth instead of a copy you hand-edit everywhere.
Unlike a git submodule or a symlink, each repo gets a real copy committed
into .claude/, so teammates, CI, and sandboxes never need this tool at runtime
— only the person adding or updating a skill runs the sync.
- Zero runtime dependencies (Node built-ins only).
- No build step — run it straight from npm with
npx. gitis needed only forgithub:sources; local catalogs need nothing.
Quick start
Run from the repo you want to set up. npx fetches the tool; the catalog comes
from --from:
npx skills-syncer --from github:acme/our-skills --skill fsd-rules react-rules✓ synced 2 skills · 1 agent · AGENTS.md → my-repo (github:acme/our-skills)Then commit what it wrote: .claude/skills/, .claude/agents/ (if any),
AGENTS.md, skills-syncer.json, and skills-syncer-lock.json.
Usage scenarios
1. Set up a brand-new repo
Pick the skills you want; their required agents come along automatically.
npx skills-syncer --from github:acme/our-skills --skill fsd-rules react-rules2. Install everything
npx skills-syncer --from github:acme/our-skills --skill '*' --agent '*'A '*' selection is stored literally in skills-syncer.json, so a later bare
re-sync (scenario 4) picks up skills added to the catalog since.
3. Add an agent on its own
Agents are a first-class catalog — install one directly even if no selected skill requires it.
npx skills-syncer --from github:acme/our-skills --skill run-maintain --agent worker4. Re-sync later to pull catalog updates
With no flags, the source and selection are read from skills-syncer.json. Run
this after the catalog changes to refresh this repo's copies:
npx skills-syncerCommit the diff. Because it is idempotent, re-running with no changes is a no-op.
5. Narrow or change the selection
Pass a new --skill/--agent set. Items dropped from the selection are removed
from .claude/ (the run prints removed skills: …); repo-authored skills and
agents are never touched.
# was fsd-rules + react-rules; now just fsd-rules — react-rules is removed
npx skills-syncer --from github:acme/our-skills --skill fsd-rules6. Develop against a local catalog
Point --from at a checkout or any folder. Useful while authoring skills before
pushing them.
npx skills-syncer --from ../our-skills --skill '*'
npx skills-syncer --from /abs/path/to/catalog --skill fsd-rules7. Pin to a branch or tag
Append #ref to a github: source to clone a specific branch or tag.
npx skills-syncer --from github:acme/our-skills#v2 --skill '*'8. Roll a catalog change out to many repos at once
The tool is pull-only: a change reaches a repo only when the sync runs there.
--all re-syncs every immediate subfolder that has a skills-syncer.json —
each from its own recorded source and selection — so one command updates a whole
folder of repos. Run it from the folder that holds them (or pass --root):
cd ~/code/myorg # a folder of sibling repos
npx skills-syncer --all # re-sync each repo from its own source
npx skills-syncer --all --dry-run # preview every repo, write nothing
npx skills-syncer --all --root ~/code/myorg # scan a specific folderIt walks one level deep (worktrees and nested repos are not reached) and reports
how many repos synced, were skipped (no skills-syncer.json), or failed. Repos
that share a source are grouped, so a github: catalog is fetched once, not
once per repo, and a repo whose source fails doesn't stop the rest.
The output is one aligned line per repo — the source is printed once per group,
not on every line — with a summary at the end (colours show on a terminal, plain
when piped or under NO_COLOR):
skills-syncer · syncing 4 repo(s) (1 skipped — no skills-syncer.json)
from github:acme/our-skills
✓ api-core 2 skills · AGENTS.md
✓ ui-kit 22 skills · 6 agents · AGENTS.md
✓ web-app 36 skills · 6 agents · AGENTS.md
✓ checkout 1 skill · AGENTS.md −3 removed
from github:acme/legacy-skills
✗ old-portal could not clone https://github.com/acme/legacy-skills.git
done with errors · synced 4 repo(s), skipped 1 (no skills-syncer.json), failed: old-portalA removed count (−3 removed) appears when a re-sync drops items no longer in
the repo's selection.
Every sync is incremental: an item already matching the catalog is left untouched, so a re-sync with nothing to do is a true no-op (no file churn).
9. Preview a sync without writing
--dry-run (or -n) computes the full plan — what would be installed,
overwritten, or removed — and writes nothing. Re-run without it to apply.
npx skills-syncer --from github:acme/our-skills --skill '*' --dry-runFlags
| Flag | Meaning |
| --- | --- |
| --from <src> | catalog source: github:owner/repo[#ref] or a local path |
| --skill <names…> | skills to install ('*' = all in the catalog) |
| --agent <names…> | agents to install directly ('*' = all); a selected skill's required agents come automatically |
| --all | re-sync every immediate subfolder that has a skills-syncer.json |
| --root <dir> | with --all, the folder to scan (default: current dir) |
| --no-claude-link | write no CLAUDE.md; remove one this tool made. Recorded in skills-syncer.json; applies to --all too |
| --claude-link | CLAUDE.md is a symlink to AGENTS.md (the default) |
| --claude-import | CLAUDE.md is a real file holding @AGENTS.md, the Claude Code import |
| --no-hooks | vendor no hooks; remove ones this tool installed. Recorded in skills-syncer.json; applies to --all too |
| --hooks | vendor the catalog's hooks (the default) |
| --dry-run, -n | show what would change; write nothing |
| --help, -h | show usage |
| --version, -v | print the version |
With no flags, the source and selection are read from skills-syncer.json.
The source catalog
A source is just a directory — a github:owner/repo[#ref] (shallow-cloned) or a
local path — laid out like this:
<catalog>/
skills/<name>/SKILL.md ... # or .claude/skills/<name>/ (auto-detected)
agents/<role>.md # or .claude/agents/<role>.md
hooks/<file> # or .claude/hooks/ (optional) hook scripts
settings.json # or .claude/settings.json (optional) hook wiring
skill-agents.json # optional: { "<skill>": ["<agent>", ...] }
AGENTS.md # optional: shared instructions block- Hooks are not part of the selection. A skill is a document an agent loads;
a hook is repo-wide wiring that runs whether an agent asks for it or not. So
every repo takes all of the catalog's hooks, or none of them with
--no-hooks. See Hooks below. - Skills and agents are two catalogs. An agent installs when it is named with
--agent, or required by a selected skill viaskill-agents.json. So selecting an orchestrator skill never leaves it without its agents. AGENTS.mdis merged into the top of the target repo'sAGENTS.mdinside fenced markers; repo-specific notes below the block are preserved across re-syncs. Alongside it the sync pointsCLAUDE.mdatAGENTS.mdso Claude Code reads the same instructions — a symlink by default, an@AGENTS.mdimport with--claude-import, or nothing with--no-claude-link. Either way the target is a sibling in the same repo, so it still rides with git into worktrees and sandboxes. A repo-authored realCLAUDE.mdis left untouched — see below.
Choosing the CLAUDE.md shape
Three shapes, one flag each. All three keep a single copy of the text in
AGENTS.md.
| Flag | What lands at CLAUDE.md |
| --- | --- |
| (default) / --claude-link | a symlink to AGENTS.md |
| --claude-import | a real file holding @AGENTS.md — the Claude Code import |
| --no-claude-link | nothing |
npx skills-syncer --claude-import # this repo
npx skills-syncer --all --claude-import # every repo under the folderUse --claude-import where a symlink does not survive: Windows without developer
mode, an export or archive step that dereferences links, a copy that flattens
them. The file is one line, so git treats it like any other text file:
@AGENTS.mdSwitching is free — a later --claude-link turns the file back into a symlink,
and --no-claude-link removes it. Only what this tool wrote is replaced or
removed: a CLAUDE.md the repo authored is left alone, with a warning. (A
symlink named CLAUDE.md counts as this tool's, so a stale one is repointed.)
A non-default choice is recorded in skills-syncer.json ("claudeLink": false
or "claudeLink": "import"), so plain re-syncs — including --all, which reads
each repo's own config — keep honouring it with no flag to repeat. Unlike
--from/--skill/--agent, these flags are not ignored by --all: they
override every repo's recorded value, so a fleet flips in one run. Passing two of
them together is an error.
Bundled catalog (ship the tool with your catalog)
A catalog repo can bundle skills-syncer as its own bin, so consumers run it
straight from the catalog with no --from:
// package.json in your catalog repo
{ "bin": { "your-catalog": "bin/skills-syncer.mjs" } }npx github:acme/our-skills --skill '*' # the catalog is its own sourceWhen no --from is given and there is no skills-syncer.json, the tool falls
back to its own package root if that carries a catalog (skills/,
.claude/skills/, …). The lock records the catalog's package name as the source,
and skills-syncer.json keeps only the selection (a bare re-sync resolves the
bundled catalog again).
What it writes into your repo
| File | Role |
| --- | --- |
| .claude/skills/<name>/ | each selected skill folder (real copy) |
| .claude/agents/<role>.md | each selected/required agent (registered subagent) |
| .claude/hooks/<file> | every hook script in the catalog (real copy) |
| .claude/settings.json | the catalog's hooks block, merged in; every other key is left alone |
| AGENTS.md | shared block merged in, repo notes kept below |
| CLAUDE.md | symlink → AGENTS.md, or @AGENTS.md in a real file with --claude-import, or nothing with --no-claude-link (a repo-authored CLAUDE.md is never touched) |
| skills-syncer.json | your choice: source + selection (hand-editable, committed) |
| skills-syncer-lock.json | generated manifest: per-item content hash |
A re-sync replaces only what the lock installed and removes what you dropped from the selection — it never touches a repo-authored skill or agent.
Both JSON files are written the way prettier and biome print them: two-space indent, one key per line, and a short array kept on one line while it fits 80 columns. A repo formatter therefore has nothing to fix, and a re-sync has no formatting to undo. Should a formatter still differ (a repo that indents with tabs, say), it wins: the tool rewrites a file only when the data changes, never to restyle it.
skills-syncer.json is hand-editable — change the from, skills, or agents
fields and run a bare npx skills-syncer instead of retyping flags:
{
"from": "github:acme/our-skills",
"skills": ["fsd-rules", "react-rules"],
"agents": ["worker"],
"claudeLink": "import"
}claudeLink is optional. It appears only when the repo picked something other
than the default symlink: "import" for the @AGENTS.md file, or false for no
CLAUDE.md at all.
Hooks
A skill is a document an agent chooses to load. A hook is different. It runs on
an event, whether an agent asks for it or not, and it only runs when
.claude/settings.json points at it. So the catalog ships both halves:
<catalog>/
hooks/language-check.mjs # the script
settings.json # { "hooks": { "Stop": [ ... ] } }The sync copies every file under hooks/ into the repo, and merges only the
hooks block of settings.json. Every other key in the repo settings is left
alone. Use $CLAUDE_PROJECT_DIR in a command so the path works in any repo:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/language-check.mjs\"" }
]
}
]
}
}The lock records each hook group the tool installed. A later sync takes those exact groups back out before it writes the new ones. So:
- A hook you add to the repo by hand is never removed.
- A hook dropped from the catalog is dropped from the repo.
- A changed command replaces the old one instead of piling up next to it.
- A hook file the repo authored is never overwritten. The tool warns and skips it.
A repo opts out with --no-hooks, which is recorded as "hooks": false in
skills-syncer.json. Opting out also removes the hooks the tool installed
earlier, and deletes .claude/settings.json when nothing else is left in it.
Requirements
- Node ≥ 18
giton PATH (only forgithub:sources; local paths need nothing)
License
MIT
