MYAAH! Your skills are mine… to sync.
Remote skills, agents and rules for Claude Code and Codex — declared once, synced on every session, templated per project.
skilletor is a Claude Code and Codex plugin plus a standalone CLI. It installs skills, agents and rules from configured sources and keeps them up to date: it reconciles at session start, re-checks (throttled) during the session, pulls anything new, and tells the model what changed. It is the preferred successor to manage-skills (which stays around).
/plugin marketplace add Getty/marketplace
/plugin install skilletor@getty
skilletor add shared Getty && skilletor install perl-moo@sharedadd registers (and trusts) a source; install declares an item and syncs it onto disk.
Using Codex? See Codex — the plugin works there too, with a few differences.
source (git | https tarball | local dir)
│ resolve + scan
▼
render (.njk → Nunjucks, everything else copied)
│ diff against disk + lock
▼
~/.claude/{skills,agents,rules} · <project>/.claude/{skills,agents,rules}
~/.agents/skills, $CODEX_HOME/{agents,skilletor-rules.md} · <project>/{.agents/skills,.codex/{agents,skilletor-rules.md}}
- SessionStart — check every source (5s timeout each); on a change, sync and report.
- UserPromptSubmit — when the throttle is due, kick off a detached background sync and deliver its result on a later prompt; otherwise return instantly and silent.
Skills become usable immediately; agents and rules activate after /reload-plugins or a
restart — the report says so per item.
A source is a git repo, an HTTPS .tar.gz, or a local directory. skilletor add
resolves a shorthand once and stores the explicit form:
<spec> |
resolves to |
|---|---|
Getty |
git: https://github.com/Getty/skills |
Getty/repo |
git: https://github.com/Getty/repo |
hf.co/user |
git: https://hf.co/user/skills |
gitlab.com/u/r |
git: https://gitlab.com/u/r |
host.tld/ |
https://host.tld/skills (probed: git, then .tar.gz) |
https://…/x.tar.gz |
url (HTTPS tarball) |
~/dev/skills, ./path, /abs |
local |
A source's layout (convention, no manifest):
skills/<name>/SKILL.md[.njk] + companion files
agents/<name>.md[.njk]
rules/<name>.md[.njk]
bundles/<name>.yaml # optional: named sets of items (see Bundles)
snippets/… # includes only, not installable
skilletor.json # optional: { "description": "…", "vars": { defaults } }
skilletor.json at three levels; the scope follows from which file declares an item:
| File | Scope | Installs into (Claude Code; Codex: see Codex) |
|---|---|---|
~/.claude/skilletor.json |
user | ~/.claude/{skills,agents,rules} |
<project>/.claude/skilletor.json |
project (committed) | <project>/.claude/{skills,agents,rules} |
<project>/.claude/skilletor.local.json |
machine-local (not committed) | as project |
{
"sources": {
"shared": { "git": "https://github.com/Getty/skills", "ref": "main" },
"team": { "url": "https://skills.example.com/skills.tar.gz" },
"mine": { "local": "~/dev/my-skills" }
},
"install": {
"skills": ["perl-moo@shared"],
"agents": ["karr@shared"],
"rules": ["commit-style@team"],
"bundles": ["perl@shared"]
},
"vars": { "kubernetes": true },
"gitignore": true,
"checkInterval": 1800
}Only skill, agent and rule are installable — skilletor never syncs hooks,
settings.json or MCP configs. An optional "targets" key picks the harnesses to install
for (claude, codex) — see Codex.
skilletor uninstall removes explicit entries from one config — the user config, or the
project config with --project. rule:k8s@shared removes only from rules;
k8s@shared removes from every list. Every item is checked before anything is edited:
an item with no explicit entry in that config fails the command (exit 1, config
untouched), and the error names where it comes from instead — a wildcard, a bundle,
another type's list, or the other scope's config.
"*@source" in an install list declares every item of that type the source offers; *
may stand anywhere in the name, so a pattern picks the matching ones:
{ "install": { "rules": ["*@shared"], "skills": ["perl-*@shared"] } }"rule:*@shared" is the same with an explicit type ("rule:*-style@shared" for a
pattern). On the command line the type prefix is required, and the argument must be
quoted so the shell does not glob it:
skilletor install 'rule:*@shared' 'skill:perl-*@shared' [--project]
skilletor uninstall 'rule:*@shared' [--project]A wildcard is expanded against the source on every sync, so items added upstream arrive
on the next sync and items removed upstream are deleted. A pattern that matches nothing
warns (skill wildcard zzz-*@shared matches nothing in source shared); a bare * stays
silent, since a source without rules is normal. When names overlap (per type and scope)
nothing fails:
- an explicit entry and a wildcard over the same source name the same item → the explicit entry is used, silently;
- an explicit entry and another source's wildcard → the explicit entry wins, the report warns;
- two wildcards or bundles of the same source yield the same name (
perl-*and*-styleboth matchperl-style) → installed once, silently; - wildcards or bundles of different sources offer the same name → that one name is skipped with a warning (an installed copy stays as it is); every other item proceeds.
An item installed through a wildcard cannot be uninstalled by name — the wildcard would
bring it back. Uninstall the wildcard (skilletor uninstall 'rule:*@shared'), or keep it
and switch the item off via vars (empty render = skipped).
Uninstalling an explicit entry that a wildcard also covers works, with a warning that the
item returns on the next sync.
Declaring the same wildcard twice in one scope is a config error. If a source cannot be
resolved (offline without cache, untrusted, broken layout), everything it installed —
explicitly or through a wildcard — stays in place; deletion only follows a successful
resolve. skilletor status marks such items via *@shared (or via perl-*@shared) and
adds one line per wildcard, e.g. * rules/* @shared (3 installed).
A source can name a set of items in bundles/<name>.yaml (or .yml — not both), so a
project declares one entry instead of repeating a list:
# bundles/perl.yaml in github.com/Getty/skills
description: Everything for Perl projects
skills:
- "perl-*" # quote a leading * in a block list
- testing
- karr@gitlab.com/peter # an item of another source, by address
agents: [perl-reviewer]
rules: ["*-style"]
bundles: [base] # another bundle of this source, included
vars:
perl_version: "5.40"description is required; skills, agents, rules take names and patterns (bare =
this source); bundles takes bare names of this source's bundles, nested recursively
(a cycle is an error); vars sets defaults for the items this bundle yields. Any other
key is an error. The YAML is skilletor's own subset: no anchors, aliases or tags — which
is why an unquoted - *-style fails to parse and must be written - "*-style".
skilletor install bundle:perl@shared [--project] # adds "perl@shared" to install.bundles
skilletor uninstall bundle:perl@shared [--project]perl@shared without the prefix works too when no skill, agent or rule of shared is
called perl; otherwise the command fails as ambiguous and names both forms
(skill:perl@shared, bundle:perl@shared). Bundles are expanded on every sync, like
wildcards, and their items follow the same overlap rules. A name the source lacks or a
pattern that matches nothing warns; a broken bundle (missing, unparseable, cycle, bad
address) affects only itself, and what it installed before stays.
Vars. For an item a bundle yields: source defaults < bundle vars < user < project < local. Bundle vars reach only that bundle's items. Along a chain the outer bundle wins
(perl over the base it includes). If one item is reached through two chains that set
a key differently, neither value applies — the item falls back to the source default and
the report warns, naming both bundles. An item you declare explicitly gets no bundle vars,
even when a bundle yields it too.
Items of other sources. name@<spec> (patterns too: perl-*@gitlab.com/peter) names
an item of another source by its address, so a bundle means the same on every machine.
<spec> is the skilletor add shorthand, limited to forms that need no network probe:
Getty, Getty/repo, gitlab.com/peter, hf.co/user, full https://… URLs. A generic
host must be written as a full https:// URL, and local paths are not allowed. The entry
is served by whichever configured source has the same resolved git/url — its config
name does not matter, and a local override of it (authoring mode) still applies.
If that source is missing:
skilletor install bundle:perl@sharedon a terminal asks per missing source —bundle perl needs a source you don't have yet: gitlab.com/peter → https://gitlab.com/peter/skills — add it as [peter]? (name, or n to skip)— and adds it likeskilletor add(which trusts it) to the config the bundle goes into.- Without a terminal the command changes nothing, exits 1 and prints the commands to run:
skilletor add peter gitlab.com/peter --project. syncand the hooks never add a source — a trusted source must not pull in an untrusted one, since rendering runs code. They warn once per missing source and bundle (bundle perl@shared needs gitlab.com/peter (https://gitlab.com/peter/skills): run skilletor install bundle:perl@shared), skip those items and keep installed copies.
Seeing and removing. skilletor available lists each bundle (type bundle) with its
description and expanded members; --json adds members and vars. skilletor status
marks items via bundle:perl@shared and adds * bundle:perl@shared (7 installed). An item
only a bundle declares cannot be uninstalled by name: the error names the bundle — uninstall
the bundle, or switch the item off via vars.
skilletor always reconciles at session start. During a session it also
re-checks your sources in the background, throttled by checkInterval (user
config only — in a project or local file it is a config error that stops skilletor
loading its config; seconds, default 1800 = 30 min). If that's too chatty for you:
{ "checkInterval": 0 } // turn the in-session check off entirelyWith 0 (or any value ≤ 0) skilletor only syncs at session start — no
background fetching while you work. Raise the number instead to check less often
(e.g. 3600 for hourly).
A source file ending in .njk is rendered with Nunjucks and installed with .njk
stripped; everything else is copied byte for byte, so skills that use {{ }}/{% %}
themselves stay intact.
{% if vars.kubernetes %}
Use the {{ project.name }} cluster in namespace {{ vars.k8s_namespace }}.
{% endif %}Context: vars.* (source defaults < bundle vars < user < project < local), project.* (project
scope), scope, harness (claude or codex), target.dir, host.*, user.*,
item.*. There is deliberately no env.*, and printing an undefined variable ({{ vars.x }}) is an error (so a typo can't
ship an empty skill). Testing one ({% if vars.x %}) is not: it is simply false.
If an item's main file is a template — SKILL.md.njk for a skill, <name>.md.njk for
an agent or rule — and it renders to nothing but whitespace (a leading frontmatter block
does not count), the item does not apply in that scope. It is skipped as a whole: nothing
is written (for a skill, none of its other files either), an already installed copy is
removed, and it is neither an error nor a warning. A main file that is not a template is
never skipped, even if empty. The sync report says · rules/k8s skipped (renders empty),
or - rules/k8s (removed: renders empty) when a copy was deleted; status shows
(skipped: renders empty).
That makes a source of small, gated rules — one per concern — switchable per config:
---
paths: ["**/*.yaml"]
---
{% if vars.kubernetes %}
Use kubectl --context {{ vars.k8s_context }}.
{% endif %}Install the whole set once with "rules": ["*@shared"] and turn individual rules on with
"vars": { "kubernetes": true } in the user, project or local config. k8s_context
is printed inside the gate, so it needs a default in the source's skilletor.json.
Vars follow the item's scope: a user-scope item sees only user vars (plus source
defaults); project and local vars switch items installed in project scope.
Maintaining a source? Override the same-named source with a local checkout in your
user config:
{ "sources": { "shared": { "local": "~/dev/skills" } } }skilletor then reads that directory directly and re-renders on every sync — edits land immediately. Push with git and every machine pulls them on the next check. Installed files are build artifacts; there are no hardlinks.
With "gitignore": true (default; project config only — in the user config it is a
config error that stops skilletor loading its config) skilletor keeps a marked block in
<project>/.claude/.gitignore listing the exact managed paths, the lock and
skilletor.local.json (and the same kind of block in <project>/.agents/.gitignore and
<project>/.codex/.gitignore for Codex items). Only skilletor.json is committed; your
own hand-written skills beside the managed ones stay version-controlled and are never touched. Set
"gitignore": false to commit everything instead (useful only for items without
machine-specific variables).
A file is skilletor-managed exactly when it appears in
<scope>/.claude/skilletor.lock.json. Managed files are overwritten on the next sync —
change them in the source, not in place.
skilletor installs for Claude Code, the OpenAI Codex CLI, or both — from the same sources
and the same skilletor.json. Config, lock and state stay under .claude/ for both
harnesses, even on a Codex-only machine.
Without a targets key skilletor installs for every harness it finds, by files the
harness itself creates (never ~/.claude/ alone — skilletor's own config lives there):
| Harness | In use when any of these exists |
|---|---|
claude |
~/.claude.json, ~/.claude/settings.json, ~/.claude/projects/ |
codex |
in $CODEX_HOME (default ~/.codex): config.toml, auth.json, sessions/, installation_id |
"targets": ["claude"], ["codex"] or ["claude", "codex"] overrides detection:
- Machine targets = the user config's
targetsif set, else the detected set. If that is empty,sync,checkandstatusstop with a config error naming the markers, and nothing is touched — settargetsin~/.claude/skilletor.json. - User scope installs for the machine targets.
- Project scope installs for the machine targets, narrowed by the project's
targets(skilletor.local.jsonif it has one, elseskilletor.json). A project can only narrow, never add a harness the machine does not use; an empty intersection installs nothing for the project and warns.
Detection runs on every sync, so a harness that disappears (say ~/.codex is deleted) has
its items removed on the next sync — pin targets if that is not what you want. Items of
a harness that is switched off are removed the same way.
<base> is ~ for user scope and the project root for project scope.
| Type | Claude Code | Codex |
|---|---|---|
| skill | <base>/.claude/skills/<name>/ |
<base>/.agents/skills/<name>/ |
| agent | <base>/.claude/agents/<name>.md |
user: $CODEX_HOME/agents/<name>.toml; project: <project>/.codex/agents/<name>.toml |
| rule | <base>/.claude/rules/<name>.md |
a section of $CODEX_HOME/skilletor-rules.md (user) or <project>/.codex/skilletor-rules.md (project), delivered by the SessionStart hook |
Every item is rendered once per target, with harness (claude or codex) in the
template context, so a source can branch on it. Gating a whole item works like any other
empty render: a skill whose SKILL.md.njk is
wrapped in {% if harness == "claude" %} is installed for Claude Code and skipped for
Codex. Skills are copied as they are — Codex accepts Claude's SKILL.md format.
The lock stays one file per scope (<base>/.claude/skilletor.lock.json); Codex entries
are keyed codex:skills/<name>, codex:agents/<name>, codex:rules/<name>, and the sync
report and status list them under those keys. Codex items are active from the next Codex
session.
Codex reads project agents from <project>/.codex/agents/ only in a project it
trusts; in an untrusted project it ignores .codex/ with a warning.
A Codex agent role is a TOML file. skilletor renders the agent's Markdown for codex and
converts it: name and description come from the frontmatter, the body becomes
developer_instructions. Codex-specific settings go under an optional codex: mapping,
passed through as top-level TOML keys (they override the three above):
---
name: reviewer
description: Reviews diffs for correctness and style.
model: sonnet
tools: Read, Grep
codex:
model_reasoning_effort: high
sandbox_mode: read-only
---
You review code. …becomes
name = "reviewer"
description = "Reviews diffs for correctness and style."
model_reasoning_effort = "high"
sandbox_mode = "read-only"
developer_instructions = '''
You review code. …
'''- Claude-only keys are not carried over —
model,tools,allowed-tools,colorand any other key outsidecodex:— and nothing is reported for them. There is no faithful mapping (model: sonnetmeans nothing to Codex); put Codex values undercodex:. codex:values may be strings, numbers, booleans, string arrays, or one level of tables of those. Anything else is dropped with a warning naming the key. skilletor does not check the values against Codex's schema — Codex ignores the whole role on a bad value.briefing.skillsis not written for Codex. Codex reads agent-role files strictly and ignores the whole role over any unknown key, a[briefing]table included; the sync report notes how many agents lost theirbriefing.skills.- An agent without a
descriptionis not written for Codex (warning) — Codex rejects such a role. An agent whose body is empty is skipped for Codex.
Codex has no rules directory, and AGENTS.md is usually committed, cannot be partly
ignored and is cut at project_doc_max_bytes — so no rule text goes there. skilletor
writes all of a scope's Codex rules into one rules file, and its SessionStart hook
hands that file to Codex:
| Scope | Rules file | Pointer block in |
|---|---|---|
| user | $CODEX_HOME/skilletor-rules.md |
$CODEX_HOME/AGENTS.md |
| project | <project>/.codex/skilletor-rules.md |
<project>/AGENTS.md |
<!-- skilletor:rules scope=project -->
<!-- managed by skilletor — edits are overwritten; change the rule in its source -->
<!-- skilletor:rule k8s source=shared -->
Applies when working with files matching: `k8s/**`, `*.yaml`.
Use kubectl carefully.-
One section per rule, sorted by name. The body is the rule rendered for
codexwithout its frontmatter; apaths:list (Codex cannot load a rule conditionally) becomes the leading "Applies when working with files matching" line. -
The file is wholly skilletor's. It exists exactly while the scope has a Codex rule and is deleted with the last one. A section edited or deleted by hand is restored on the next sync and reported as an overwritten local change (
.codex/skilletor-rules.md#rules/k8s). Change the rule in its source. -
Delivery. On
startupandclearthe hook puts the user rules file, then the project one, at the start of the session's context — read from disk, so a failed or timed-out sync still delivers the last good rules. Onresumeit adds nothing: the resumed history already holds the first copy, and a second would duplicate it.UserPromptSubmitnever adds rules. -
The pointer.
AGENTS.mdgets only a fixed block telling Codex to read the rules file when the hook's message is gone (after compaction, say). Its text never changes, so it appears in a diff only when a scope gains its first Codex rule or loses its last one:<!-- skilletor:begin --> <!-- managed by skilletor — edits inside are overwritten --> Additional rules for this project are managed by skilletor. They are normally provided at session start as a developer message beginning with `<!-- skilletor:rules`. If that message is not in your context (for example after context compaction), read `.codex/skilletor-rules.md` before you start a task, and follow it. <!-- skilletor:end -->
The user block says "for all projects" and names the user rules file by absolute path. Everything outside the markers is yours and never modified. A missing
AGENTS.mdis created with just the block; an existing one gets it appended. When the rules file goes, the block goes; a file left empty is deleted. -
Refusals skip only the pointer — the rules file and the hook delivery are unaffected. skilletor warns and leaves
AGENTS.mdalone;--forcedoes not override:- the markers are malformed:
beginwithoutend,endbeforebegin, either twice; AGENTS.mdis a symlink (commonlyAGENTS.md→CLAUDE.md: writing through it would send Claude Code to the Codex rules), a directory, or unreadable;- Claude Code is a target too and a
CLAUDE.mdit reads is the same file as thatAGENTS.md(CLAUDE.mdor.claude/CLAUDE.mdin a project,~/.claude/CLAUDE.mdfor$CODEX_HOME/AGENTS.md; linked or hard-linked).
- the markers are malformed:
-
Warnings: an
AGENTS.override.mdnext to the file (Codex reads the override instead, so the pointer is not seen); a projectAGENTS.mdlarger than Codex'sproject_doc_max_bytes(default 32768, read from$CODEX_HOME/config.toml) — Codex cuts project instructions from the end, where the pointer sits. -
Git: with
"gitignore": true(the default) the project rules file is listed in the managed.codex/.gitignoreblock, so rule text stays out of commits; only the small pointer block shows up in the diff ofAGENTS.md. -
Coming from an older skilletor: rules that used to sit as sections of the block in
AGENTS.mdmove to the rules file on the next sync, and the block is replaced by the pointer. The report lists the rules as updated, not as overwritten local changes.
The repository ships a Codex plugin manifest (.codex-plugin/plugin.json) next to the
Claude one; both ship the bundled skill. The Codex manifest points at its own
hooks/codex-hooks.json — the same hooks as hooks/hooks.json, run with --harness codex,
plus the rules delivery.
- Trust the hooks. Codex runs a plugin's hooks only after you trust them — open
/hooksin Codex and trust skilletor'sSessionStartandUserPromptSubmithooks. Until then Codex skips them without a word: no syncs, no rules. While Codex is a target and$CODEX_HOME/config.tomlholds no trust entry for skilletor'sSessionStarthook,syncandstatuswarn once per run — in--jsonoutput as a top-levelwarningsarray ({ "scopes": […], "warnings": ["Codex has not trusted …"] }, absent when empty; the per-scopewarningsofsyncare unchanged). An update that changes the hook file makes Codex mark the hook as modified; trust it again in/hooks. skilletor does not detect that case — the entry is still there. - The CLI is not on
PATHinside Codex (Codex cannot add a plugin'sbin/to it). The bundled skill tells the model to run it by absolute path, from the plugin's cache directory:$CODEX_HOME/plugins/cache/<marketplace>/skilletor/<version>/bin/skilletor. In your own shell, point an alias at that file or at a clone of this repository (alias skilletor=~/src/skilletor/bin/skilletor—dist/is committed, nonpm installneeded). Use an alias, not a symlink: the launcher findsdist/relative to its own path. - Codex sets no
CLAUDE_PROJECT_DIR; the hook takes the git top level of the session's working directory as the project root, so a session started in a subdirectory still finds<repo>/.claude/skilletor.json.
Trust means code execution — Nunjucks is not a sandbox, so a source can run code when rendered. This is the same trust level as installing a plugin.
- Adding a source with
skilletor add(or your own user config) trusts it. - A source that appears only in a project config (a cloned repo) is never fetched
until you run
skilletor trust <name>; if the project later changes the URL, trust lapses. - Fixed target directories per harness; item names, tar entries and includes may not escape the
source or target root; symlinks in sources are rejected;
urlis HTTPS-only.
manage-skills hardlinks a single source-of-truth file into each project. skilletor installs build artifacts (rendered per instance) from remote sources and keeps them current automatically. Use skilletor when you want templated, auto-updating items from a shared remote; manage-skills remains for hardlinked local sharing.
skilletor add [name] <spec> [--project] # add a source (resolves shorthand, trusts it), then sync
skilletor source list [--json] | source remove <name> [--project] [--force] # --force: even with installed items
skilletor available [source] [--json] # catalog of trusted sources
skilletor install <item>... [--project] # name@source (type:name@source if ambiguous), then sync
skilletor install 'rule:*@shared' [--project] # wildcard: every rule of the source (type prefix required)
skilletor install 'skill:perl-*@shared' # name pattern: every skill whose name matches
skilletor install bundle:perl@shared # a bundle; offers to add sources it needs (terminal only)
skilletor uninstall <item>... [--project] # [type:]name@source (no type: every list); wildcards, bundle: as written
skilletor sync | check | status # --scope user|project|all, --json, --project-dir <dir>
skilletor sync --force # overwrite and adopt unmanaged files reported as conflicts
skilletor trust <source>
check writes nothing and exits non-zero when a source has changed.
- Node.js ≥ 18 and
git. The only bundled runtime dependency is Nunjucks.
MIT.
