@sunnygg/distributor
v1.2.0
Published
Synchronize Agent Skills across supported agent harnesses.
Readme
npx @sunnygg/distributor initGetting started
npx @sunnygg/distributor init
npx @sunnygg/distributor import
npx @sunnygg/distributor syncinit creates the Distributor configuration and prompts you to choose project
or global syncing, a skill source, and target agent harnesses. It also scans
supported harness directories and offers to import any existing skills it
finds.
import scans every native and compatible skill directory declared by the
available harness adapters. You can select which discovered skills to copy into
the configured source. Existing source skills are preserved. If Distributor
has not been initialized, import runs interactive initialization first.
sync links the skills from that source into the configured agent harnesses.
Add your first skill
Initialize a project with the defaults, add a skill, inspect the plan, and sync it:
distributor init --yes
mkdir -p .agents/skills/code-review
cat > .agents/skills/code-review/SKILL.md <<'EOF'
---
name: code-review
description: Review code changes.
---
EOF
distributor sync --dry-run
distributor syncinit --yes creates the default config, .agents/skills, and the local-state
ignore file without overwriting existing content. Plain distributor init
prompts when run interactively. In a non-interactive terminal, use --yes.
Each non-hidden directory immediately under the source root that contains a
regular file named exactly SKILL.md is a skill. Skills with invalid
frontmatter or unsafe entries are skipped and reported as warnings by status
and sync; other valid skills continue to load. Other non-hidden files and
directories are helper content and are preserved at the same relative paths.
Each top-level skill or helper directory is linked as one directory; a
top-level helper file is linked as a file. Because skill directories are linked
intact, optional content such as agents/openai.yaml (or agents/openai.yml)
remains visible through every linked skill folder.
Command details
distributor # show help
distributor --help # show help
distributor help # show help
distributor help sync # show sync help
distributor --version # print the installed version
distributor -V # print the installed version
distributor version # print the installed version
distributor init # interactive initialization
distributor init --yes # initialize with defaults
distributor init -y # same as --yes
distributor import # import skills from harness directories
distributor status # show skill and reference status
distributor sync # sync every enabled harness
distributor sync --harness codex # sync one enabled harness
distributor sync --dry-run # plan without writing
distributor remove # remove every managed linkdistributor status shows table summaries for source skills, skill-to-placement
references per configured harness, skill configuration, and source and harness
storage paths. A configured skill uses ✓; a skill that needs sync or has a
conflict uses ⚠. The command also reports whether references are up to date.
It uses the same read-only planning checks as sync and does not write links,
directories, or managed state.
--harness <id> may appear only once and must name an available harness that
is enabled in the project config. A dry run follows the same config, skill,
adapter, state, target, conflict, and diff-inspection path as a real sync, then
stops before the first write-capable operation. It does not create directories,
links, .distributor files, or filesystem metadata.
Configuration
Distributor searches upward from the current directory for the nearest
distributor.config.json, distributor.config.js, or
distributor.config.ts. In a Git worktree, discovery stops at the worktree
root. The config's directory is the project root.
The exact config generated by distributor init --yes is:
{
"scope": "project",
"source": ".agents/skills",
"harnesses": [
{ "name": "codex", "useHarnessFolder": true },
{ "name": "claude-code", "useHarnessFolder": true },
{ "name": "opencode", "useHarnessFolder": true },
{ "name": "cursor", "useHarnessFolder": true },
{ "name": "gemini-cli", "useHarnessFolder": true },
{ "name": "antigravity", "useHarnessFolder": true },
{ "name": "github-copilot", "useHarnessFolder": true },
{ "name": "openhands", "useHarnessFolder": true },
{ "name": "pi", "useHarnessFolder": true },
{ "name": "cline", "useHarnessFolder": true },
{ "name": "goose", "useHarnessFolder": true },
{ "name": "crush", "useHarnessFolder": true },
{ "name": "qwen-code", "useHarnessFolder": true },
{ "name": "kilo-code", "useHarnessFolder": true },
{ "name": "roo-code", "useHarnessFolder": true },
{ "name": "trae-agent", "useHarnessFolder": true }
]
}scope defaults to project when omitted. project links skills inside the
project. global still reads the source from the project, but links those
skills into user-level directories.
source defaults to .agents/skills when omitted. Every harness entry requires
name. The optional useHarnessFolder field defaults to false. When it is
false, automatic placement prefers the scope's .agents/skills directory
when that harness supports it. When it is true, automatic placement uses the
harness's native directory. Harnesses without .agents/skills support use
their native directory either way, so the boolean has no effect for them.
An explicit targets array takes precedence over useHarnessFolder. The
expanded form below shows explicit project and user placements plus a custom
project target:
{
"scope": "project",
"source": ".agents/skills",
"harnesses": [
{
"name": "codex",
"useHarnessFolder": true,
"targets": [{ "placement": "project" }, { "placement": "user" }]
},
{
"name": "claude-code",
"useHarnessFolder": true,
"targets": [
{
"placement": "project",
"path": ".custom/claude-skills"
},
{ "placement": "user" }
]
},
{
"name": "opencode",
"useHarnessFolder": false,
"targets": [{ "placement": "agents-project" }, { "placement": "user" }]
}
]
}Configuration rules:
harnessesis a non-empty array of objects. Each object requiresnameand may include a booleanuseHarnessFolder, which defaults tofalse; harness IDs must be unique.- A target may contain
placement,path, or both. Whenplacementis omitted, the adapter's default project placement supplies its behavior. - Relative paths resolve from the project root.
~,$HOME, and$PROJECT_ROOTare supported; other variable syntax is rejected. - Only project and explicitly selected user placements are allowed. Admin, system, plugin, package, and configured scopes are rejected.
pathexplicitly overrides the selected placement's target root and is how to choose a custom directory.- Unknown fields, placements, adapters, empty target arrays, and duplicate effective targets are errors.
JavaScript and TypeScript configs use the same shape and must default-export their value:
import type { DistributorConfig } from "@sunnygg/distributor";
const config = {
source: ".agents/skills",
harnesses: [
{ name: "codex", useHarnessFolder: true },
{ name: "claude-code", useHarnessFolder: true },
{ name: "opencode", useHarnessFolder: true },
],
} satisfies DistributorConfig;
export default config;JavaScript and TypeScript config files are trusted executable code and are loaded by Distributor. Use them only when you trust the project. Skill Markdown, YAML, scripts, references, and assets are read as data and are never executed by Distributor.
Writing custom adapters
A custom adapter teaches Distributor where an otherwise unsupported agent harness reads Agent Skills. The adapter does not run the harness or convert a skill. It names one or more skill directories, and Distributor links each skill's files from the configured source into the selected directory.
Assume the project used in the examples is /work/my-app and commands are run
from that directory. The important paths are:
/work/my-app/ project root
├── distributor.config.json Distributor project configuration
├── .agents/skills/ canonical skill source
│ └── code-review/SKILL.md one source skill
├── .distributor/adapters/ custom adapters loaded for this run
│ └── team-agent.json one adapter definition
└── .team-agent/skills/ Team Agent's project skill directory
└── code-review/
└── SKILL.md -> ../../../.agents/skills/code-review/SKILL.mdDistributor finds the project root from the nearest distributor.config.*
file, but it finds custom adapters in .distributor/adapters under the exact
working directory. Therefore, run Distributor from /work/my-app in this
example. Running it from /work/my-app/packages/api would look for adapters in
/work/my-app/packages/api/.distributor/adapters, even if Distributor finds
the project configuration higher up.
Only immediate .json, .js, and .ts files are loaded. Nested adapter files
and other extensions are ignored. The adapter's name is its harness ID; the
adapter filename can be anything.
Quick start: one project directory with JSON
JSON is the shortest option and does not execute code. From the project root,
create .distributor/adapters/team-agent.json:
{
"name": "team-agent",
"displayName": "Team Agent",
"adapterStatus": "available",
"supportsNativeSkills": true,
"placements": [
{
"id": "project",
"item": "skills",
"support": "native",
"scope": "project",
"defaultPath": ".team-agent/skills",
"createIfMissing": true
}
]
}Here, .distributor/adapters/team-agent.json is relative to the directory in
which distributor runs. .team-agent/skills is relative to the project root,
so it resolves to /work/my-app/.team-agent/skills. createIfMissing: true
lets Distributor create the target directory when needed.
Add the adapter ID to distributor.config.json if the project is already
initialized:
{
"scope": "project",
"source": ".agents/skills",
"harnesses": [{ "name": "team-agent", "useHarnessFolder": true }]
}In this configuration, .agents/skills resolves to
/work/my-app/.agents/skills. After adding a skill there, preview and apply the
links:
mkdir -p .agents/skills/code-review
printf '%s\n' '---' 'name: code-review' 'description: Review code changes.' '---' \
> .agents/skills/code-review/SKILL.md
distributor sync --dry-run
distributor syncThe resulting target directory is
/work/my-app/.team-agent/skills/code-review, a symbolic link to
/work/my-app/.agents/skills/code-review. Distributor links each top-level
skill or helper directory as a unit and does not replace the harness's entire
skills root.
A new project can instead create the adapter first and run distributor init
or distributor init --yes. Available custom adapters are included during
initialization.
Example: project and user placements with JavaScript
Use multiple placements when a harness supports both a repository-local skill
directory and a user-wide directory. Create
.distributor/adapters/workbench.js:
const adapter = {
name: "workbench",
displayName: "Workbench",
adapterStatus: "available",
supportsNativeSkills: true,
defaultProjectPlacementId: "project",
placements: [
{
id: "project",
item: "skills",
support: "native",
scope: "project",
defaultPath: ".workbench/skills",
createIfMissing: true,
},
{
id: "user",
item: "skills",
support: "native",
scope: "user",
defaultPath: "~/.config/workbench/skills",
createIfMissing: true,
},
],
};
export default adapter;The two path roots are different:
projectresolves.workbench/skillsfrom the config directory, for example/work/my-app/.workbench/skills.userexpands~to the current user's home directory, for example/home/alex/.config/workbench/skills.defaultProjectPlacementId: "project"is the automatic harness-folder placement whenscopeisproject. It is required because this adapter declares more than one placement.
An object without targets selects automatic placement for the configured
scope:
{
"scope": "project",
"source": ".agents/skills",
"harnesses": [{ "name": "workbench", "useHarnessFolder": true }]
}To link to both locations regardless of the top-level scope, select them explicitly:
{
"scope": "project",
"source": ".agents/skills",
"harnesses": [
{
"name": "workbench",
"useHarnessFolder": true,
"targets": [
{ "placement": "project" },
{ "placement": "user" }
]
}
]
}The placement IDs in targets refer to placements[].id in the adapter; they
are not filesystem paths.
Example: typed adapter with compatibility and environment overrides
TypeScript provides editor and compiler feedback through Distributor's public
HarnessConfig type. This more complete adapter models a native directory, a
legacy compatible directory, and a configurable user directory. Create
.distributor/adapters/acme-agent.ts:
import type { HarnessConfig } from "@sunnygg/distributor";
const adapter = {
name: "acme-agent",
displayName: "Acme Agent",
adapterStatus: "available",
supportsNativeSkills: true,
defaultProjectPlacementId: "project",
placements: [
{
id: "project",
item: "skills",
support: "native",
scope: "project",
defaultPath: ".acme/skills",
createIfMissing: true,
notes: "Preferred repository-local directory.",
},
{
id: "legacy-project",
item: "skills",
support: "compatibility",
scope: "project",
defaultPath: ".acme-agent/skills",
createIfMissing: true,
notes: "Directory recognized by older Acme Agent releases.",
},
{
id: "user",
item: "skills",
support: "native",
scope: "user",
defaultPath: "~/.config/acme-agent/skills",
environmentVariables: ["ACME_AGENT_SKILLS_DIR"],
createIfMissing: true,
},
],
sources: ["https://acme-agent.example/docs/skills"],
verifiedAt: "2026-07-22",
} satisfies HarnessConfig;
export default adapter;Replace the example documentation URL and verification date with the real harness documentation and the date on which you checked it. The paths resolve as follows:
.acme/skillsbecomes/work/my-app/.acme/skills..acme-agent/skillsbecomes/work/my-app/.acme-agent/skills.~/.config/acme-agent/skillsbecomes, for example,/home/alex/.config/acme-agent/skills.- If the
userplacement is selected explicitly andACME_AGENT_SKILLS_DIR=/mnt/acme-skills, it uses/mnt/acme-skillsinstead of itsdefaultPath.importalso scans that location. Distributor uses the first non-empty variable listed inenvironmentVariables.
A project configuration can select the legacy placement and override its root:
{
"scope": "project",
"source": "$PROJECT_ROOT/.agents/skills",
"harnesses": [
{
"name": "acme-agent",
"useHarnessFolder": true,
"targets": [
{
"placement": "legacy-project",
"path": "$PROJECT_ROOT/.local/acme-skills"
}
]
}
]
}Here, both $PROJECT_ROOT values expand to /work/my-app. The source is still
/work/my-app/.agents/skills; the selected target is
/work/my-app/.local/acme-skills, not the placement's default
/work/my-app/.acme-agent/skills. An explicit target path takes priority over
both environmentVariables and defaultPath.
Adapter field reference
| Field | Meaning |
| ----- | ------- |
| name | Unique lowercase kebab-case harness ID used in harnesses and --harness. It must not duplicate a built-in or custom adapter ID. |
| displayName | Human-readable name shown by Distributor. |
| adapterStatus | Use available for a working adapter. planned and blocked entries are catalog metadata and cannot be selected for sync. |
| supportsNativeSkills | Whether the harness itself supports the Agent Skills format. |
| defaultProjectPlacementId | ID of the native or compatible project placement used automatically for project scope. It may be omitted only when an available adapter has exactly one placement. |
| placements | Non-empty list of directories the harness can read. Placement IDs must be unique within the adapter. |
| sources | Optional non-empty list of documentation URLs used to establish the adapter metadata. |
| verifiedAt | Optional ISO date (YYYY-MM-DD) when those sources and paths were verified. |
Each placement contains:
| Field | Meaning |
| ----- | ------- |
| id | Name used by targets[].placement, such as project, legacy-project, or user. |
| item | Kind of content at this location. Use skills; import scans only placements whose item is skills. |
| support | native when documented by the harness, compatibility when the harness intentionally recognizes another layout, or unverified when support has not been confirmed. Automatic placement and import use native or compatible skill placements. |
| scope | project for a repository-local path or user for a per-user path. Custom sync targets are limited to these scopes. |
| defaultPath | Target root used when project configuration does not supply an explicit path. Relative values resolve from the project root. ~, $HOME, and $PROJECT_ROOT are supported. |
| environmentVariables | Optional ordered list of environment variables used by explicit placement resolution and import; the first non-empty value replaces defaultPath. An explicit target path still takes priority. |
| createIfMissing | Whether Distributor may create missing target directories. Set it to false only when the harness must create the directory itself. |
| notes | Optional explanation of requirements, versions, or compatibility behavior. |
JSON files contain the adapter object directly. JavaScript and TypeScript files must default-export it and are trusted executable code: they run when Distributor loads them. Commit and use executable adapters only when you trust their source.
New initialization metadata keeps .distributor/adapters visible to Git while
ignoring local state. Existing .distributor/.gitignore files may need:
!adapters/
!adapters/**If an adapter is not found, first confirm the command's working directory. If
it fails validation, check that every required field shown above is present,
that all IDs are unique, that an available multi-placement adapter names a
valid project default, and that the adapter's name does not collide with a
built-in adapter.
Available adapters and placements
| Harness ID | Native project behavior (useHarnessFolder: true) | Explicit selectable placements |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| codex | Uses .agents/skills; global harness-folder sync uses .codex/skills | project, user (~/.codex/skills), agents-user (~/.agents/skills), admin |
| claude-code | Uses .claude/skills; the default source is linked there | project (.claude/skills), user (~/.claude/skills) |
| opencode | Uses .opencode/skills, but also recognizes .agents/skills and .claude/skills | project, agents-project, claude-project, user, agents-user, claude-user |
| cursor | Uses .cursor/skills and recognizes .agents/skills | project, agents-project, user, agents-user |
| gemini-cli | Uses .gemini/skills and recognizes .agents/skills | project, agents-project, user, agents-user |
| antigravity | Uses .agents/skills; the default source is already satisfied | project, legacy-project, user |
| github-copilot | Uses .github/skills and recognizes shared Agent/Claude paths | project, agents-project, claude-project, user, agents-user |
| openhands | Uses .agents/skills; the default source is already satisfied | project, legacy-project, user, openhands-user |
| pi | Uses .pi/skills and recognizes .agents/skills | project, agents-project, user, agents-user |
| cline | Uses .cline/skills and recognizes Cline Rules and Claude paths | project, clinerules-project, claude-project, user |
| goose | Uses .agents/skills; requires the Summon extension | project, goose-project, claude-project, user, claude-user |
| crush | Uses .crush/skills and recognizes shared Agent/Claude/Cursor paths | project, agents-project, claude-project, cursor-project, user, agents-user, claude-user |
| qwen-code | Uses .qwen/skills | project, user |
| kilo-code | Uses .kilo/skills and recognizes the shared Agent path | project, agents-project, user |
| roo-code | Uses .roo/skills and recognizes .agents/skills | project, agents-project, user, agents-user |
| trae-agent | Uses .trae/skills | project, user |
With useHarnessFolder: false, automatic resolution uses the compatible
.agents/skills placement shown in the table when one exists. Otherwise it
uses the same native placement as true. An explicit target array uses exactly
the placements listed in that array.
Project placements stay inside the project unless an explicit path says
otherwise. Each adapter declares its user paths; inspect the table's placement
IDs or the harness specification before selecting one explicitly.
If a project-local source is linked to a target outside the project, Distributor uses an absolute source link.
Ownership, conflicts, and stale targets
Distributor records ownership in <project-root>/.distributor/state.json.
This is local implementation state and should not be committed. A state entry
grants ownership only while the target remains a symlink with the exact raw
link value Distributor recorded.
- An existing exact expected symlink can be adopted without replacing it.
- An unmanaged file, directory, different symlink, or user-modified managed symlink is a conflict and is never overwritten.
- Planning inspects every requested harness before applying. One conflict makes the entire plan non-applicable, with no target or state writes.
- Independent apply-time failures may leave other successful operations in
place and recorded; the run exits
1and remains safe to retry. - A managed target becomes stale when its source disappears, its harness is removed, or its placement changes. Stale targets are removed during sync after Distributor verifies that the recorded link is unchanged.
distributor removeremoves every recorded target that is still the exact symbolic link Distributor recorded. Missing targets are cleared from state; changed links and non-links are reported and preserved. Empty target directories are left in place.
Exit codes
| Code | Meaning |
| ---- | -------------------------------------------------------------------- |
| 0 | Success, including no-op, warning-only, and stale-only runs |
| 1 | Operational, source, state, conflict, symlink, or filesystem failure |
| 2 | Invalid command usage or invalid/missing project configuration |
Initial-release scope
All sixteen harnesses in the configuration specification are available. This release intentionally has no force mode, copy or junction fallback, automatic cleanup during sync, transforms, generated artifacts, or content/frontmatter rewriting. Distributor creates direct directory symlinks for skill and helper folders, and file symlinks only for top-level helper files.
