Purpose
Static, deterministic quality gate for skill authoring. The check action runs the bundled
check-skill.sh. Twenty-five checks with no model invocation, so results are reproducible in CI or a
pre-commit hook. The validate-evals action checks a skill's <skill>/evals/evals.json against the bundled
JSON schema, then runs the bundled check-evals-quality.sh, a deterministic eval-quality lint
(duplicate case ids/names, missing fixtures, empty or vague grading criteria, set-coverage
warnings) that goes beyond structure without ever running a model-graded eval. The listing-budget action runs check-listing-budget.sh, a separate, always-advisory
report on the SHARED listing budget every loaded skill draws from together (a different, cross-skill
limit from check's per-skill entry cap).
Skills-directory resolution
The checker never assumes a repo layout (convention-resolution ladder). It resolves the skills root in
this order. First hit wins:
${user_config.skills_root}. Set it when your skills live outside .claude/skills (run
/skill-quality:setup to configure).
${CLAUDE_PROJECT_DIR}/.claude/skills. The conventional default.
The skill passes the resolved root to the script via the CHECK_SKILL_SKILLS_ROOT environment
variable. When skills_root is configured, export it before invoking the script:
CHECK_SKILL_SKILLS_ROOT="${user_config.skills_root}" \
bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-skill.sh" <skill-name>
When it is unset, invoke the script plain. It falls back to ${CLAUDE_PROJECT_DIR}/.claude/skills.
Gating a marketplace-installed skill. A plugin:skill name (e.g. source-control:setup) is
not auto-resolved: the checker resolves a bare skill name under one root and does not walk Claude
Code's plugin cache to locate an install. The cache keeps each installed version of a copied plugin
in its own directory, ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>, and the previous
version stays on disk for a grace period after an update
(plugins-reference, verified 2026-09-02; recheck
when that page's plugin-cache section changes), so more than one candidate can exist and the checker
will not guess which one you mean. To gate an installed skill, point the root at its installed
skills dir explicitly:
CHECK_SKILL_SKILLS_ROOT=~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills \
bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-skill.sh" <skill-leaf-name>
The cache is a copy, not a git checkout, so the git-backed checks (3 trigger-preservation, 8
vendor byte-identity, 9 stale-metadata, 13 committed-artifact scan) no-op against it. A "new skill /
skipped" result is expected there, not a defect. Passing a plugin:skill name unresolved prints
this exact guidance.
Arguments
Parse $ARGUMENTS:
check <skill-name> (default action). Run the static contract gate over one skill.
check (no name). Run the gate over every skill under the resolved root.
validate-evals <skill-name>. Validate one skill's <skill>/evals/evals.json against the schema.
validate-evals (no name). Validate every skill's <skill>/evals/evals.json that exists.
listing-budget (no root). Report the shared listing-budget estimate over every
listing-eligible skill under the resolved root.
listing-budget <root> [<root> ...]. Pool every listing-eligible skill under each given root
into ONE shared aggregate (e.g. every plugin's skills dir in a marketplace repo). Every root given
must exist.
Action: check
Resolve the skills root (above). If the directory does not exist, report it and offer
/skill-quality:setup.
For a named skill, run:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-skill.sh" <skill-name>
For no name, enumerate each immediate subdirectory of the skills root that contains a SKILL.md
and run the script once per skill, collecting results.
Report per skill:
- PASS / FAIL from the script's exit code (0 = pass, 1 = one or more
FAIL: lines).
- The
FAIL: lines verbatim (each is an actionable defect).
WARN: lines grouped after failures (advisory: a trigger phrase dropped or moved vs the
base ref, soft line target, missing gotchas surface, action-router without evals, orphan
spokes, an injection with no shell: whose commands only look portable, an injected
command carrying no || <fallback>, same-context judgment language with no fresh-eyes
declaration or a stale exemption directive, and a description/verb-contract polarity
mismatch). A dropped-trigger warning is a review item: confirm the description still names
the intent each dropped phrase carried, or restore the phrase.
For a multi-skill run, end with a one-line rollup: N passed, M failed.
The FAIL: messages are self-describing. Do not re-derive their meaning; surface them and, when the
user asks, fix the cited skill. A broken-internal-ref FAIL points at a SKILL.md:<line>. Hand-verify
that line before editing, since it may be an illustrative example path rather than a real broken ref.
Action: validate-evals
Locate <skills-root>/<skill-name>/evals/evals.json. If absent, report that the skill ships no
evals (not a failure, because evals are warranted, not mandatory).
Read the bundled schema at
${CLAUDE_PLUGIN_ROOT}/reference/evals.schema.json and the
skill's evals.json.
If a JSON-schema validator is available (check-jsonschema, ajv, or python -m jsonschema),
run it and report conformance. Otherwise validate structurally against the schema: skill_name
and a non-empty evals array are required; each case requires id, prompt, and at least one
non-empty grading criterion: a non-empty expected_output string, a non-empty expectations
array, or a non-empty assertions array (a case that cannot be graded is not an eval); a
rich-form case may add name (kebab-case) and files.
Report each violation with its JSON path, or confirm the file conforms.
Run the deterministic eval-quality lint over every located file, all at once. The script
accepts multiple paths:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-evals-quality.sh" <skills-root>/<skill>/evals/evals.json
Report its FAIL: lines verbatim (each is an actionable defect: duplicate case ids/names,
an unresolvable files fixture, an empty criterion item), then its WARN: lines grouped
after (advisory quality heuristics: vague criterion phrasing, thin sole-criterion
expected_output, identical prompt+files pairs, a set with no refusal/anti-pattern case).
The script exits 0 when only warnings remain; run --help for the full Q1-Q9 check list.
If jq is absent the script exits 2. Report that the quality lint was skipped for that
reason; the schema verdict from steps 3-4 still stands.
Action: listing-budget
Resolve the root(s): explicit <root> ... arguments if given; otherwise the same
skills-root resolution as check (above).
Run:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-listing-budget.sh" [<root> ...]
Report the printed aggregate, the budget it was compared against (and whether that budget is the
documented default, a fixed override, or a reconstructed one, and the script labels which), and the
biggest contributors when it overflows. The action is complete when the report names all three
of those elements; the script exiting 0 alone is not the done-condition (it is advisory and
always exits 0 on a successful run).
This is a different, cross-skill limit from check's per-skill entry cap (description +
when_to_use <= 1536 chars, the documented default of skillListingMaxDescChars per
https://code.claude.com/docs/en/skills#frontmatter-reference, verified 2026-08-31; recheck
trigger: that page or the settings page moving either default re-derives this sentence and the
scripts' constants): the shared budget every loaded skill draws from together
(skillListingBudgetFraction, default 1% of the model's context window).
The script exits 0 regardless of overflow, because the live budget depends on the model's context window and a
consumer's own settings, neither of which this static check can observe. Point /doctor at the live
session for the authoritative resolved cost.
Only listing-eligible skills count. A skill with disable-model-invocation: true has its
description kept out of the model-visible listing entirely, so it spends none of the shared budget
and the report skips it. Counting those would overstate the aggregate. A consumer's
skillOverrides can free further descriptions by collapsing entries to "name-only", which
repository content cannot reveal, so the reported figure is an upper bound for anyone who sets it.
A missing explicit root and a nonnumeric override are both environment errors (exit 2), never a
silent skip or a coerced-to-zero budget.
Both claims are verified 2026-09-06 against Claude Code 2.1.263 and the skills page
(https://code.claude.com/docs/en/skills, the invocation-mode table row for
disable-model-invocation: true, "Description not in context", and "Skill descriptions are cut
short", which names "name-only" as the way to free budget). Recheck when either section stops
carrying its statement, or when a release note names skill listing budget or skillOverrides.
Cross-skill invocation (doctrine)
The Skill tool executes one skill within the main conversation, so a step needing two skills is two
calls. Do not instruct Skill-tool invocation of a disable-model-invocation: true
(user-invoked-only) target. Tell the user to run /plugin:skill instead. Verified 2026-09-06
against Claude Code 2.1.263 and two pages: the tools reference
(https://code.claude.com/docs/en/tools-reference, the Skill row) and the skills page
(https://code.claude.com/docs/en/skills, "to keep Claude from invoking it through the Skill tool,
set disable-model-invocation: true"). Recheck when the Skill row describes more than one skill
per call, when the skills page stops carrying that sentence, or when a release note names the Skill
tool. This gate does not automate that reachability check; author and review against the invariant.
Gotchas
- A git repository is optional. Git-backed checks (trigger-keyword preservation, vendor
byte-identity, stale-tracking metadata, committed-artifact scan) skip with a note when cwd
is outside a repo. Marketplace plugin-cache installs are plain trees. Set
CHECK_SKILL_SKILLS_ROOT (or CLAUDE_PROJECT_DIR) so the non-git checks still resolve a
skills root; without either and without a git toplevel, the script exits 2 naming the
missing root.
check-skill.sh runs npx markdownlint-cli2 for check 6; when npx is absent that check downgrades
to a WARN rather than failing, so a run on a machine without Node still gates on every other check.
- Check 6 defers to the repo's markdownlint config. Run it from inside that repo.
markdownlint-cli2
discovers the nearest .markdownlint-cli2.jsonc from its working directory. Run the checker from
outside the target repo (or against a marketplace-installed skill in the plugin cache, which has no
config) and markdownlint applies its DEFAULTS, so rules a repo deliberately disables (commonly
MD013 line-length for injection blocks and tables, MD041 first-line-heading for a frontmatter/H2
start, MD060 table-pipe style) fire as spurious failures on a skill that passes in-repo. This is the
usual cause of a "shipped marketplace skill fails the marketplace's own gate" report: it is a
wrong-config artifact, not a real regression. Injection blocks are not special-cased. A declared
shell: block with long lines is MD013-subject like any other content; whether it fails is entirely the
consumer's markdownlint config's call (disable MD013, or wrap the lines), never something this gate
overrides. In this marketplace's own CI the division of labor is explicit: the skill-quality gate skips
markdownlint (CHECK_SKILL_SKIP_MARKDOWNLINT=1 in the repo's check-changed-skills.sh gate) and the
hygiene lane lints all repo markdown, SKILL.md included, under the repo config.
- Trigger-keyword preservation compares the working tree against
HEAD by default, so a brand-new skill
(no committed version) skips check 3. That is expected, not a silent pass. For a post-commit audit
(where HEAD == the working tree hides an already-committed change), set CHECK_SKILL_BASE_REF to a
ref before the change (e.g. HEAD^ or a merge-base) and run on a clean tree; it reroutes checks 3/8/9.
- Check 3 (trigger-keyword preservation) is advisory: it warns on a dropped phrase and never fails
the run. It tracks single-quoted
'phrase' triggers; an unquoted Use when: list is not tracked,
and check 12 warns so those phrases get quoted and covered. A dropped phrase found verbatim in a
sibling skill's description/when_to_use under the same skills root, where the sibling did NOT
already carry it at the base ref, is a trigger MOVE and warns naming the host, because the listing
still routes the phrase. A phrase absent everywhere, or one the sibling carried all along
(coincidental overlap, not a move), warns as dropped and asks the reviewer to confirm the
description still names the intent the phrase carried (a deliberate consolidation of near-synonym
triggers into an intent category) or to restore it. Treat that warning as a review item, not
noise.
- Check 19 (injection shell-declaration) FAILs only when a
! injection carries detectable
bash-only syntax (/dev/null, command -v, a pipe into a Unix text tool) AND no shell: is
declared; portable-looking commands downgrade to a WARN, since static analysis cannot prove
portability. A shell: declaration is trusted wholesale. The check does not validate that the
injected commands actually match the declared shell (so shell: pwsh with bash-only commands is
out of scope). Both checks 19 and 20 scan the injected command text only. A bash-only token in a
plain ```bash example or in prose never trips them.
- Check 21 (fresh-eyes declaration conformance) is WARN-only on its judgment-language heuristic;
only a malformed or reason-less
fresh-eyes-exempt directive FAILs. Its proximity window is
per-file, so a declaration living in a referenced spoke file cannot satisfy it. The WARN says
so; hand-verify before editing. Literal directive examples belong inside code fences (both
detectors are fence- and inline-span-aware); a bare <class> placeholder in prose FAILs as an
unknown class. Spec: reference/fresh-eyes-declarations.md.
- Check 18 (precompute opportunity) is an advisory heuristic, never a FAIL. It cannot tell an
instruction-to-run shell block from an illustrative example, so a WARN is a candidate to judge, not a
defect. Like a check-5 ref, hand-verify the block before converting it. It reads only fenced shell
blocks (not prose "run
git status first") and stays silent whenever the skill already uses any !
injection, so it under-reports by design; a clean run is not proof there is no precompute opportunity.
- Check 23 (completion-criteria signal) is an advisory heuristic, never a FAIL. It fires only when a
numbered procedure of three or more steps carries NO completion-signal token at all. It detects
the absence of any done-condition, and cannot grade whether a stated criterion is observable or
good; its broad token set means it under-reports by design. The write-side doctrine whose floor it
checks is
docs-hygiene:write-for-agents (steps state observable completion criteria; guard
premature completion, post-completion obligations, and legwork). When authoring new agent docs or
fixing a flagged procedure, invoke /docs-hygiene:write-for-agents via the Skill tool.
- Check 24 (explicit invocation mode) FAILs a marketplace plugin skill (
plugins/*/skills/*) whose
frontmatter omits disable-model-invocation, and only WARNs anywhere else: the absent-key default
is already false, so a consumer's own skill is informed by this fleet's convention rather than
broken by it. A non-boolean value FAILs everywhere: the check reads the bare scalar, so a quoted
"false" fails as the YAML string it is, while a trailing # comment naming the exception class
is fine. The rubric that owns the decision, the
model-invoked default and the only three exception classes a true may claim, is
docs/conventions/invocation-mode/README.md.
Class attribution is NOT machine-checkable: only a setup skill's true is deterministic (class
(ii), the PLUGIN-PHILOSOPHY setup contract), so every other true emits a note to hand-verify
rather than a warning no scan could clear.
- Check 25 (description/verb-contract polarity) is an advisory heuristic, never a FAIL. It
flags a listing-surface mismatch between the description lead (before
Use when:) and the
Naming verb contract or the body: a report-only leaf (audit/scan) whose lead advertises
mutation without an explicit override, a mutate leaf (clean/tidy/fix) whose lead
claims read-only/report-only, a read-only lead whose body mutates on bare invocation, or a
mutate-advertising lead whose body claims the skill never mutates. --fix in the listing
is the compliant override shape and clears a report-only verb. Out of scope: whether any
audit skill should gain a --fix path, and any rename. A WARN is a candidate to
hand-verify, not a mandate to rewrite the fleet. Trigger phrases, "read-only by default",
the noun "remediation", and a negated "or rewrites" list do not advertise mutation.
check-evals-quality.sh requires jq (exit 2 without it, and the schema validation of
validate-evals steps 3-4 is unaffected). Its WARN-tier checks (Q5-Q9) are lexical heuristics:
Q9 (set-coverage) detects refusal/anti-pattern cases by wording, so a set whose guardrail case
phrases the prohibition unusually can WARN despite covering it. Read the set before adding a
case. It deliberately does not flag low case count: the marketplace's low eval volume is a recorded
divergence from the evaluation guidance.
check-evals-quality.sh resolves each case's files entries relative to the skill directory
first, then the evals directory. An entry that is prose (environment description) rather than a
real path FAILs Q4. Describe environment state in the case's prompt parenthetical instead,
or ship a fixture. When files is empty/absent, path-shaped tokens in prompt/expected_output
that resolve nowhere WARN under the same Q4 roots unless the case sets narration: true.
- A clean
listing-budget report is a signal to investigate against /doctor in a live session,
not a guarantee nothing is dropped there. In this marketplace's own repo, each plugin owns its own
plugins/<plugin>/skills/ root, so gating the whole marketplace means pooling every plugin's root
into one call (check-listing-budget.sh plugins/*/skills) rather than running it once per plugin
in isolation. The marketplace's CI workflow runs that pooled call as a dedicated step on every run.
1---2name: check-23description: Skill-authoring QA for Claude Code skills. Use when: 'check this skill', 'skill quality', 'lint my skill', 'is this SKILL.md valid', 'validate skill frontmatter', 'check skill before publishing', 'validate evals.json', 'shared listing budget', 'is the skill listing overflowing', or before shipping a skill or plugin. Actions: `check [<skill-name>]` runs a twenty-five-check static contract gate (frontmatter, invocation mode, verb-contract polarity, per-skill listing-entry cap, advisory trigger-phrase check vs HEAD, line caps, broken internal refs, markdownlint, gotchas surface, evals presence, and more) and reports PASS/FAIL with warnings; `validate-evals [<skill-name>]` checks a skill's evals/evals.json against the bundled schema, then runs a deterministic eval-quality lint; `listing-budget [<root> ...]` reports the SHARED aggregate listing-budget estimate across every listing-eligible skill under the resolved root(s). Advisory only, never blocks. Not for: writing new skills, or running model-graded evals.4---56## Purpose78Static, deterministic quality gate for skill authoring. The `check` action runs the bundled9`check-skill.sh`. Twenty-five checks with no model invocation, so results are reproducible in CI or a10pre-commit hook. The `validate-evals` action checks a skill's `<skill>/evals/evals.json` against the bundled11JSON schema, then runs the bundled `check-evals-quality.sh`, a deterministic eval-quality lint12(duplicate case ids/names, missing fixtures, empty or vague grading criteria, set-coverage13warnings) that goes beyond structure without ever running a model-graded eval. The `listing-budget` action runs `check-listing-budget.sh`, a separate, always-advisory14report on the SHARED listing budget every loaded skill draws from together (a different, cross-skill15limit from `check`'s per-skill entry cap).1617## Skills-directory resolution1819The checker never assumes a repo layout (convention-resolution ladder). It resolves the skills root in20this order. First hit wins:21221. `${user_config.skills_root}`. Set it when your skills live outside `.claude/skills` (run23 `/skill-quality:setup` to configure).242. `${CLAUDE_PROJECT_DIR}/.claude/skills`. The conventional default.2526The skill passes the resolved root to the script via the `CHECK_SKILL_SKILLS_ROOT` environment27variable. When `skills_root` is configured, export it before invoking the script:2829```shell30CHECK_SKILL_SKILLS_ROOT="${user_config.skills_root}" \31 bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-skill.sh" <skill-name>32```3334When it is unset, invoke the script plain. It falls back to `${CLAUDE_PROJECT_DIR}/.claude/skills`.3536**Gating a marketplace-installed skill.** A `plugin:skill` name (e.g. `source-control:setup`) is37not auto-resolved: the checker resolves a bare skill name under one root and does not walk Claude38Code's plugin cache to locate an install. The cache keeps each installed version of a copied plugin39in its own directory, `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>`, and the previous40version stays on disk for a grace period after an update41([plugins-reference](https://code.claude.com/docs/en/plugins-reference), verified 2026-09-02; recheck42when that page's plugin-cache section changes), so more than one candidate can exist and the checker43will not guess which one you mean. To gate an installed skill, point the root at its installed44skills dir explicitly:4546```shell47CHECK_SKILL_SKILLS_ROOT=~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills \48 bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-skill.sh" <skill-leaf-name>49```5051The cache is a **copy, not a git checkout**, so the git-backed checks (3 trigger-preservation, 852vendor byte-identity, 9 stale-metadata, 13 committed-artifact scan) no-op against it. A "new skill /53skipped" result is expected there, not a defect. Passing a `plugin:skill` name unresolved prints54this exact guidance.5556## Arguments5758Parse `$ARGUMENTS`:5960- **`check <skill-name>`** (default action). Run the static contract gate over one skill.61- **`check`** *(no name)*. Run the gate over every skill under the resolved root.62- **`validate-evals <skill-name>`**. Validate one skill's `<skill>/evals/evals.json` against the schema.63- **`validate-evals`** *(no name)*. Validate every skill's `<skill>/evals/evals.json` that exists.64- **`listing-budget`** *(no root)*. Report the shared listing-budget estimate over every65 listing-eligible skill under the resolved root.66- **`listing-budget <root> [<root> ...]`**. Pool every listing-eligible skill under each given root67 into ONE shared aggregate (e.g. every plugin's skills dir in a marketplace repo). Every root given68 must exist.6970## Action: check71721. Resolve the skills root (above). If the directory does not exist, report it and offer73 `/skill-quality:setup`.742. For a named skill, run:7576 ```shell77 bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-skill.sh" <skill-name>78 ```7980 For no name, enumerate each immediate subdirectory of the skills root that contains a `SKILL.md`81 and run the script once per skill, collecting results.823. Report per skill:83 - **PASS / FAIL** from the script's exit code (0 = pass, 1 = one or more `FAIL:` lines).84 - The `FAIL:` lines verbatim (each is an actionable defect).85 - `WARN:` lines grouped after failures (advisory: a trigger phrase dropped or moved vs the86 base ref, soft line target, missing gotchas surface, action-router without evals, orphan87 spokes, an injection with no `shell:` whose commands only *look* portable, an injected88 command carrying no `|| <fallback>`, same-context judgment language with no fresh-eyes89 declaration or a stale exemption directive, and a description/verb-contract polarity90 mismatch). A dropped-trigger warning is a review item: confirm the description still names91 the intent each dropped phrase carried, or restore the phrase.924. For a multi-skill run, end with a one-line rollup: `N passed, M failed`.9394The `FAIL:` messages are self-describing. Do not re-derive their meaning; surface them and, when the95user asks, fix the cited skill. A broken-internal-ref FAIL points at a `SKILL.md:<line>`. Hand-verify96that line before editing, since it may be an illustrative example path rather than a real broken ref.9798## Action: validate-evals991001. Locate `<skills-root>/<skill-name>/evals/evals.json`. If absent, report that the skill ships no101 evals (not a failure, because evals are warranted, not mandatory).1022. Read the bundled schema at103 [`${CLAUDE_PLUGIN_ROOT}/reference/evals.schema.json`](../../reference/evals.schema.json) and the104 skill's `evals.json`.1053. If a JSON-schema validator is available (`check-jsonschema`, `ajv`, or `python -m jsonschema`),106 run it and report conformance. Otherwise validate structurally against the schema: `skill_name`107 and a non-empty `evals` array are required; each case requires `id`, `prompt`, and at least one108 non-empty grading criterion: a non-empty `expected_output` string, a non-empty `expectations`109 array, or a non-empty `assertions` array (a case that cannot be graded is not an eval); a110 rich-form case may add `name` (kebab-case) and `files`.1114. Report each violation with its JSON path, or confirm the file conforms.1125. Run the deterministic eval-quality lint over every located file, all at once. The script113 accepts multiple paths:114115 ```shell116 bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-evals-quality.sh" <skills-root>/<skill>/evals/evals.json117 ```118119 Report its `FAIL:` lines verbatim (each is an actionable defect: duplicate case ids/names,120 an unresolvable `files` fixture, an empty criterion item), then its `WARN:` lines grouped121 after (advisory quality heuristics: vague criterion phrasing, thin sole-criterion122 `expected_output`, identical prompt+files pairs, a set with no refusal/anti-pattern case).123 The script exits 0 when only warnings remain; run `--help` for the full Q1-Q9 check list.124 If `jq` is absent the script exits 2. Report that the quality lint was skipped for that125 reason; the schema verdict from steps 3-4 still stands.126127## Action: listing-budget1281291. Resolve the root(s): explicit `<root> ...` arguments if given; otherwise the same130 skills-root resolution as `check` (above).1312. Run:132133 ```shell134 bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-listing-budget.sh" [<root> ...]135 ```1361373. Report the printed aggregate, the budget it was compared against (and whether that budget is the138 documented default, a fixed override, or a reconstructed one, and the script labels which), and the139 biggest contributors when it overflows. The action is complete when the report names all three140 of those elements; the script exiting 0 alone is not the done-condition (it is advisory and141 always exits 0 on a successful run).142143This is a **different, cross-skill limit** from `check`'s per-skill entry cap (`description` +144`when_to_use` <= 1536 chars, the documented default of `skillListingMaxDescChars` per145<https://code.claude.com/docs/en/skills#frontmatter-reference>, verified 2026-08-31; recheck146trigger: that page or the settings page moving either default re-derives this sentence and the147scripts' constants): the shared budget every loaded skill draws from together148(`skillListingBudgetFraction`, default 1% of the model's context window).149The script exits 0 regardless of overflow, because the live budget depends on the model's context window and a150consumer's own settings, neither of which this static check can observe. Point `/doctor` at the live151session for the authoritative resolved cost.152153**Only listing-eligible skills count.** A skill with `disable-model-invocation: true` has its154description kept out of the model-visible listing entirely, so it spends none of the shared budget155and the report skips it. Counting those would overstate the aggregate. A consumer's156`skillOverrides` can free further descriptions by collapsing entries to `"name-only"`, which157repository content cannot reveal, so the reported figure is an upper bound for anyone who sets it.158A missing explicit root and a nonnumeric override are both environment errors (exit 2), never a159silent skip or a coerced-to-zero budget.160161Both claims are verified 2026-09-06 against Claude Code 2.1.263 and the skills page162(<https://code.claude.com/docs/en/skills>, the invocation-mode table row for163`disable-model-invocation: true`, "Description not in context", and "Skill descriptions are cut164short", which names `"name-only"` as the way to free budget). Recheck when either section stops165carrying its statement, or when a release note names skill listing budget or `skillOverrides`.166167## Cross-skill invocation (doctrine)168169The Skill tool executes one skill within the main conversation, so a step needing two skills is two170calls. Do not instruct Skill-tool invocation of a `disable-model-invocation: true`171(user-invoked-only) target. Tell the user to run `/plugin:skill` instead. Verified 2026-09-06172against Claude Code 2.1.263 and two pages: the tools reference173(<https://code.claude.com/docs/en/tools-reference>, the `Skill` row) and the skills page174(<https://code.claude.com/docs/en/skills>, "to keep Claude from invoking it through the Skill tool,175set `disable-model-invocation: true`"). Recheck when the `Skill` row describes more than one skill176per call, when the skills page stops carrying that sentence, or when a release note names the Skill177tool. This gate does not automate that reachability check; author and review against the invariant.178179## Gotchas180181- A git repository is optional. Git-backed checks (trigger-keyword preservation, vendor182 byte-identity, stale-tracking metadata, committed-artifact scan) skip with a note when cwd183 is outside a repo. Marketplace plugin-cache installs are plain trees. Set184 `CHECK_SKILL_SKILLS_ROOT` (or `CLAUDE_PROJECT_DIR`) so the non-git checks still resolve a185 skills root; without either and without a git toplevel, the script exits 2 naming the186 missing root.187- `check-skill.sh` runs `npx markdownlint-cli2` for check 6; when `npx` is absent that check downgrades188 to a WARN rather than failing, so a run on a machine without Node still gates on every other check.189- **Check 6 defers to the repo's markdownlint config. Run it from inside that repo.** `markdownlint-cli2`190 discovers the nearest `.markdownlint-cli2.jsonc` from its working directory. Run the checker from191 *outside* the target repo (or against a marketplace-installed skill in the plugin cache, which has no192 config) and markdownlint applies its DEFAULTS, so rules a repo deliberately disables (commonly193 `MD013` line-length for injection blocks and tables, `MD041` first-line-heading for a frontmatter/H2194 start, `MD060` table-pipe style) fire as spurious failures on a skill that passes in-repo. This is the195 usual cause of a "shipped marketplace skill fails the marketplace's own gate" report: it is a196 wrong-config artifact, not a real regression. **Injection blocks are not special-cased**. A declared197 `shell:` block with long lines is MD013-subject like any other content; whether it fails is entirely the198 consumer's markdownlint config's call (disable `MD013`, or wrap the lines), never something this gate199 overrides. In this marketplace's own CI the division of labor is explicit: the skill-quality gate skips200 markdownlint (`CHECK_SKILL_SKIP_MARKDOWNLINT=1` in the repo's `check-changed-skills.sh` gate) and the201 hygiene lane lints all repo markdown, SKILL.md included, under the repo config.202- Trigger-keyword preservation compares the working tree against `HEAD` by default, so a brand-new skill203 (no committed version) skips check 3. That is expected, not a silent pass. For a post-commit audit204 (where `HEAD` == the working tree hides an already-committed change), set `CHECK_SKILL_BASE_REF` to a205 ref before the change (e.g. `HEAD^` or a merge-base) and run on a clean tree; it reroutes checks 3/8/9.206- Check 3 (trigger-keyword preservation) is advisory: it warns on a dropped phrase and never fails207 the run. It tracks single-quoted `'phrase'` triggers; an unquoted `Use when:` list is not tracked,208 and check 12 warns so those phrases get quoted and covered. A dropped phrase found verbatim in a209 sibling skill's description/when_to_use under the same skills root, where the sibling did NOT210 already carry it at the base ref, is a trigger MOVE and warns naming the host, because the listing211 still routes the phrase. A phrase absent everywhere, or one the sibling carried all along212 (coincidental overlap, not a move), warns as dropped and asks the reviewer to confirm the213 description still names the intent the phrase carried (a deliberate consolidation of near-synonym214 triggers into an intent category) or to restore it. Treat that warning as a review item, not215 noise.216- Check 19 (injection shell-declaration) FAILs only when a `!` injection carries *detectable*217 bash-only syntax (`/dev/null`, `command -v`, a pipe into a Unix text tool) AND no `shell:` is218 declared; portable-looking commands downgrade to a WARN, since static analysis cannot prove219 portability. A `shell:` declaration is trusted wholesale. The check does not validate that the220 injected commands actually match the declared shell (so `shell: pwsh` with bash-only commands is221 out of scope). Both checks 19 and 20 scan the injected command text only. A bash-only token in a222 plain `` ```bash `` example or in prose never trips them.223- Check 21 (fresh-eyes declaration conformance) is WARN-only on its judgment-language heuristic;224 only a malformed or reason-less `fresh-eyes-exempt` directive FAILs. Its proximity window is225 per-file, so a declaration living in a referenced spoke file cannot satisfy it. The WARN says226 so; hand-verify before editing. Literal directive examples belong inside code fences (both227 detectors are fence- and inline-span-aware); a bare `<class>` placeholder in prose FAILs as an228 unknown class. Spec: `reference/fresh-eyes-declarations.md`.229- Check 18 (precompute opportunity) is an advisory heuristic, never a FAIL. It cannot tell an230 instruction-to-run shell block from an illustrative example, so a WARN is a candidate to judge, not a231 defect. Like a check-5 ref, hand-verify the block before converting it. It reads only fenced shell232 blocks (not prose "run `git status` first") and stays silent whenever the skill already uses any `!`233 injection, so it under-reports by design; a clean run is not proof there is no precompute opportunity.234- Check 23 (completion-criteria signal) is an advisory heuristic, never a FAIL. It fires only when a235 numbered procedure of three or more steps carries NO completion-signal token at all. It detects236 the absence of any done-condition, and cannot grade whether a stated criterion is observable or237 good; its broad token set means it under-reports by design. The write-side doctrine whose floor it238 checks is `docs-hygiene:write-for-agents` (steps state observable completion criteria; guard239 premature completion, post-completion obligations, and legwork). When authoring new agent docs or240 fixing a flagged procedure, invoke `/docs-hygiene:write-for-agents` via the Skill tool.241- Check 24 (explicit invocation mode) FAILs a marketplace plugin skill (`plugins/*/skills/*`) whose242 frontmatter omits `disable-model-invocation`, and only WARNs anywhere else: the absent-key default243 is already `false`, so a consumer's own skill is informed by this fleet's convention rather than244 broken by it. A non-boolean value FAILs everywhere: the check reads the bare scalar, so a quoted245 `"false"` fails as the YAML string it is, while a trailing `# comment` naming the exception class246 is fine. The rubric that owns the decision, the247 model-invoked default and the only three exception classes a `true` may claim, is248 [`docs/conventions/invocation-mode/README.md`](https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/invocation-mode/README.md).249 Class attribution is NOT machine-checkable: only a `setup` skill's `true` is deterministic (class250 (ii), the PLUGIN-PHILOSOPHY setup contract), so every other `true` emits a note to hand-verify251 rather than a warning no scan could clear.252- Check 25 (description/verb-contract polarity) is an advisory heuristic, never a FAIL. It253 flags a listing-surface mismatch between the description lead (before `Use when:`) and the254 Naming verb contract or the body: a report-only leaf (`audit`/`scan`) whose lead advertises255 mutation without an explicit override, a mutate leaf (`clean`/`tidy`/`fix`) whose lead256 claims read-only/report-only, a read-only lead whose body mutates on bare invocation, or a257 mutate-advertising lead whose body claims the skill never mutates. `--fix` in the listing258 is the compliant override shape and clears a report-only verb. Out of scope: whether any259 `audit` skill should gain a `--fix` path, and any rename. A WARN is a candidate to260 hand-verify, not a mandate to rewrite the fleet. Trigger phrases, "read-only by default",261 the noun "remediation", and a negated "or rewrites" list do not advertise mutation.262- `check-evals-quality.sh` requires `jq` (exit 2 without it, and the schema validation of263 `validate-evals` steps 3-4 is unaffected). Its WARN-tier checks (Q5-Q9) are lexical heuristics:264 Q9 (set-coverage) detects refusal/anti-pattern cases by wording, so a set whose guardrail case265 phrases the prohibition unusually can WARN despite covering it. Read the set before adding a266 case. It deliberately does not flag low case count: the marketplace's low eval volume is a recorded267 divergence from the evaluation guidance.268- `check-evals-quality.sh` resolves each case's `files` entries relative to the skill directory269 first, then the evals directory. An entry that is prose (environment description) rather than a270 real path FAILs Q4. Describe environment state in the case's `prompt` parenthetical instead,271 or ship a fixture. When `files` is empty/absent, path-shaped tokens in `prompt`/`expected_output`272 that resolve nowhere WARN under the same Q4 roots unless the case sets `narration: true`.273- A clean `listing-budget` report is a signal to investigate against `/doctor` in a live session,274 not a guarantee nothing is dropped there. In this marketplace's own repo, each plugin owns its own275 `plugins/<plugin>/skills/` root, so gating the whole marketplace means pooling every plugin's root276 into one call (`check-listing-budget.sh plugins/*/skills`) rather than running it once per plugin277 in isolation. The marketplace's CI workflow runs that pooled call as a dedicated step on every run.