# /skill

The package manager for Claude Code skills — `npm`/`brew` for the skill registry
at [`blamechris/skill-templates`](https://github.com/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/skills/<name>/SKILL.md`,
  `pi` → `~/.pi/agent/skills/<name>/SKILL.md`. The first three are repo-tracked; **`pi` is the one
  user-global target** — Pi has no repo-local discovery, so its artifact lands in the user's home
  dir and is opt-in rather than a safe default. 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.** This writes into the repo's shared working copy, which another
   session may have moved to its own branch since this one started — so re-check first:
   `git branch --show-current`. If it is not the branch you started on, STOP and tell the
   user rather than writing; a skill install landing on someone else's branch is how it
   gets swept into their commit. Then create `.claude/commands/` if absent, strip any
   pre-existing `<!-- skill-templates: … -->` lines from the body, and 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, guards, and the posture pin 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:

   ```bash
   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, every check 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. Six 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, an explicitly passed profile that cannot be read, and usage error. The last five 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.

   ```bash
   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 emits a
   repo-tracked skill folder under `.codex/skills/` so it can be reviewed, copied into
   `~/.codex/skills`, or used by runners that support repo-local Codex skills. **`pi` writes outside
   the repo** (`~/.pi/agent/skills/`), so only add it when the user actually drives this repo with
   Pi — never volunteer it into a committed `targets:` line on their behalf. 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/skills/<name>/` (dir), `pi` → `~/.pi/agent/skills/<name>/` (dir, **outside the
repo** — remove it only when `pi` 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 three **repo-local** artifacts exist —
never probe `~/.pi` on a guess, because an unrelated Pi skill of the same name could live there.
Report what was removed.

## Lockfile schema (`.claude/skills.lock`)

```json
{
  "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.

<!-- skill-templates: skill af9c1e4 2026-08-03 -->
