Declarative Sources
Rulesync can fetch rules and skills from external repositories using the install command. Instead of manually running fetch for each source, declare it in your rulesync.jsonc and run rulesync install to resolve and fetch its selected artifacts. Then rulesync generate processes them as curated inputs. Typical workflow: rulesync install && rulesync generate.
To add one source without editing JSONC by hand, run rulesync add <source>. It preserves existing comments, appends the source entry, installs it, and updates the appropriate lockfile:
rulesync add anthropics/skills --skills skill-creator
# Add one rule without selecting any skills
rulesync add acme/ai-standards --rules testing-guidelinesThe command fetches only the source being added. Existing sources must already be locked and installed; run rulesync install first when they are not. If the new source fails, Rulesync restores the manifest, source lockfiles, curated rules, and curated skills to their previous state.
Configuration
Add a sources array to your rulesync.jsonc:
{
"$schema": "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json",
"targets": ["copilot", "claudecode"],
"features": ["rules", "skills"],
"sources": [
// Fetch all skills from a GitHub repository (default transport)
{ "source": "owner/repo" },
// Fetch only specific skills by name
{ "source": "anthropics/skills", "skills": ["skill-creator"] },
// Fetch only specific .md rules from rules/ (no skills)
{
"source": "acme/ai-standards",
"rules": ["testing-guidelines", "typescript-conventions"],
},
// Rules and skills can be selected from the same source
{
"source": "acme/ai-assets",
"rules": ["*"],
"rulesPath": "exports/rules",
"skills": ["review-pr"],
"path": "exports/skills",
},
// With ref pinning and subdirectory path (same syntax as fetch command)
{ "source": "owner/repo@v1.0.0:path/to/skills" },
// Git transport — works with any git remote (Azure DevOps, Bitbucket, etc.)
{
"source": "https://dev.azure.com/org/project/_git/repo",
"transport": "git",
"ref": "main",
"path": "exports/skills",
},
// Git transport with a local repository
{ "source": "file:///path/to/local/repo", "transport": "git" },
// Git transport against a single-skill repo whose SKILL.md is at the root
{
"source": "https://github.com/feature-sliced/skills",
"transport": "git",
"path": ".",
},
// npm transport (EXPERIMENTAL) — fetch a package from an npm-compatible
// registry (npmjs.org, JFrog Artifactory, Sonatype Nexus, Verdaccio, ...)
{
"source": "@acme/skill-package",
"transport": "npm",
"registry": "https://acme.jfrog.io/artifactory/api/npm/npm-local/",
"tokenEnv": "ACME_NPM_TOKEN",
},
],
}Each entry in sources accepts:
| Property | Type | Description |
|---|---|---|
source | string | Repository source. For GitHub transport: owner/repo or owner/repo@ref:path. For git transport: a full git URL. For npm transport: a package name (pkg or @scope/pkg). |
skills | string[] | Optional skill names to fetch. "*" selects all skills. When both skills and rules are omitted, all skills are fetched for backward compatibility. An empty list ([]) is an error; to take no skills, omit skills and list only rules. |
rules | string[] | Optional rule names to fetch. Names may include or omit .md; "*" selects every direct .md file under rulesPath. Setting only rules fetches no skills. An empty list ([]) is an error; to take no rules, omit rules and list the skills you want in skills. |
transport | string | "github" (default) uses the GitHub REST API. "git" uses git CLI and works with any git remote. "npm" (experimental) fetches a package from an npm-compatible registry. |
ref | string | Branch, tag, or ref to fetch from. Defaults to the remote's default branch. For GitHub transport, use the @ref source syntax. For npm transport: an exact version or dist-tag (defaults to latest). |
path | string | Path to the skills directory within the repository. Defaults to "skills". Set to "", ".", or "./" to target the entire repository root (see note below). For GitHub transport, use the :path source syntax. |
rulesPath | string | Path to the rules directory within the repository or package. Defaults to "rules". This is independent from the skills-only path field. |
registry | string | npm transport only. Base URL of the npm-compatible registry. Defaults to https://registry.npmjs.org. |
tokenEnv | string | npm transport only. Name of the environment variable holding the registry token. Defaults to NPM_TOKEN. Must be NPM_TOKEN, NODE_AUTH_TOKEN, or end with _NPM_TOKEN (e.g. ACME_NPM_TOKEN). |
An empty skills or rules list fails only the source that declares it, in --frozen mode too: the other sources are still installed into .curated/, and then install exits with an error that counts the failed sources.
Rules are flat source files: only direct .md children of rulesPath are discovered. Nested rule files are not installed. Fetched rules are written to .rulesync/rules/.curated/<rule-name>.md; during generation they behave as if they were ordinary files directly under .rulesync/rules/.
Repository-root paths (
path: "."): Whenpathis"",".", or"./"(with thegittransport), rulesync disables sparse-checkout and fetches the entire repository tree, then groups each top-level directory as a skill. This is useful for single-skill repositories whoseSKILL.mdlives at the repo root (<repo>/SKILL.md) rather than under askills/container. Because the whole tree is fetched, prefer a narrowerpathfor large repositories; the fetch is still bounded by rulesync's file-count, total-size, and depth limits.
Symlinks (
gittransport): Symlinks inside the fetchedpathorrulesPathare skipped. If the directory itself, or one of its parents, is a symlink that resolves outside the cloned repository, rulesync warns and fetches nothing from it, so a source repository cannot makeinstallcopy files from elsewhere on your machine.
npm Transport (Experimental)
WARNING
The npm transport is experimental. Its configuration surface and lockfile format may change in a future release.
The npm transport fetches skills from any registry that implements the npm registry API. Because JFrog Artifactory, Sonatype Nexus, Verdaccio, GitHub Packages, and similar private registries all expose an npm-compatible API, a single transport with a configurable registry URL covers them all. This lets enterprises whose build environments cannot reach public GitHub distribute skills internally as npm packages.
How a package is fetched:
- The package metadata (packument) is fetched from
<registry>/<package>using the abbreviatedapplication/vnd.npm.install-v1+jsonform. - The declared
ref(an exact version or a dist-tag such aslatestorbeta— semver ranges are not supported) is resolved to a concrete version. - The version's tarball is downloaded and verified against the registry's
dist.integrity/dist.shasummetadata. - The tarball is extracted in memory with a hardened minimal tar reader: only regular files are materialized (symlinks, hardlinks, and device entries are skipped), path traversal is rejected, and extraction is capped at 10,000 files / 100 MB to prevent decompression bombs.
Package layout: skills are discovered the same way as for the git transports. Skill directories under skills/ (or the configured path) are installed as .rulesync/skills/.curated/<name>/. Direct .md files under rules/ (or the configured rulesPath) can be selected with rules and are installed under .rulesync/rules/.curated/. A single-skill package with SKILL.md at the package root is installed as one skill named after the package's base name (@acme/my-skill installs as my-skill); note that this root fallback installs the package's root-level files only, so prefer the skills/<name>/ layout for skills that carry subdirectories such as references/.
Authentication uses a bearer token from an environment variable: NPM_TOKEN by default, or the variable named by the per-source tokenEnv field. The token is sent as Authorization: Bearer <token> to the registry (and to the tarball host only when it matches the registry host). .npmrc files are intentionally not read.
For CI environments using NODE_AUTH_TOKEN, set "tokenEnv": "NODE_AUTH_TOKEN" explicitly. The default remains NPM_TOKEN.
Because registry and tokenEnv live in the project's committed config, a cloned repository must not be able to send your token to a host of its choosing. rulesync therefore:
- only accepts a
tokenEnvnamedNPM_TOKEN,NODE_AUTH_TOKEN, or ending with_NPM_TOKEN, so a config cannot point it at unrelated secrets such asGITHUB_TOKEN; - only sends a token to
https://registry.npmjs.org, or to a registry whose origin you list in theRULESYNC_NPM_TRUSTED_REGISTRIESenvironment variable (comma-separated, e.g.RULESYNC_NPM_TRUSTED_REGISTRIES=https://acme.jfrog.io). Each entry must be a full URL with its scheme; an entry such asacme.jfrog.iocannot be read as an origin, so it trusts nothing and rulesync warns about it. For any other registry, theNPM_TOKENfallback is not sent (rulesync warns and requests anonymously), and an explicittokenEnvis an error.
Resolved versions are pinned in rulesync-npm.lock.json (next to rulesync.lock), which records the resolved version, the tarball integrity, and per-artifact content hashes. Commit it for reproducible installs; --update and --frozen behave the same as for git sources.
How It Works
When rulesync install runs and sources is configured:
- Lockfile resolution — Each source's ref is resolved to a commit SHA and stored in
rulesync.lock(at the project root). On subsequent runs the exact locked SHA is checked out for deterministic builds. npm-transport sources are pinned in a separaterulesync-npm.lock.json(resolved version + tarball integrity). - Remote artifact listing — The configured skills and rules directories are listed from the remote source.
- Filtering — Only the names selected by
skillsandrulesare fetched. Omitting both fields retains the historical behavior of fetching all skills. - Precedence rules:
- Local inputs win within one source tree — Rules and skills outside
.curated/take precedence over a same-named curated artifact in that input root. Across multipleinputRoots, root order remains primary: a later root replaces an earlier root's effective artifact even when the later artifact is curated. - First-declared source wins — If two sources provide an artifact with the same name, the one declared first in the
sourcesarray is used. This holds even though GitHub-transport sources are downloaded concurrently (up to 4 sources and 10 requests at a time): writes, name-collision checks and lockfile updates still run one source at a time in declared order. git and npm transport sources are fetched one at a time.
- Local inputs win within one source tree — Rules and skills outside
- Output — Fetched rules are written to
.rulesync/rules/.curated/<rule-name>.md; fetched skills are written to.rulesync/skills/.curated/<skill-name>/. Both directories are automatically added to.gitignorebyrulesync gitignore. A skill's supporting file that the source records as executable keeps that bit in.curated/(mode0755), sorulesync generatecarries it into each generated skill directory and ascripts/run.shthe skill tells the agent to run stays runnable. The GitHub transport reads the mode from the repository tree (git mode100755) with one extra request per source, and only when a fetched skill has files besideSKILL.md; if the tree is cut short or cannot be read, the files are still installed without the bit and a warning says so. The git transport takes the bit from the checked-out file, and the npm transport from the tarball entry's mode.
Install Modes
rulesync install supports three install modes via --mode <mode>:
| Mode | Manifest input | Lockfile | Output layout |
|---|---|---|---|
rulesync | rulesync.jsonc sources | rulesync.lock (+ rulesync-npm.lock.json for npm sources) | .rulesync/rules/.curated/<name>.md, .rulesync/skills/.curated/<name>/ (then re-emitted by rulesync generate) |
apm | apm.yml dependencies.apm | rulesync-apm.lock.yaml | .github/instructions/, .github/skills/ (APM v1 layout) |
gh | rulesync.jsonc sources | rulesync-gh.lock.yaml | Per-agent / per-scope dirs (matching gh skill install) |
When --mode is omitted, rulesync defaults to rulesync mode. If apm.yml is present and sources is also defined, you must pass --mode apm or --mode rulesync to disambiguate.
In --mode apm, a plain install uses the commit recorded in rulesync-apm.lock.yaml only while the lock entry's ref is still the one apm.yml declares (or, without one, the repository's default branch), and only after checking that the commit is still in that ref's history in the dependency's repository; a commit that is gone upstream or only exists in a fork fails the dependency, and --update re-resolves it. A changed ref is resolved again (--frozen fails instead). Under --frozen, every dependency's files are checked before any of them is written. Stale files from a previous install are only removed from .github/instructions/ and .github/skills/; any other path the lockfile names is left alone with a warning.
--mode gh — gh-skill-install–compatible layout
--mode gh reads the same sources array from rulesync.jsonc but writes each discovered skill into the agent-specific directory expected by gh skill install. Each source supports two extra fields:
| Property | Type | Default | Description |
|---|---|---|---|
agent | string | github-copilot | One of github-copilot, claude-code, cursor, codex, gemini, antigravity. |
scope | string | project | project writes inside the project root; user writes inside the user's home directory. |
Agent → install directory mapping:
| Agent | Project scope (relative to project root) | User scope (relative to home) |
|---|---|---|
github-copilot | .agents/skills | .copilot/skills |
claude-code | .claude/skills | .claude/skills |
cursor | .agents/skills | .cursor/skills |
codex | .agents/skills | .agents/skills |
gemini | .agents/skills | .gemini/skills |
antigravity | .agents/skills | .gemini/antigravity/skills |
For each skill discovered as skills/<name>/SKILL.md in the remote repository, rulesync deploys the entire skill directory to <install-dir>/<name>/ and injects a provenance frontmatter block (source, repository, ref) into the deployed SKILL.md. The lockfile rulesync-gh.lock.yaml records one entry per (source, agent, scope, skill) tuple.
Per-source field support in --mode gh:
| Field | Status |
|---|---|
source | Required. Must resolve to a GitHub repository (owner/repo, owner/repo@ref, or an https://github.com/... URL). |
skills | Optional. When set, only the listed skill names are installed; remote skills not in the list are skipped, and missing names log a warning. |
rules | Rejected. Declarative rules are supported only in --mode rulesync. |
rulesPath | Rejected. Declarative rules are supported only in --mode rulesync. |
ref | Optional. Pins a tag, branch, or commit SHA. When omitted, gh mode resolves to the latest release's tag, falling back to the default branch. |
agent | Optional. Defaults to github-copilot. See the agent table above. |
scope | Optional. Defaults to project. |
transport | Rejected. gh mode is GitHub-only and does not honor the git transport. Drop the field or switch to --mode rulesync. |
path | Rejected. The remote layout is fixed to skills/<name>/SKILL.md. Repositories that store skills elsewhere are not supported in gh mode. |
The remote repository must use the layout skills/<name>/SKILL.md (one directory per skill, each containing a SKILL.md). Other layouts are not auto-discovered.
Example rulesync.jsonc:
{
"targets": ["claudecode"],
"features": ["rules"],
"sources": [
// Default: agent=github-copilot, scope=project -> .agents/skills/git-commit/
{ "source": "acme/skills", "skills": ["git-commit"] },
// Same source, deployed for Claude Code at user scope -> ~/.claude/skills/git-commit/
{
"source": "acme/skills",
"skills": ["git-commit"],
"agent": "claude-code",
"scope": "user",
},
],
}Run with npx rulesync install --mode gh.
A plain rulesync install --mode gh installs each source at the commit recorded in rulesync-gh.lock.yaml, so a new upstream release or a moved branch is not picked up until you run it with --update. A source whose ref was changed in rulesync.jsonc (or added or dropped) is no longer covered by the lock and is resolved again. --frozen fails on such a change instead. Before using a locked commit, rulesync checks that it is still in the history of the locked ref in that repository; a commit that is gone (for example after a force-push) or that only exists in a fork fails the source, and so does a locked ref that is gone upstream (a renamed default branch, a deleted release); --update re-resolves it.
At project scope, a deployed file is refused when any path component below the project root — the install directory, the skill directory, or the file itself — is a symbolic link, and the install fails with Refusing to follow a symbolic link. This stops a cloned repository from committing a link such as .agents/skills/x/config -> ../../../.git/config that would send the remote bytes into another file of the project. User scope is not checked, so a ~/.claude linked into a dotfiles checkout keeps working.
Files that a previous run deployed and the upstream no longer ships are removed again, but only from that skill's own install directory. A stale file that is itself a symbolic link is removed as a link, without touching its target; a stale file reached through a linked directory is left alone with a warning. Because rulesync-gh.lock.yaml is committed with the project, user-scope files are only cleaned up while the current config still installs the same source with the same agent at user scope; otherwise rulesync warns and leaves them for you to remove.
CLI Options
The install command accepts these flags:
| Flag | Description |
|---|---|
--mode <mode> | Install mode: rulesync (default), apm, or gh. See Install Modes above. |
--update | Force re-resolve all source refs, ignoring the lockfile (useful to pull new updates). |
--frozen | Fail if a lockfile is missing or does not cover declared sources and their skill and rule selections. Fetches missing locked artifacts without updating the lockfile. Useful for CI. |
--outdated | Report which sources are behind in the lockfile without installing or writing anything. See Checking for Outdated Sources below. |
--token <token> | GitHub token for private repositories. |
# Install rules and skills using locked refs
rulesync install
# Force update to latest refs
rulesync install --update
# Strict CI mode — fail if lockfile doesn't cover all sources and selections
rulesync install --frozen
# Report sources whose lockfile entry is behind (read-only)
rulesync install --outdated
# Install then generate
rulesync install && rulesync generate
# Skip source installation — just don't run install
rulesync generateLockfile
The lockfile at rulesync.lock (at the project root) records the resolved commit SHA, the skill and rule selections each entry was written for, and per-artifact integrity hashes for each source so that builds are reproducible. Rulesync verifies cached rule and skill content against these hashes before reusing it: a curated rule or skill whose files were edited, added, or removed in place is fetched again at its locked ref (under --frozen too) and restored to the locked content. If the content fetched at the locked ref does not match the recorded hash, rulesync warns and takes it; under --frozen it refuses to install that source instead. Two cases are checked for presence only: a skill whose name an earlier declared source already installed (the curated directory holds that source's copy, which takes precedence), and a skill locked with an empty integrity by the migration from the legacy lockfile format, until --update or a ref change fetches it again and records its hash. It is safe to commit this file. An example:
{
"lockfileVersion": 1,
"sources": {
"owner/skill-repo": {
"requestedRef": "main",
"resolvedRef": "abc123def456...",
"resolvedAt": "2025-01-15T12:00:00.000Z",
"skills": {
"my-skill": { "integrity": "sha256-abcdef..." },
"another-skill": { "integrity": "sha256-123456..." }
},
"skillSelection": ["*"],
"skillsPath": "skills",
"rules": {
"testing-guidelines": { "integrity": "sha256-789abc..." }
},
"ruleSelection": ["*"],
"rulesPath": "rules",
"resolvedRuleNames": ["testing-guidelines"]
}
}
}To update locked refs, run rulesync install --update.
A locked commit is only reused for the ref it was resolved from: when the ref a git or GitHub source declares (in ref or in the source string, such as owner/repo@v2) differs from the entry's requestedRef, a plain rulesync install resolves the source again, and --frozen fails instead. For a source that declares a ref, an entry that records no requestedRef (for example one migrated from an older lockfile) keeps its locked commit and passes --frozen; the declared ref is recorded on it the next time the entry is written. Removing a source's ref altogether is not detected, because the requestedRef of a source without one is the default branch it resolved to: the locked commit stays in use until rulesync install --update. For the git transport, ref must name a branch or a tag exactly (v1 never matches a branch such as feature/v1); an annotated tag resolves to the commit it points at, and a name that is both a branch and a tag pointing at different commits is refused as ambiguous.
Checking for Outdated Sources
rulesync install --outdated answers "is my lockfile behind its sources?" without changing anything: it writes no lockfile, fetches no rules or skills, and touches no files. For each declared source it resolves the ref the same way --update would — the ref declared in rulesync.jsonc (or in the source string, such as owner/repo@v1), or the default branch when none is declared (the latest dist-tag for npm sources) — and compares the result with the commit SHA (resolvedRef) or package version (resolvedVersion) in the lockfile. Each source is reported as up to date, outdated, not locked (declared but missing from the lockfile), or failed (its ref could not be resolved, for example because the network or the registry is unreachable, or a token is missing).
The exit code makes the check usable in CI and scripts:
| Exit code | Meaning |
|---|---|
0 | Every source is locked at the ref it resolves to now. |
1 | At least one source is outdated or not locked; rulesync install --update moves it on. |
2 | At least one source could not be resolved, so its status is unknown. |
With the global --json flag, the per-source report (source, transport, status, requestedRef, lockedRef, latestRef, error) is returned as data.sources on success and as error.details.sources when the command exits non-zero. Any other error — an invalid flag combination or an unreadable configuration file, for example — also exits with code 1, as it does for every command; with --json, only an outdated result carries error.details.sources. --outdated works only in the default rulesync mode and cannot be combined with --update or --frozen. It checks refs only; whether the lockfile covers each source's skills and rules selection is what --frozen checks.
Changing a source's skills or rules selection in rulesync.jsonc (for example, adding a skill name to an explicit list, or switching to "*"), or its path or rulesPath, is picked up by the next plain rulesync install: the entry is refetched at its locked ref and the lockfile records the new selection and path. Under --frozen, a selection or path the lockfile does not cover fails the install instead. A lockfile written before skillSelection or skillsPath was recorded is fetched again once, at its locked ref, by the next plain rulesync install, which then records them; commit the updated lockfile so --frozen installs keep reusing the cache.
npm-transport sources (experimental) are pinned in a separate rulesync-npm.lock.json, because they lock a resolved package version and tarball integrity instead of a commit SHA:
{
"lockfileVersion": 1,
"sources": {
"@acme/skill-package": {
"registry": "https://acme.jfrog.io/artifactory/api/npm/npm-local",
"requestedVersion": "latest",
"resolvedVersion": "1.2.3",
"integrity": "sha512-...",
"resolvedAt": "2026-01-15T12:00:00.000Z",
"skills": {
"my-skill": { "integrity": "sha256-abcdef..." }
},
"skillSelection": ["my-skill"],
"skillsPath": "skills",
"rules": {
"testing-guidelines": { "integrity": "sha256-789abc..." }
},
"ruleSelection": ["testing-guidelines"],
"rulesPath": "rules",
"resolvedRuleNames": ["testing-guidelines"]
}
}
}It is safe (and recommended) to commit this file as well.
Authentication
GitHub transport uses the GITHUB_TOKEN or GH_TOKEN environment variable for authentication. This is required for private repositories and recommended for better rate limits. Git transport relies on your local git credential configuration (SSH keys, credential helpers, etc.). npm transport (experimental) uses the NPM_TOKEN environment variable, or the variable named by the per-source tokenEnv field (which must be NPM_TOKEN, NODE_AUTH_TOKEN, or end with _NPM_TOKEN); .npmrc files are not read. A registry other than https://registry.npmjs.org only receives the token when its origin is listed in RULESYNC_NPM_TRUSTED_REGISTRIES (see npm Transport).
# Using environment variable
export GITHUB_TOKEN=ghp_xxxx
npx rulesync install
# Or using GitHub CLI
GITHUB_TOKEN=$(gh auth token) npx rulesync installTIP
The install command also accepts a --token flag for explicit authentication: rulesync install --token ghp_xxxx.
Curated vs Local Inputs
| Location | Type | Precedence within one root | Committed to Git |
|---|---|---|---|
.rulesync/skills/<name>/ | Local | Higher | Yes |
.rulesync/skills/.curated/<name>/ | Curated | Lower | No (gitignored) |
.rulesync/rules/<name>.md | Local | Higher | Yes |
.rulesync/rules/.curated/<name>.md | Curated | Lower | No (gitignored) |
When a local and curated artifact in the same source tree share a name, the local artifact is used and the remote one is not fetched. With multiple input roots, this per-root selection happens before the roots are merged in order; see Separate Input Root.
