Skip to content

Repository files navigation

skills-syncer

CI

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.
  • git is needed only for github: 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-rules

2. 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 worker

4. 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-syncer

Commit 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-rules

6. 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-rules

7. 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 folder

It 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-portal

A 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-run

Flags

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 via skill-agents.json. So selecting an orchestrator skill never leaves it without its agents.
  • AGENTS.md is merged into the top of the target repo's AGENTS.md inside fenced markers; repo-specific notes below the block are preserved across re-syncs. Alongside it the sync points CLAUDE.md at AGENTS.md so Claude Code reads the same instructions — a symlink by default, an @AGENTS.md import 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 real CLAUDE.md is 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 folder

Use --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.md

Switching 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 source

When 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
  • git on PATH (only for github: sources; local paths need nothing)

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages