blamechris

/skill

"The package manager for Claude Code skills — `npm`/`brew` for the skill registry at..."

blamechris 7 1 Updated 2mo ago
GitHub

Install

npx skillscat add blamechris/chroxy/claude-skills-skill

Install via the SkillsCat registry.

SKILL.md

/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 in
    generic/<name>.md (with {{CUSTOMIZE: …}} markers) and an index registry.json.
  • Index — registry.json at the registry root: one entry per skill with its
    current template git hash and a one-line description. The cheap source of truth
    for list/outdated. Regenerated by scripts/build-index.sh.
  • Installed skill — .claude/commands/<name>.md in 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.md in this repo: the repo's identity and
    per-skill tailoring notes. The agent reads it (plus CLAUDE.md and the code) to
    fill {{CUSTOMIZE}} markers. If absent, the agent infers from CLAUDE.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. Lets outdated/update work 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; codex and pi
    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 by compile-skill-targets.mjs --check (#7253); a home dir is not part of any commit. The active
    list comes from the targets: line in .claude/skill-profile.md (prompt the user if absent;
    the compiler falls back to claude). 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 at assets/compile-skill-targets.mjs.

Resolving the registry

Find the registry once, in this order; use the first that works:

  1. $SKILL_REGISTRY_DIR if set — a local clone path.
  2. ~/Projects/skill-templates if it exists — the conventional local clone (fast,
    works offline for reads).
  3. 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]'

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

  1. Resolve the registry and read registry.json.
  2. 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).
  3. Print a table: skill, registry hash, installed hash (from the stamp/lock), and
    status — installed / outdated / not installed. Group installed first.

skill add <name>

  1. Resolve & fetch. Read registry.json; confirm <name> exists (if not, suggest
    near matches from the index and stop). Fetch generic/<name>.md and capture its
    current template hash.

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

  3. 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 Points section (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).
  4. Self-validate before writing (the agent IS the validator here — these cover the
    registry's validate_output checks, including its skeleton-preservation pass):

    • No residual markers: no {{CUSTOMIZE remains (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, so
      outdated can't be fooled by a planted version.
    • Guards intact: if this skill has a guards array in registry.json, every guard
      must still be satisfied — for each guard, at least one of its anyOf regexes matches
      the customized output. A guard miss means customization stripped a load-bearing
      section; restore it. (These are the content checks ported from sync.sh.)
    • If any check fails, fix and re-check before writing. Never write defective output.
  5. Write + stamp. Create .claude/commands/ if absent, strip any pre-existing
    <!-- skill-templates: … --> lines from the body, then write to
    .claude/commands/<name>.md ending with exactly:
    <!-- skill-templates: <name> <hash> <date> --> (today's date, YYYY-MM-DD).

  6. 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 $REG is 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 $REG is 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 pass registry.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 — gh failing (offline, 404, rate limit) leaves a 0-byte
      file behind, and bash on 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 printing
      ERROR: 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 unrelated
      registry.json sits 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 wrong
      registry.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, missing
      python3, unreadable skill file, and usage error. The last four are
      environment breakage.

    For skill add, exit 2 is always a failure: add stops 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 miss into 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, no python3, 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 know

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

  7. Compile to native targets. Ensure the compiler exists in this repo (create scripts/ if
    absent). If scripts/compile-skill-targets.mjs is missing — or you're running update, 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 to scripts/ and track it in VCS so it travels with the repo. Read the targets: 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 to claude if unset). Then
    run node 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. codex and pi
    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 committed targets: line on their behalf, and never expect
    their output in a diff. The compiler prints a
    hint if it sees ~/.pi on the machine but pi is unselected; that hint is advisory and never
    adds the target itself. Keep the list of targets you compiled with for the next step.

  8. Record in the lockfile. Only after a successful compile, create .claude/skills.lock
    (schema below) if absent and upsert <name> atomically with: the template hash; the
    profileHash when a .claude/skill-profile.md exists (so update can tell when the profile
    changed, not just the template); and the targets you just compiled with (so remove cleans
    exactly the right native artifacts and outdated can detect target drift). Writing the entry
    only after compile succeeds means a failed compile never leaves a half-written or
    targets-less entry.

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

  1. Resolve the registry and read registry.json.
  2. 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.md hash (the skill was tailored against an older profile);
    • corruption drift — the installed file fails a registry guard (some guard's
      anyOf regexes no longer match — a load-bearing section was lost).
    • target drift — only when the lock entry has a targets array (a pre-targets install
      with no such field is treated as "unknown" — fall back to disk inspection, never flagged just
      for the field's absence): the profile's targets: list has an agent not in the lock entry's
      targets (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.
  3. Print the drifted skills with the reason, e.g. name abc1234 → def5678 (version) or
    name (guard miss: idempotency) or name (target drift: gemini not compiled). If none, say
    so. (This is the consumer-side port of the registry's sync.sh drift check — version stamp
    and content guards.)

skill update [name]

  1. Run outdated. If <name> is given, narrow to it.
  2. For each stale skill, re-run add (which re-fetches, re-customizes against current
    repo context, and re-stamps). Skip skills already current.
  3. Report what was updated old → new and 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). outdated diffs this against registry.json.
  • installed — date the install/update ran (YYYY-MM-DD).
  • profileHash (optional) — short hash of .claude/skill-profile.md at install time.
    Present only when a profile existed. outdated flags profile drift when it no longer
    matches the current profile, so a changed profile triggers a re-tailor on update.
  • targets (optional) — the agents this skill was compiled for at install time
    (e.g. ["claude", "gemini"]). Written by the add compile step; remove uses it to delete
    exactly the right native artifacts, and outdated uses it to detect target drift. Absent on
    pre-targets installs — treat a missing targets as "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 automated validate_output.
  • Auto-install on miss. A repo or global CLAUDE.md rule 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 with node 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
    with node scripts/compile-skill-targets.mjs --name <name> (--dry-run previews).
  • Idempotent. Re-running add for 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. /skill needs no customization to work — it ships
    with sane registry defaults so it can bootstrap a repo that has nothing else installed.

Categories