Skip to content

fix(skill-quality): name the sibling skill when a cross-skill citation misses - #2179

Merged
kyle-sexton merged 2 commits into
mainfrom
work/2173-auto
Aug 10, 2026
Merged

fix(skill-quality): name the sibling skill when a cross-skill citation misses#2179
kyle-sexton merged 2 commits into
mainfrom
work/2173-auto

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes #2173

Summary

check-skill.sh check 5 resolves every bare context/…-shaped path against the citing skill's
own directory. A skill citing a sibling skill's supporting file therefore failed as a broken
internal ref — "no such file under the skill dir" — while the file plainly existed one directory
over, and the message pointed at the citing skill. The author's natural next move was to look for
the file where it could never be. That misdirection cost two round trips in a single PR (#2161),
once in each direction.

This implements issue options (2) message and (1) docs. Option (3) (narrowing extraction to
link targets) is deliberately not implemented: prose and inline-code refs are presumed-intended
coverage, and dropping them is a separate call.

Fix

  • Sibling-aware diagnosis (check 5). When the unresolved path also resolves under a sibling
    skill of the same skills root, the finding names that sibling and the citation form that works.
    Still a FAIL — the bare form really does resolve against the citing skill, so it is wrong
    regardless of where the file lives; only the diagnosis gains detail.

  • The sibling hit is evidence, not proof. Because this check deliberately extracts prose and
    inline-code refs, a generic path (scripts/run.sh) can collide with an unrelated same-named
    sibling file. The original message — including its hand-verify the line before fixing, may be an illustrative example instruction — is kept verbatim, and the sibling evidence is appended
    conditionally, so both readings stay live:

    FAIL: broken skill-internal ref: context/suppression.md (no such file under the skill dir; cited
    at SKILL.md:7 — hand-verify the line before fixing, may be an illustrative example). A file with
    that path DOES exist under sibling skill 'host': if that is the file meant, this is a cross-skill
    citation, and a bare path always resolves against the CITING skill's dir — write it as
    ${CLAUDE_PLUGIN_ROOT}/skills/host/context/suppression.md. If the names merely collide, the ref is
    unrelated to that sibling.
    
  • Layout-aware suggestion. Which form is correct depends on the skills root, and the script's
    resolution ladder deliberately bakes no layout. A plugin-shaped root gets
    ${CLAUDE_PLUGIN_ROOT}/skills/<sibling>/<path>; outside a plugin that variable is undefined, so
    the message names the layout-free ../<sibling>/<path> instead of advertising a variable the
    consumer does not have. The plugin-shaped test itself is hoisted to a single IS_PLUGIN_SKILL
    computed once after the skill dir resolves — check 1's bare-alias branch and check 5's suggested
    form now read one boolean instead of restating the same layout convention twice.

  • No false positives. A path no sibling hosts keeps the original message unchanged. Sibling
    enumeration reuses check 3's "$SKILLS_ROOT"/*/SKILL.md glob (sorted, so a path present under
    more than one sibling names the first deterministically) and skips the citing skill.

  • The two grep generators feeding the loop are untouched — extraction behavior is unchanged.

  • Docs (docs/PLUGIN-PHILOSOPHY.md). Two sentences in the existing Configuration ownership and
    scope
    section, directly parallel to the ${CLAUDE_PROJECT_DIR} project-path rule already there
    and to the section's own Bundled plugin code and assets | ${CLAUDE_PLUGIN_ROOT} row. No new
    section. plugins/skill-quality/README.md's check-5 bullet and the script's own header list are
    updated to match.

Verification

  • plugins/skill-quality/scripts/check-skill.test.sh — three new cases, all confirmed passing in
    CI (plugin-gatePASS: plugins/skill-quality/scripts/check-skill.test.sh):
    • 36a: plugin-shaped root names the host sibling and the ${CLAUDE_PLUGIN_ROOT} form, and
      still carries hand-verify the line — the caveat cannot be silently dropped again.
    • 36b: non-plugin root names the sibling-relative ../<sibling>/<path> form and not the
      plugin-root one.
    • 36c: a ref no sibling hosts keeps the plain broken-internal-ref message with no sibling skill
      text (false-positive guard).
  • Manual reproduction against a throwaway plugin-shaped fixture reproduces the message quoted above,
    while a genuinely absent ref in the same SKILL.md still reports the unmodified
    broken skill-internal ref: context/absent-file.md (…).
  • Repo gates, base-ref form against the committed tree: check-shell-portability.sh,
    check-skill-portability.sh, check-changelog-parity.sh --check-bump, check-changed-skills.sh,
    check-contract-slice-prune.sh --check-diff — all pass. markdownlint-cli2 clean on the changed
    markdown. shellcheck clean and shfmt -i 2 clean on both changed shell files.
  • Coverage note: the full suite was run to completion on Linux CI, not locally on Windows/Git Bash
    (where it is impractically slow); local Windows confirmation is limited to targeted smoke tests of
    the two new message branches. Nothing in the change is platform-sensitive — pure bash string ops
    and -e/-f tests, no date/readlink/GNU-only constructs — and shell-portability-lint passes.
  • plugins/skill-quality bumped to 0.15.1 with a matching ## [0.15.1] CHANGELOG entry (patch is
    consistent with this plugin's history: 0.12.1 shipped a whole new check as a patch).

Related

…n misses

Check 5 resolves every bare `context/…`-shaped path against the CITING
skill's own directory. A skill citing a sibling skill's supporting file
therefore failed with "no such file under the skill dir" while the file
plainly existed one directory over, and the message pointed at the citing
skill — so the author's natural next move was to look for the file where
it could never be. That misdirection cost two round trips in a single PR
(#2161), once in each direction.

The bare form really is wrong, so this stays a FAIL; only the diagnosis
changes. When the unresolved path resolves under a sibling skill of the
same skills root, the finding names that sibling and the citation form
that works. Which form is layout-dependent, so the message follows check
1's existing plugin detection: `${CLAUDE_PLUGIN_ROOT}/skills/<sibling>/…`
in a plugin-shaped root, `../<sibling>/…` outside one, where that
variable is undefined and advertising it would be wrong guidance.

Extraction is deliberately untouched — prose and inline-code refs stay in
scope, since narrowing to link targets would drop coverage the check is
presumed to have.

PLUGIN-PHILOSOPHY.md gains the rule where the anchoring rules already
live, so an author meets the form before the gate teaches it to them.

Closes #2173

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FdBsorqGjCU4Mg5nrmgz4m
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 25s —— View job


Security review complete

  • Gather context (PR diff, changed files)
  • Review plugins/skill-quality/scripts/check-skill.sh changes for injection / logic / path-handling issues
  • Review plugins/skill-quality/scripts/check-skill.test.sh changes
  • Review docs/README/CHANGELOG/plugin.json changes
  • Post findings

This PR only touches a local, static shell-based lint script for skill-authoring quality (check-skill.sh), its test fixtures, and docs/metadata. No workflow files, no network calls, no secrets handling. I found no security issues in this diff.

Notes from what I checked (not findings, just what was verified as safe):

  • No injection surface introduced. The new sibling-lookup loop (plugins/skill-quality/scripts/check-skill.sh lines ~366–393) reuses the same $ref value already extracted by the untouched grep generators at the bottom of the check-5 block. Those generators constrain matches to [A-Za-z0-9._/#-] — no shell metacharacters, $(), backticks, or quotes can appear in $ref, so the new [[ -e "${other_md%/SKILL.md}/$ref" ]] existence tests and the new err "... $REF_HOST/$ref" message strings can't be leveraged for command injection or format-string tricks (err/warn/note all use printf '%s\n' "$*", a fixed format string).
  • No new path-traversal exposure. The character class does allow . and /, so a ../-laden ref could in principle test file existence outside the skill dir — but that's pre-existing behavior of check 5's original bare [[ ! -e "$SKILL_DIR/$ref" ]] test (untouched by this PR), and the new code only extends the same existence-only check (no content is read or echoed back) to sibling directories under the same, repo-local $SKILLS_ROOT. Nothing about the trust boundary changes here.
  • Sibling enumeration is safe. for other_md in "$SKILLS_ROOT"/*/SKILL.md is a quoted glob with no eval, and plugin.json detection reuses check 1's existing, unmodified logic.
  • Test, docs, README, CHANGELOG, and plugin.json version-bump changes are inert (fixtures under a temp dir, prose, and a semver bump) — nothing security-relevant.

Per instructions, I deferred to zizmor's static lane for any workflow/supply-chain/permissions concerns — none of those surfaces are touched by this PR anyway (no .github/workflows/* files changed).

Branch: work/2173-auto

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e60197baa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/skill-quality/scripts/check-skill.sh
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 4m 10s —— View job


Review complete

  • Gather context (diff, changed files, CLAUDE.md/REVIEW.md guidelines)
  • Understand the request (code review of correctness + alignment with CLAUDE.md)
  • Review plugins/skill-quality/scripts/check-skill.sh changes
  • Review plugins/skill-quality/scripts/check-skill.test.sh new tests
  • Review docs updates (docs/PLUGIN-PHILOSOPHY.md, README.md, CHANGELOG.md, plugin.json version bump)
  • Post inline comments for findings
  • Post final summary

Scope note: this repo has .github/workflows/claude-security-review.yml, so per REVIEW.md this lane leaves security findings to that lane. Nothing in this diff touches auth/authorization/secrets/injection anyway — it's a bash static-analysis check plus docs.

What the change does

Check 5 in check-skill.sh resolves every bare context/…-shaped path against the citing skill's own directory. When a ref misses there, the fix now scans sibling skill directories under the same skills root (reusing check 3's "$SKILLS_ROOT"/*/SKILL.md glob pattern) before falling back to the original "broken ref" message. If a sibling hosts the file, the FAIL message names that sibling and the correct citation form — ${CLAUDE_PLUGIN_ROOT}/skills/<sibling>/<path> in a plugin-shaped root (reusing check 1's plugin detection), or ../<sibling>/<path> outside one. A path no sibling hosts keeps the original wording verbatim, so there's no new false-positive surface.

Correctness

Traced the new branch (plugins/skill-quality/scripts/check-skill.sh:366-398) end to end:

  • The [[ "$other_md" == "$SKILL_MD" ]] self-skip and the sorted-glob "first match wins" behavior both mirror the existing pattern in check 3 (lines 296–298, 318–319), so this isn't a new assumption for the script.
  • The plugin-vs-non-plugin branch reuses check 1's exact detection expression, and the ${CLAUDE_PLUGIN_ROOT}/skills/... form is consistent with the fixed skills/ directory convention used by every plugin in this repo (confirmed via grep — 112 hits across plugins/*/skills/... and existing ${CLAUDE_PLUGIN_ROOT}/skills references) — so hard-coding skills/ in the message doesn't fight the "no baked layout" design of the SKILLS_ROOT resolution ladder; that ladder is about locating the citing skill under an arbitrary repo layout, not about the plugin skills-directory spec itself.
  • The non-plugin fallback ../$REF_HOST/$ref is a relative path from the citing skill's own directory to the sibling's — correct given SKILL_DIR and the sibling are both direct children of SKILLS_ROOT, and consistent with the new docs/PLUGIN-PHILOSOPHY.md example pairing the plugin-root form with a relative link for GitHub browsing.
  • Traced test cases 36a/36b/36c by hand against the fixture setup (including reused PLUGIN_SKILLS/plugin.json fixture from test 34): each assertion matches what the code actually produces. No gaps found in the plugin / non-plugin / no-sibling-hosts coverage.

One inline suggestion posted (maintainability, not a correctness bug): the plugin-shaped-root test (-f "$SKILL_DIR/../../.claude-plugin/plugin.json") is now duplicated verbatim at two call sites (check 1 and check 5) with no shared source of truth — see inline comment.

Docs / changelog / version

docs/PLUGIN-PHILOSOPHY.md, plugins/skill-quality/README.md, and the script's own header list all describe the new behavior consistently and match what the code does. CHANGELOG.md's new ## [0.15.1] entry matches the plugin.json version bump (0.15.00.15.1) and the Keep a Changelog format used elsewhere in the file.

What I could not independently verify

This session's sandbox blocked executing shellcheck, shfmt, and the test suite itself (check-skill.test.sh) — those tool invocations were denied approval. So the PR body's claims of a clean shellcheck/shfmt pass and a full green test run are author-claimed and unverified by me; I did read the shell diff and the new test cases closely by hand instead, per above. If you'd like independent execution confirmation, that needs to run in an environment with those tools permitted.

No blocking issues found. This is a solid, well-scoped fix with good test coverage for the plugin / non-plugin / no-sibling-hosts cases.

Comment thread plugins/skill-quality/scripts/check-skill.sh Outdated
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count.

…in detection

Review findings on the check-5 message.

The sibling existence hit is evidence, not proof. Check 5 deliberately
extracts prose and inline-code refs, so a generic path (`scripts/run.sh`)
can collide with an unrelated same-named file under a sibling. The first
wording asserted the cross-skill reading and dropped the hand-verify
caveat, which would misdirect exactly the illustrative-example case the
caveat exists for. The finding now keeps the original message verbatim
and APPENDS the sibling evidence conditionally, so both readings — real
cross-skill citation, coincidental name collision — stay live and the
author still gets the anchored form to paste.

The plugin-shaped-root test was also restated verbatim at two call sites
(check 1's bare-alias warning, check 5's suggested form). One layout
convention asserted twice drifts the moment it changes, so it is hoisted
to a single IS_PLUGIN_SKILL computed once after the skill dir resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FdBsorqGjCU4Mg5nrmgz4m
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 32s —— View job


Review complete

  • Gather context (diff via gh pr diff, since this sandbox's shallow clone has no origin/main merge base and git fetch requires interactive approval unavailable here)
  • Review plugins/skill-quality/scripts/check-skill.sh changes (sibling-aware diagnosis + hoisted IS_PLUGIN_SKILL)
  • Review plugins/skill-quality/scripts/check-skill.test.sh new tests (36a/36b/36c)
  • Review docs (docs/PLUGIN-PHILOSOPHY.md), README, CHANGELOG, plugin.json version bump
  • Post findings

Scope note: per REVIEW.md, this repo has .github/workflows/claude-security-review.yml, so this lane leaves security findings to that lane. Nothing here touches auth/secrets/injection anyway.

This is the follow-up commit — both round-1 findings are addressed

Commit 3f58e20 (on top of 7e60197) directly answers both pieces of feedback from the first review pass:

  • Codex's ambiguity concern (comment on check-skill.sh:401, "preserve ambiguity when sibling paths collide") — the sibling-hit message no longer asserts the sibling is the target. It now reads "if that is the file meant, this is a cross-skill citation... If the names merely collide, the ref is unrelated to that sibling" (check-skill.sh:417), and the original hand-verify wording is kept verbatim rather than replaced. This closes the exact failure mode Codex flagged (a generic path like scripts/run.sh colliding with an unrelated sibling file).
  • My own round-1 suggestion (hoist the duplicated plugin-detection test) — IS_PLUGIN_SKILL is now computed once at check-skill.sh:209-210, placed right after the SKILL_MD existence check (before check 1 needs it), and both check 1 (line 244) and check 5 (line 412) now reuse the single boolean instead of re-running the -f .../plugin.json test.

Correctness (traced by hand)

  • IS_PLUGIN_SKILL and the sibling-scan loop reuse existing, already-battle-tested patterns rather than introducing new ones: the for other_md in "$SKILLS_ROOT"/*/SKILL.md glob, the [[ -f "$other_md" ]]/self-skip guards, and the "sorted glob → first match wins" behavior are byte-for-byte the same shape as check 3's existing sibling loop (check-skill.sh:306-308), so this isn't a new assumption for the script.
  • ${other_md%/SKILL.md}/$ref correctly reduces to $SKILLS_ROOT/$dir/$ref — verified against how SKILL_DIR/SKILL_MD are built at the top of the script (SKILL_DIR="$SKILLS_ROOT/$SKILL_NAME", SKILL_MD="$SKILL_DIR/SKILL.md").
  • The plugin/non-plugin branch producing ${CLAUDE_PLUGIN_ROOT}/skills/$REF_HOST/$ref vs ../$REF_HOST/$ref is correct given SKILL_DIR and the sibling are both direct children of SKILLS_ROOT.
  • Verified the three new test fixtures (36a/36b/36c) actually exercise what they claim: 36a's PLUGIN_SKILLS root correctly has $TMP/plugins/demo/.claude-plugin/plugin.json two levels up (reused from the earlier test-34 fixture), so IS_PLUGIN_SKILL=1 is genuinely exercised; 36b/36c's $SKILLS root ($TMP/.claude/skills) has no .claude-plugin/plugin.json two levels up anywhere in the fixture tree, so the non-plugin branch is genuinely exercised too — this isn't a test asserting a branch it never actually reaches.
  • 36c (no sibling hosts the ref) correctly falls through to the original, unmodified broken skill-internal ref message with no sibling skill text — confirms the false-positive guard works.

No correctness issues found in this diff. Docs (docs/PLUGIN-PHILOSOPHY.md), README's check-5 bullet, the script's own header comment, and the CHANGELOG.md/plugin.json version bump (0.15.00.15.1) are all internally consistent with what the code does.

Not independently verified

I ran no commands in this session beyond reading the diff and source by hand (gh pr diff, Read, Grep) — this sandbox couldn't reach origin/main via git fetch (shallow clone, needs interactive approval unavailable here) and I did not execute shellcheck, shfmt, or check-skill.test.sh. The PR body's claims of clean shellcheck/shfmt and a full green test run are author-claimed and unverified by me — I instead traced the shell diff and the three new test cases by hand, per above.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 28s —— View job


Security review complete

  • Gather context (gh/git diff origin/main...HEAD, changed files)
  • Review plugins/skill-quality/scripts/check-skill.sh changes for injection / access-control / path-handling issues
  • Review plugins/skill-quality/scripts/check-skill.test.sh changes
  • Review docs/README/CHANGELOG/plugin.json changes
  • Post findings

No security issues found. This PR only touches a local, static shell-based lint script for skill-authoring quality (check-skill.sh), its test fixtures, and docs/metadata — no workflow files, no network calls, no secrets handling, no change to trust boundaries.

Verified independently on the current head (3f58e20), which is one commit ahead of what the earlier automated pass on this PR reviewed:

  • No injection surface. The new sibling-lookup loop (check-skill.sh#L378-L416) consumes $ref, which is produced by the two grep generators at the bottom of the check-5 block — both untouched by this PR and constrained to [A-Za-z0-9._/#-]+ (confirmed by re-reading check-skill.sh#L422-L424): no shell metacharacters, $(), backticks, or quotes can appear in it. err()/warn()/note() all call printf with a fixed format string ('FAIL: %s\n', "$*" as the argument, not the format) — confirmed at check-skill.sh#L158-L170 — so the new err "... $REF_HOST/$ref" messages can't be leveraged for format-string or command-injection tricks.
  • No new path-traversal exposure. The allowed character class does include . and /, so a crafted ../-laden ref could in principle test file existence outside the skill dir — but that's pre-existing behavior of check 5's original bare [[ ! -e "$SKILL_DIR/$ref" ]] test, unmodified here. The new code ([[ -e "${other_md%/SKILL.md}/$ref" ]]) only extends the same existence-only check to sibling directories under the same, repo-local $SKILLS_ROOT — nothing is read, catted, or executed, only a boolean existence result feeds into the diagnostic string. No trust boundary is crossed: SKILL.md content is authored locally by the same developer running the linter, not attacker-controlled input arriving over any external boundary.
  • Sibling enumeration is safe. for other_md in "$SKILLS_ROOT"/*/SKILL.md is a quoted glob, no eval, no word-splitting risk from unquoted expansion. IS_PLUGIN_SKILL plugin-shape detection (check-skill.sh#L203-L209) is a plain -f test against a fixed relative path, now hoisted to one call site and reused by checks 1 and 5 (addressing the maintainability point raised in the prior review pass) — no behavior change from the duplicated version.
  • Test, docs, README, CHANGELOG, and plugin.json version-bump changes are inert (fixtures under a $TMP temp dir, prose, and a semver bump) — nothing security-relevant.

No .github/workflows/* files are touched by this PR, so zizmor's static lane (supply-chain, unpinned actions, dangerous triggers, permissions) has nothing to add here either — this finding set is scoped to the logic/data-flow/trust-boundary reasoning called out for this lane, per repo instructions.

Branch: work/2173-auto

@kyle-sexton
kyle-sexton merged commit 4ebbf8c into main Aug 10, 2026
35 checks passed
@kyle-sexton
kyle-sexton deleted the work/2173-auto branch August 10, 2026 22:33
kyle-sexton added a commit that referenced this pull request Aug 11, 2026
…eferred gate calls (#2186)

No linked issue

## Summary

Three deferred judgment calls, all made here.

**The uncited doc pages were never dispositioned, and the set is larger
than recorded.** PR #2177
worked from an index of 112 core `/docs/en/` pages with ~33 uncited.
Re-derived today, the index
lists **135** core pages and this repo cites **78**, leaving **57**
unevaluated. #2177 also
demonstrated the miss rate of dismissing by eye: it took six pages that
had been written off as
irrelevant and found every one load-bearing enough to warrant a verdict.
So all 57 get a recorded
disposition here rather than a second eyeball pass.

The finding that mattered is in the **cross-platform contract**. It
reads one axis — the operating
system — and names `feature-availability` as its canonical input. That
page carries two axes,
model provider and subscription plan, and scopes itself to what runs
locally: "The Claude Code CLI
and everything that runs locally work on every provider." The **host
surface** a consumer runs in
was therefore never read at all, by either the contract or its input. It
has to be, because a host
can withhold the plugin system itself rather than one capability, and
where no plugin loads there is
no portable path for one to owe.

**`check-skill.sh` check 5** and **check 12's 4-skill warning floor**
were both left open as "a
separate call". Both are decided, at their own sites, with the reasoning
recorded so neither is
re-litigated from a false premise.

## Fix

### 1. Uncited-page disposition (57 pages)

One stated relevance test, applied to all 57 so the dismissals are
auditable rather than tacit:

> **Relevant** if the page describes a surface a plugin author can
**declare, invoke, or must
> accommodate.** Otherwise **not relevant.**

**Split: 1 adopt / 0 defer / 3 decline / 4 relevant-as-evidence / 49 not
relevant.**

Verdicts land in `docs/PLUGIN-PHILOSOPHY.md` under Native-first →
**Recorded gate runs**, in the

[upstream-drift](https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/upstream-drift/README.md)
four-part shape (claim, basis, as-of date, recheck trigger), following
#2177's form.

| Page | Disposition |
|---|---|
| `platforms` | **Relevant → ADOPT, as a citation.** The canonical
host-surface index; the axis `feature-availability` does not carry.
Lands doctrine. |
| `github-enterprise-server` | **Relevant → DECLINE.** A real
plugin-distribution surface ("Plugin marketplaces \| ✅ Supported") —
declines on need, not subject: nothing here documents a GHES-hosted
mirror or fork. |
| `ultrareview` | **Relevant → DECLINE.** Fails gate 1: every run is
human-confirmed and metered, so no skill can reach it. |
| `chrome` | **Relevant → DECLINE.** Ships as the built-in
`claude-in-chrome` skill; nothing to declare. Recorded because it only
*looked* cited — see the dead-link note below. |
| `desktop`, `vs-code`, `mobile`, `desktop-wsl` | **Relevant → read in
full as the evidence base for the `platforms` row**; no separate
verdict, because they are one finding seen from four pages rather than
four surfaces. Quoted verbatim in that row. |
| `jetbrains` | Not relevant — false friend: "plugin" there is the
JetBrains IDE plugin, a different sense. Ranked 2nd by `plugin`-keyword
density and is the one page where that signal is pure noise. |
| `desktop-quickstart`, `desktop-linux`, `desktop-ios-simulator`,
`web-quickstart`, `troubleshoot-install` | Not relevant — install and
first-run recipes; nothing declarable. |
| `slack`, `claude-tag` | Not relevant on their own — delegation front
ends indexed by `platforms`, which is the adopted citation; `slack` is
additionally being retired for Team/Enterprise. |
| `devcontainer` | Not relevant — container recipe; its only
`marketplace` hit is a VS Code extension link. |
| `gitlab-ci-cd`, `github-actions-cloud-providers` | Not relevant — CI
recipes and provider IAM routing; zero plugin or skill surface
(`gitlab-ci-cd`: 0 keyword hits). |
| `amazon-bedrock`, `google-vertex-ai`, `microsoft-foundry`,
`claude-platform-on-aws` | Not relevant — provider auth/IAM config; the
plugin-facing consequence is the availability matrix, already adopted as
`feature-availability`. |
| `gateways`, `llm-gateway`, `llm-gateway-connect`,
`llm-gateway-protocol`, `llm-gateway-rollout` | Not relevant — org
request-routing plane between the client and a provider; no plugin
declares or observes it. |
| `claude-apps-gateway`, `claude-apps-gateway-config`,
`claude-apps-gateway-deploy`, `claude-apps-gateway-on-aws`,
`claude-apps-gateway-on-gcp`, `claude-apps-gateway-spend-limits` | Not
relevant — deploying and operating Anthropic's gateway product;
`gateway.yaml`, Kubernetes, spend caps. |
| `self-hosted-environments`, `self-hosted-environments-quickstart`,
`self-hosted-environments-configuration`,
`self-hosted-environments-deploy`, `self-hosted-environments-identity`,
`self-hosted-environments-reference`, `self-hosted-environments-testing`
| Not relevant — standing up and operating cloud-session runners on org
infrastructure. |
| `admin-setup`, `authentication`, `legal-and-compliance`,
`third-party-integrations` | Not relevant — enterprise deployment,
identity, and policy plane; no surface a plugin declares or observes. |
| `analytics` | Not relevant — but **fetched, not assumed**, because
per-skill or per-plugin cost attribution would have bound instruction
economy. It has none: attribution is PR-level only. (The
per-skill/per-plugin usage breakdown is a consumer-side `/usage` dialog,
not an authoring input.) |
| `network-config` | Not relevant, and the third clause of the test is
why rather than the family label: proxy, custom CA, and mTLS are
**transport configured on the client**, so a skill making a network call
either succeeds or sees an ordinary failure — there is nothing to
declare or degrade. Its two plugin-adjacent lines are egress allowlist
entries a network admin sets, not a plugin (`downloads.claude.ai` for
"Plugin executable downloads"; `storage.googleapis.com` for "plugin
metadata shown in `/plugin`"). |
| `corporate-launcher` | Not relevant, checked against the page rather
than dismissed as admin tooling: `CLAUDE_CODE_PROCESS_WRAPPER` wraps
"every process Claude Code launches **from its own binary** — the
background service, every session it hosts in agent view, and Claude
Code's relaunches after an update". A plugin's
`${CLAUDE_PLUGIN_ROOT}/bin/` invocation is a Bash-tool subprocess, not a
Claude Code self-spawn, so the `bin/` stance is unaffected and owes no
change. |
| `champion-kit`, `communications-kit` | Not relevant —
internal-advocacy and rollout-comms collateral. |
| `accessibility`, `keybindings`, `terminal-config`, `voice-dictation`,
`fullscreen`, `fast-mode` | Not relevant — consumer client settings; no
plugin declares or must accommodate them. |
| `prompt-library` | Not relevant — copy-paste prompts for users, not an
authoring surface. |

**Doctrine added — one paragraph, plus four table rows.** The
cross-platform contract gains the host
axis, citing `platforms` and restating none of its facts. The three
verbatim host facts (Desktop-in-WSL
sessions lack "connectors and plugins"; `/plugin` "[doesn't] work from
the app" on mobile; Desktop's
Cowork tab sources plugins "not from the CLI's `~/.claude` directory")
live in the gate-run row, where
they carry a recheck trigger — not in the contract, which states only
the rule they establish.

**A dead citation, deliberately not fixed.** Every doc URL this repo
cites was checked live — all 81
slugs plus the 4 subpath citations (`agent-sdk/overview`,
`agent-sdk/agent-loop`, `agent-sdk/plugins`,
`whats-new/2026-w32`). **84 of 85 return 200.** One does not:
`code.claude.com/docs/en/browser` now 404s (`chrome` is the live page).
Its sole occurrence
is `plugins/playbooks/skills/boris/vendor/SKILL.md:938` — a **verbatim
upstream baseline kept for
drift detection**, which the plugin README says to treat as untrusted
and which `/playbooks:update`
owns. Hand-editing it would corrupt the vendor SHA it exists to compare.
Recorded in the `chrome` row
with that path as its recheck trigger instead.

### 2. `check-skill.sh` check 5 — KEEP the extractor as-is (decided,
recorded at the site)

Two premises are usually offered for narrowing to markdown-link targets.
Both are false, and the
comment now says so, because the premise is what keeps the question
alive:

1. **"It matches bare paths in prose."** It does not, and never did.
Both generators are delimited —
backtick-wrapped, or a `](…)` link target — and both are scoped to the
`INTERNAL_DIRS` allowlist.
Naked prose cannot match. (#2179's own summary and CHANGELOG entry
describe it as extracting
"prose and inline-code refs"; the in-script wording is corrected here to
match what the greps do.)
2. **"The backtick branch is redundant."** Measured over the 196-skill
corpus rather than argued:

   | Measure | Count |
   |---|---|
   | Backtick-form refs, all SKILL.md | 282 |
   | Link-form refs, all SKILL.md | 475 |
| **Unique backtick-form refs with no link form anywhere in the same
file** | **122** |
   | …spread across | **39 skills** |
   | …of those 122, resolving to a real file today | **122 (100%)** |

Narrowing would drop 122 real, currently-resolving supporting-file
references across 39 skills.
The link branch being the larger share is not the question; the overlap
is, and 122 refs sit
   outside it.

The false-positive risk that motivated the proposal is real but
**latent, not observed** — zero on
the current corpus. It is handled by message wording (every failure
carries `hand-verify the line
before fixing, may be an illustrative example`) rather than by deleting
coverage of 39 skills.
Reopen only if a false positive is actually observed.

### 3. Check 12's 4-skill warning floor — INTENTIONAL, no dmi carve-out
(all 4 confirmed)

#2181's reasoning holds, and upstream states the premise more strongly
than #2181 did. The skills
doc's frontmatter-behavior table gives, for `disable-model-invocation:
true`:
**"Description not in context, full skill loads when you invoke"** — so
trigger phrasing on such a
skill cannot route anything, at all. `user-invocable` defaults to `true`
(confirmed on the same page,
not assumed), so `github:setup` omitting it is slash-command-only,
exactly its declared contract.

The load-bearing half of #2181's argument is the *stranded-phrase* test,
which is an empirical claim
about the current tree, so each was re-checked against the tree rather
than against #2181's prose:

| Skill | Verdict | Confirmed against the tree |
|---|---|---|
| `discipline:wait-what` | **Right to leave** | Its description *is* the
instruction; the trigger is noticing you have stopped following. No
sibling needed — by construction the model cannot detect it. |
| `firecrawl:update` | **Right to leave** | Maintainer-only. Sibling
`firecrawl:firecrawl` **verified** to carry the consumer phrases
(`'scrape this page'`, `'crawl this site'`, `'WebFetch is blocked'`, …).
Nothing stranded. |
| `playbooks:update` | **Right to leave** | Maintainer-only. Sibling
`playbooks:boris` **verified** to carry `'how does Boris use Claude
Code'`, `'Claude Code workflow tips'`, `'optimize my CLAUDE.md'`, …
Nothing stranded. |
| `github:setup` | **Right to leave** — the weakest of the four as
originally argued, and it holds | #2181 argued from intent
("user-invoked only"). Checked instead for a stranded phrase:
model-invocable siblings `github:advise` and `github:audit` carry the
plugin's consumer-facing routing, including `'help me set up Y'`.
`setup` covers plugin *prerequisites* (gh auth, writing
`.claude/github/`), which is a deliberate slash command, not a routing
target. |

**No carve-out is added**, and that is the recorded call. Exempting
dmi-true from check 12 would
suppress a warning that is doing no harm while hiding the `kindle-dedrm`
failure mode #2181 itself
surfaced — a phrase reachable only from a skill the model can never
match. The floor stays; the
exemptions stay documented at the check-12 site.

## Verification

**Method.** Every page was fetched with `curl -sL …/<slug>.md` — the raw
markdown, not WebFetch.
That removes the summarizer and the truncation window from the loop
entirely, so the METHOD RULE
holds trivially: every upstream sentence quoted in this PR and in the
doctrine is verbatim from a
complete page, and a genuine "the page never states X" is a checkable
claim rather than a routine
false negative. Byte counts confirm no truncation (e.g. `desktop.md`
96,288 bytes, `vs-code.md`
49,764). No page was asked to confirm a sentence from this repo.

**The uncited set was re-derived, not inherited.** The grep was also
re-run with **no `--include`
filters** to be sure no citation lives in a file type the filter misses
— identical result, 81
slugs, so 57 uncited is the real number.

**Every cited URL was checked live**: 84 of 85 (81 slugs + 4 subpath
citations) return 200; the
single 404 is the vendored `browser` link described above.

**The gate-1 check that decided the headline adopt** was run against the
page rather than assumed:
`feature-availability`'s section headings are *Availability by model
provider*, *Availability by
subscription plan*, and *Model availability* — no host-surface axis —
and its only feature table
header row is `| Feature | Pro | Max | Team | Enterprise |`. Had it
carried a host axis, `platforms`
would have been a redundant second index and this would be a decline
instead.

Gates run the CI way, against the **committed** tree, base-ref form:

| Gate | Result |
|---|---|
| `bash scripts/check-contract-slice-prune.sh --check-diff origin/main`
| pass — leaves no path under `docs/topics/` |
| `bash scripts/check-changelog-parity.sh --check-bump origin/main` |
pass |
| `bash scripts/check-changed-skills.sh origin/main` | pass — no changed
skills |
| `bash scripts/check-skill-portability.sh origin/main` | pass — no
skill files in scope |
| `bash scripts/check-shell-portability.sh origin/main` | pass — no
unexcused GNU-only constructs |
| `npx --yes markdownlint-cli2` over all 3 changed `.md` | **0 errors**
|
| `shellcheck` + `shfmt -i 2 -d` on `check-skill.sh` | clean |
| `bash -n check-skill.sh` | clean |
| Line endings | all 5 changed files `i/lf w/lf` |

Because the change to `check-skill.sh` is comments only,
`check-changed-skills.sh` exercises
nothing — so the script was run directly to prove it still parses and
behaves:

- `check-skill.sh measure` → `PASS — 0 errors, 0 warning(s)`, `all 10
base-ref trigger phrase(s) preserved`.
- `check-skill.sh wait-what` → `PASS — 0 errors, 2 warning(s)`, one of
which is verbatim
`description has no 'Use when:' trigger phrasing` — confirming the
documented floor still fires as
  described rather than being silently suppressed.
- `check-skill.test.sh` runs to completion in CI (`plugin-gate`);
locally on Windows/Git Bash it is
impractically slow, per the coverage note #2179 recorded. Nothing here
is behavioral.

`plugins/skill-quality` → **0.15.2** with a matching `## [0.15.2]`
entry. The `docs/` changes are
docs-only and owe no plugin bump; the `upstream-drift` **Adopters**
registry already carries a row
for the gate-run table (added in #2177), and these rows join that table
rather than create a new
adopter, so that convention needs no version change.

`docs/OFFICIAL-DOCS.md` gains the four newly load-bearing pages, per the
rule its own warning states
and the precedent #2177's review set: a needed page that is not listed
must be added.

No `docs/topics/<slug>/` directory was created — the durable outcome is
doctrine text, as the
Contract-tier prune rule requires.

## Review rounds

Three threads, all real, all answered and resolved. Each found a defect
in the *basis* of a row
rather than in its verdict, which is the failure mode a decision record
most needs caught: a verdict
outlives the reasoning nobody re-reads.

- **The GHES row's premise was overstated and its trigger fired on
arrival** (`chatgpt-codex-connector`).
It claimed "every plugin README ships the github.com shorthand".
Re-derived from the tree: 54 of 65
carry the literal string, 9 carry no install block, `dometrain` points
at another github.com
marketplace, and `plugins/github/README.md` — deliberately
marketplace-agnostic — uses the
`<marketplace-owner>/<marketplace-repo>` placeholder. All are still
`owner/repo` shorthand, so the
trigger now names the form that actually signals a non-github.com host,
a **full git URL**, of
which the tree has none. The verdict stays Decline, but the review
surfaced a real finding that had
been waved through and is now recorded in the row: a consumer
redistributing the `github` plugin
from a GHES-hosted marketplace would follow that README and have the
shorthand silently resolve to
  github.com instead of their own instance.
- **"Platform" was doing two jobs** (`chatgpt-codex-connector`). The
existing `feature-availability`
row and `docs/OFFICIAL-DOCS.md` both described that page as covering
"platform, provider, and plan",
while this change rests on the host axis being absent from it. Both
senses of the word in one table
would let a future audit read the host axis as already covered and
retire the new row as redundant.
The page's own sense is the **provider** platform — its axis headings
are *Availability by model
provider* and *Availability by subscription plan* — and both sites now
say so explicitly.
- **The `platforms` row claimed four evidence pages and quoted three**
(`claude`). Correctly
diagnosed as a missing fact rather than an overstated page: `vs-code`
does carry a host-axis fact,
and the most directly plugin-relevant of the four — its CLI-vs-extension
table gives
`Commands and skills` as `All` for the CLI against `Subset (type / to
see available)` for the
extension, so a skill this fleet ships may not be reachable there. It is
now quoted in the row.

All gates and `markdownlint-cli2` re-run clean over the changed files
after these edits. CI is green,
including `plugin-gate` — which runs `check-skill.test.sh`, the only
executable proof that the
check-5 comment insertions changed no behavior.

## Related

- #2177 — established the Recorded-gate-runs table and its four-part row
form; this run extends it
  and corrects its page census (112 → 135 core pages, ~33 → 57 uncited)
- #2179 — deferred the check-5 extractor question as "a separate call";
decided here
- #2181 — swept 196 skills for `Use when:` phrasing and left 4 with
stated reasoning; all 4 re-reviewed
  and confirmed here
- #2169 — the gate doc-currency audit these findings trace back to

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-skill.sh: a cross-skill reference fails as a broken internal ref, and the error points at the wrong directory

1 participant