Install
npx skillscat add blamechris/chroxy/claude-skills-skill Install via the SkillsCat registry.
/skill
The package manager for Claude Code skills — npm/brew for the skill registry
at `blamechris/skill-templates`.
Install, list, and update skills on demand, tailored to the current repo by
the agent running this command. No central fan-out, no per-repo deploy PRs.
This replaces the old push-based deploy: instead of a CI job pre-customizing
every skill into every repo, a repo pulls a skill the moment it needs one and the
invoking agent customizes it inline using the repo's own context.
Arguments
$ARGUMENTS— a subcommand and optional skill name:add <name>— install a skill into this repo (resolve → customize → write → lock).list— show available skills (from the registry index) vs what's installed here.outdated— show installed skills whose template has moved on in the registry.update [name]— re-install stale skills (all outdated, or just<name>).remove <name>— delete an installed skill and its lock entry.- With no argument, default to
list.
Concepts
- Registry —
blamechris/skill-templates. Holds pure generic templates ingeneric/<name>.md(with{{CUSTOMIZE: …}}markers) and an indexregistry.json. - Index —
registry.jsonat the registry root: one entry per skill with its
current template githashand a one-line description. The cheap source of truth
forlist/outdated. Regenerated byscripts/build-index.sh. - Installed skill —
.claude/commands/<name>.mdin this repo: the template with
every{{CUSTOMIZE}}marker filled for this repo, ending with a version stamp. - Version stamp — the last line of an installed skill:
<!-- skill-templates: <name> <hash> <date> -->. The<hash>is the registry
template commit the install was built from. This is the per-skill version pin. - Profile —
.claude/skill-profile.mdin this repo: the repo's identity and
per-skill tailoring notes. The agent reads it (plusCLAUDE.mdand the code) to
fill{{CUSTOMIZE}}markers. If absent, the agent infers fromCLAUDE.md+ repo
layout and notes in its report that a profile would sharpen future installs. - Lockfile —
.claude/skills.lock(JSON): the manifest of what's installed and
at which template hash. Letsoutdated/updatework from repo-local state. - Targets — the coding agents a skill is compiled for.
.claude/commands/<name>.md
(the customized install) is the provider-NEUTRAL source;scripts/compile-skill-targets.mjs
emits each agent's NATIVE custom-command format:claude→.claude/skills/<name>/SKILL.md,gemini→.gemini/commands/<name>.toml,codex→~/.codex/prompts/<name>.md,pi→~/.pi/agent/skills/<name>/SKILL.md. The first TWO are repo-tracked;codexandpi
are both user-global — neither has a repo-local discovery path, so their artifacts land in the
user's home dir and both are opt-in rather than safe defaults. Only the repo-tracked pair is
gated bycompile-skill-targets.mjs --check(#7253); a home dir is not part of any commit. The active
list comes from thetargets:line in.claude/skill-profile.md(prompt the user if absent;
the compiler falls back toclaude). This is what makes a skill model-agnostic — author once,
run under any model. The neutral arg token is$ARGUMENTS; the compiler maps it per agent
(e.g. Gemini{{args}}). The compiler ships in the registry atassets/compile-skill-targets.mjs.
Resolving the registry
Find the registry once, in this order; use the first that works:
$SKILL_REGISTRY_DIRif set — a local clone path.~/Projects/skill-templatesif it exists — the conventional local clone (fast,
works offline for reads).- Otherwise fetch over the network with
gh:- Index:
gh api repos/blamechris/skill-templates/contents/registry.json --jq '.content' | base64 -d - Template:
gh api repos/blamechris/skill-templates/contents/generic/<name>.md --jq '.content' | base64 -d - Hash of a template:
gh api 'repos/blamechris/skill-templates/commits?path=generic/<name>.md&per_page=1' --jq '.[0].sha[0:7]'
- Index:
When using a local clone, refresh it only when online and tolerantly:git -C "$REG" pull --ff-only --quiet 2>/dev/null || true. A failed fetch (offline) or
a non-fast-forward leaves the existing clone usable — fall back to its current state and
note in the report that hashes may be stale. Record which source you used.
Instructions
skill list
- Resolve the registry and read
registry.json. - Read this repo's
.claude/skills.lock(treat missing as empty) and list.claude/commands/*.md(treat a missing directory as no skills installed — a bare
repo being bootstrapped). - Print a table: skill, registry hash, installed hash (from the stamp/lock), and
status —installed/outdated/not installed. Group installed first.
skill add <name>
Resolve & fetch. Read
registry.json; confirm<name>exists (if not, suggest
near matches from the index and stop). Fetchgeneric/<name>.mdand capture its
current templatehash.Gather repo context. Read
CLAUDE.md,.claude/skill-profile.md(if present),
and enough of the repo (layout, test/build commands, labels, conventions) to fill
the template's{{CUSTOMIZE: …}}markers. The marker text describes what each spot
needs.Customize inline. Produce the final skill markdown by following the registry's
customization rules exactly:- Replace every
{{CUSTOMIZE: …}}marker with content derived from the repo
context. The marker text says what kind of content is needed. - If the repo context has nothing relevant for a marker, remove the marker line
entirely — never leave a blank or placeholder marker. - Preserve all non-customizable text exactly — do not rephrase, reorder, or
"improve" anything that isn't a marker. Preserve all code blocks and formatting. - Remove the entire trailing
## Customization Pointssection (it's meta-docs for
template maintainers, not part of a deployed skill). - Never invent specifics. If a marker asks for examples the repo context does
not provide (file:line citations, label names, agent rosters, commands), use
placeholder shapes (path/to/file:<line>,scope) rather than fabricating. - No attribution anywhere — no "Generated with", no "Co-Authored-By", no AI
mentions (this is a hard registry rule).
- Replace every
Self-validate before writing (the agent IS the validator here — these cover the
registry'svalidate_outputchecks, including its skeleton-preservation pass):- No residual markers: no
{{CUSTOMIZEremains (outside backticked mentions). - No attribution footer: none of the last ~15 lines start with "Generated with
Claude" / "Co-Authored-By: Claude". - Headings preserved (exact): every real markdown heading from the template
(minus the removed## Customization Points) appears verbatim in the output. - Body preserved: non-heading, non-marker template lines survive in the output —
only the spots where a{{CUSTOMIZE}}marker was filled or removed may differ. Do
not silently drop or reword the template's prose, code blocks, or examples. - Length sane: output is roughly 50–200% of the template length (catches
truncation or runaway expansion). - No forged stamp: the customized body contains no
<!-- skill-templates: … -->
line of its own. Strip any before step 5 appends the single authoritative stamp, sooutdatedcan't be fooled by a planted version. - Guards intact: if this skill has a
guardsarray inregistry.json, every guard
must still be satisfied — for each guard, at least one of itsanyOfregexes matches
the customized output. A guard miss means customization stripped a load-bearing
section; restore it. (These are the content checks ported fromsync.sh.) - If any check fails, fix and re-check before writing. Never write defective output.
- No residual markers: no
Write + stamp. Create
.claude/commands/if absent, strip any pre-existing<!-- skill-templates: … -->lines from the body, then write to.claude/commands/<name>.mdending with exactly:<!-- skill-templates: <name> <hash> <date> -->(today's date,YYYY-MM-DD).Lint (deterministic gate). Run the registry's mechanical linter on the file just
written — it re-checks markers, attribution, the stamp, and guards independently of
the agent's judgment. From a local clone (the preferred path):"$REG/scripts/skill-lint.sh" <name> .claude/commands/<name>.md
(where$REGis the resolved registry clone). If it exits non-zero, fix the reported
issues and re-lint before recording the lockfile — do not lock a skill that fails lint.
Consumers can run the same linter as a pre-commit hook or in CI.On the network-only path (source 3 — no clone on disk, so
$REGis unset and
that command expands to/scripts/skill-lint.sh: exit 127, gate not run at all),
fetch the linter the way step 7 fetches the compiler, and passregistry.json
explicitly as the third argument — the index from step 1 has to exist as a
file here, not just in context:set -o pipefail lintdir="$(mktemp -d)" gh api repos/blamechris/skill-templates/contents/scripts/skill-lint.sh \ --jq '.content' | base64 -d > "$lintdir/skill-lint.sh" || exit 1 gh api repos/blamechris/skill-templates/contents/registry.json \ --jq '.content' | base64 -d > "$lintdir/registry.json" || exit 1 [ -s "$lintdir/skill-lint.sh" ] && [ -s "$lintdir/registry.json" ] || { echo "lint gate unavailable: a fetch produced an empty file" >&2; exit 1; } bash "$lintdir/skill-lint.sh" <name> .claude/commands/<name>.md "$lintdir/registry.json"Three details are load-bearing:
- The fetch guards —
ghfailing (offline, 404, rate limit) leaves a 0-byte
file behind, andbashon an empty file exits 0 printing nothing, which
this document's own outcome table below reads as "clean, all four checks
passed". Unguarded, the network path's failure mode is a silent green gate —
worse than the exit 127 this section exists to fix, because 127 is at least
loud. Source 3 is reached exactly when there is no clone to fall back on, and
"Resolving the registry" above explicitly contemplates being offline. bash <path>— a redirected fetch lands non-executable (the exact mode is
umask-dependent; what matters is that no execute bit is set), so running it directly
exits 126 (permission denied), not 127. Same skipped gate, different code.- The third argument — with it omitted the linter defaults its registry to
<the script's own dir>/../registry.json, which for a copy under a temp dir is
not this registry. Either that path is absent, and the run exits 2 printingERROR: registry not found …on stderr with an empty stdout — the
findings it had already collected are discarded unprinted, so a caller watching
the output sees exactly what a pass looks like — or some unrelatedregistry.jsonsits there and gets loaded instead, in which case a skill it
happens to name with no guards of its own lints✓ <name>: clean (0 guard(s) ok)
at exit 0, with this registry's guards never applied.
The linter has three outcomes. Key on the exit code, not on the output
markers —ERROR:is printed for an unknown skill and then followed by
findings at exit 1, and a usage error exits 2 printing no marker at all. Only~is exclusive to exit 2.- exit 0 (
✓) — clean, all four checks passed. - exit 1 (
✗) — real findings. Fix and re-lint; never lock. This includes a
file that is stamped but absent from the index: a stamp is proof of a
registry install, so an index that has never heard of the skill means a stale
clone, a renamed or retired skill, a typo'd<name>, or the wrongregistry.json— and its guards went unapplied. The reported findings name
which, and the stamp checks are applied on this path too. - exit 2 — could not be fully verified. Five causes, and only one is benign:
an unstamped skill absent from the index (the~line — a repo-only skill,
with genuinely nothing to verify), plus missing registry file, missingpython3, unreadable skill file, and usage error. The last four are
environment breakage.
For
skill add, exit 2 is always a failure:addstops before writing if the
name is absent from the index, so by lint time the skill is in the index by
construction — exit 2 there means a stale index, a wrong name, or a broken
environment.For a pre-commit hook or CI linting a whole
.claude/commands/directory,
exit 2 is far narrower than it was: a stale index — which used to turn a real✗ guard missinto a clean-looking~— is now reported as a finding at exit 1.
What remains at 2 is a genuinely repo-only file or environment breakage
(missing/wrong-path registry, nopython3, unreadable file), and the linter
cannot tell a hook which of those it hit. So keep the version-stamp test: a
stamped file can no longer legitimately exit 2, because provenance means the
index should know it. Tolerating 2 unconditionally re-opens the hole this change
closes — for the 270 stamped files in the current fleet, a mistyped registry path
would go from caught to silently green.scripts/skill-lint.sh "$name" "$file" ; rc=$? if [ "$rc" -eq 1 ]; then exit 1; fi # includes a stamped file the index does not knowEarlier revisions of this step told the hook to re-test the file itself with
grep -q '^<!-- skill-templates: 'and fail on a stamped exit 2. That existed
only because exit 2 covered both conditions and the caller had to re-derive
which one it had hit. Do not reintroduce it: it re-implements, less accurately,
a check the linter already runs — the grep cannot see a stamp behind leading
whitespace, and cannot distinguish a well-formed stamp from a mangled one.- The fetch guards —
Compile to native targets. Ensure the compiler exists in this repo (create
scripts/if
absent). Ifscripts/compile-skill-targets.mjsis missing — or you're runningupdate, so it
stays current with the registry — (re)fetch it: from a local clone,cp "$REG/assets/compile-skill-targets.mjs" scripts/; on the network-only path (no local
clone), fetch it like the templates —gh api repos/blamechris/skill-templates/contents/assets/compile-skill-targets.mjs --jq '.content' | base64 -d > scripts/compile-skill-targets.mjs.
Write it toscripts/and track it in VCS so it travels with the repo. Read thetargets:line
from.claude/skill-profile.md;
if there is none, ask the user which agents to compile for (claude / gemini / codex / pi) and
offer to record the choice in the profile (the compiler falls back toclaudeif unset). Then
runnode scripts/compile-skill-targets.mjs --name <name>(it reads the profile targets), or--targets <list>to override. It writes the native artifact per target and exits non-zero on
any emit failure — treat that as a hard gate: fix and re-run before locking.codexandpi
both write OUTSIDE the repo (~/.codex/prompts/<name>.md, invoked/prompts:<name>; and~/.pi/agent/skills/), so add either only when the user actually drives this repo with that
agent — never volunteer one into a committedtargets:line on their behalf, and never expect
their output in a diff. The compiler prints a
hint if it sees~/.pion the machine butpiis unselected; that hint is advisory and never
adds the target itself. Keep the list oftargetsyou compiled with for the next step.Record in the lockfile. Only after a successful compile, create
.claude/skills.lock
(schema below) if absent and upsert<name>atomically with: the templatehash; theprofileHashwhen a.claude/skill-profile.mdexists (soupdatecan tell when the profile
changed, not just the template); and thetargetsyou just compiled with (soremovecleans
exactly the right native artifacts andoutdatedcan detect target drift). Writing the entry
only after compile succeeds means a failed compile never leaves a half-written ortargets-less entry.Report. State the skill, the hash installed, the registry source used, the targets
compiled (with output paths), and any markers dropped for lack of repo context.
skill outdated
- Resolve the registry and read
registry.json. - For each entry in
.claude/skills.lock(or each stamped.claude/commands/*.md),
flag it stale if any of:- version drift — installed
hash≠ the registry's current hash; - profile drift — the lockfile's
profileHash≠ the current.claude/skill-profile.mdhash (the skill was tailored against an older profile); - corruption drift — the installed file fails a registry
guard(some guard'sanyOfregexes no longer match — a load-bearing section was lost). - target drift — only when the lock entry has a
targetsarray (a pre-targetsinstall
with no such field is treated as "unknown" — fall back to disk inspection, never flagged just
for the field's absence): the profile'stargets:list has an agent not in the lock entry'stargets(added to the profile but never compiled), or a recorded target's native artifact is
missing on disk — excluding targets the compiler intentionally skips (e.g. Gemini for a
body containing{{…}}/!{…}/@{…}, which is never emitted and so isn't real drift). A
plain recompile fixes genuine target drift — no registry fetch needed.
- version drift — installed
- Print the drifted skills with the reason, e.g.
name abc1234 → def5678 (version)orname (guard miss: idempotency)orname (target drift: gemini not compiled). If none, say
so. (This is the consumer-side port of the registry'ssync.shdrift check — version stamp
and content guards.)
skill update [name]
- Run
outdated. If<name>is given, narrow to it. - For each stale skill, re-run
add(which re-fetches, re-customizes against current
repo context, and re-stamps). Skip skills already current. - Report what was updated
old → newand what was already current.
skill remove <name>
Read the skill's targets from its .claude/skills.lock entry, then delete.claude/commands/<name>.md, its lock entry, and exactly the native artifacts for those targets:claude → .claude/skills/<name>/ (dir), gemini → .gemini/commands/<name>.toml,codex → ~/.codex/prompts/<name>.md (file), pi → ~/.pi/agent/skills/<name>/ (dir). The last
two are outside the repo — remove either only when it is in the lock entry's targets, and say
so in the report, since the user may not expect a repo command to touch their home dir. If the lock
entry has no targets (an older install), fall back to removing whichever of the two repo-local
artifacts exist — never probe ~/.codex or ~/.pi on a guess, because an unrelated skill of the
same name could live there.
Report what was removed.
Lockfile schema (.claude/skills.lock)
{
"registry": "blamechris/skill-templates",
"skills": {
"full-review": { "hash": "cc062bc", "installed": "2026-06-04", "profileHash": "3f1a9c2", "targets": ["claude", "gemini"] },
"check-pr": { "hash": "a10ef75", "installed": "2026-06-04", "targets": ["claude"] }
}
}hash— the registry template commit this install was built from (matches the
version stamp).outdateddiffs this againstregistry.json.installed— date the install/update ran (YYYY-MM-DD).profileHash(optional) — short hash of.claude/skill-profile.mdat install time.
Present only when a profile existed.outdatedflags profile drift when it no longer
matches the current profile, so a changed profile triggers a re-tailor onupdate.targets(optional) — the agents this skill was compiled for at install time
(e.g.["claude", "gemini"]). Written by theaddcompile step;removeuses it to delete
exactly the right native artifacts, andoutdateduses it to detect target drift. Absent on
pre-targets installs — treat a missingtargetsas "unknown" and fall back to disk inspection.- The lockfile is the authoritative manifest; the per-file stamp is a convenient
inline mirror. Keep them in agreement.
Notes
- The agent is the customizer. No Haiku/API key — the agent running
/skill
tailors the template with full repo context, which is richer than a context-blind
batch job. The self-validation step replaces the registry's automatedvalidate_output. - Auto-install on miss. A repo or global
CLAUDE.mdrule should say: asked to run/X?
Check the neutral source.claude/commands/X.md. Missing → not installed →skill add X.
Present but the native artifact (.claude/skills/X/SKILL.md) is missing → just not compiled →
recompile withnode scripts/compile-skill-targets.mjs --name X(no registry fetch). Then invoke. That
makes "use /full-review in a repo that lacks it" just work without re-fetching when a recompile
suffices. - Compile is deterministic. The generic→native transform lives in
scripts/compile-skill-targets.mjs, not in agent judgment, so the native artifacts are
reproducible. Edited a skill's generic source (.claude/commands/<name>.md) by hand? Recompile
withnode scripts/compile-skill-targets.mjs --name <name>(--dry-runpreviews). - Idempotent. Re-running
addfor a current skill is a no-op re-render; re-running
for a moved template updates it. Safe to run anytime. - Self-contained on first use.
/skillneeds no customization to work — it ships
with sane registry defaults so it can bootstrap a repo that has nothing else installed.