melodic-software

Encapsulation audit — N violations

"Audit and remediate skill-encapsulation violations — external citations reaching into private surfaces inside `.claude/skills/<X>/` or `plugins/<plugin>/skills/<X>/` (marketplace monorepos) beyond the slash invocation. Use when: 'audit encapsulation', 'find skill leaks', 'skill boundary violation', 'who is reaching into <skill>', 'check skill boundaries', 'public API drift', or before refactoring a skill."

melodic-software 12 Updated 3w ago

Resources

3
GitHub

Install

npx skillscat add melodic-software/claude-code-plugins/plugins-docs-hygiene-skills-audit-encapsulation

Install via the SkillsCat registry.

SKILL.md

Why this skill exists

Skills have a public API and a private body — the contract is context/public-surface-contract.md (bundled with this skill). External consumers — rules, agents, other skills, prose docs — must cite the public surface only. Editing the private body must not break external consumers because no external consumer is allowed to depend on it.

Without enforcement, private bodies leak. A rule file cites a path inside <skill>/<some-subdir>/ because the content is convenient. The skill author refactors that subdir and the citation breaks silently — nothing fails the build, nothing greps red. The skill cannot evolve without coordinating with every leaker.

This skill provides the detection + classification + remediation discipline that closes that gap. Distinct concern from /docs-hygiene:extract-ssot (which detects content duplication via Rule of Three). Encapsulation violations are single-violation matters — Rule of Three does not gate them.

Public surface matrix

The Skill surface — public = frontmatter + documented actions + args/flags + /skill-name slash invocation + the scripts/ entry surface; private = everything else inside .claude/skills/<X>/ or plugins/<plugin>/skills/<X>/ (any OTHER subdirectory, all *.schema.json at any depth, all heading anchors) — is defined by context/public-surface-contract.md, including the data-file and scripts/ entry-surface carve-outs and the rip-and-paste-portability rationale. This skill audits cites against two more authoring surfaces the contract file does not enumerate:

Surface Public (cite externally) Private
Rule file (.claude/rules/*.md) All H2/H3 headings (entire file is shared vocabulary) n/a
Scheduled-automation prompt (e.g. .claude/routines/*.md, if the consumer repo keeps them) Top-of-file orchestration prompt Internal exclusion lists, escape conditions

CI / git-hook sharing — pick a technique, not an exception

Workflows and git hooks needing logic that ALSO lives in a skill must not reach into skill internals. Pick a sharing technique by logic size — scripts/ facade (the skill exposes a public scripts/<name>.sh entry delegating to a private backend; hooks/CI invoke it), intentional duplication, plugin packaging, or headless claude -p '/skill <action>' invocation — per context/public-surface-contract.md "CI / git-hook consumption — entry surface, not internals".

The choice is per-cite. The bundled detect.sh is a candidate enumerator: it lists path shapes that may be private-surface cites. Exit 1 means candidates exist after mechanical filters — not that those hits are adjudicated violations. Classification (filter taxonomy below) is required before treating a hit as illegal. Do not hard-gate CI or pre-commit on detect.sh's exit code alone.

Action router

Argument Action Purpose
(empty) / detect Default Run detection grep, classify legal vs illegal, output violation table — behind the no-scope confirmation below when nothing narrows the scope
sweep Explicit repo-wide detect Default detect minus the no-scope confirmation — for when the user already said "whole repo"
fix <file>:<line> Targeted remediation Interactive Path A (promote-out) vs Path B (route-via-/<skill>) for one hit
file-issues Batch File one tracking work item per illegal hit in the consumer's tracker (e.g. gh issue create); emit a checklist if no tracker is available

One action per response.

No inherited scope — confirm before a repo-wide run

Shared clean-tree / no-scope shape: `../../context/clean-tree-fallback.md`.
This skill's trigger is "no inherited scope" (not only "clean tree") — a dirty tree with unrelated
edits still needs the confirm when nothing names the detect surface.

The default action's domain is the whole repo, but a bare invocation does not prove the user meant
that. When the invocation arrives with NO inherited working set — no diff in flight, no prior audit
notes, nothing in the conversation narrowing the scope — ask ONE confirmation before running the
detection, presenting these prescribed defaults for adjustment:

Default Value
Scope entire tracked repo (the detect script's scan domain)
Mode detect + classify only; remediation stays a separate, user-approved step
Worker fan-out off — classify in-session. When the user opts into subagent classification or remediation, run 2–3 concurrent workers, no more, while rate-limit telemetry is absent; when the rate-limit-guard plugin's snapshot is readable, resolve pacing from its reader contract instead (that contract owns the snapshot path, staleness rule, and thresholds — read them there)

An inherited scope — or a scope the user already stated in the invocation ("across the repo",
"audit this directory") — suppresses the question; audit that surface directly. sweep is the
explicit opt-in that skips the confirmation and runs the repo-wide detect immediately.

Detection

bash "${CLAUDE_SKILL_DIR}/scripts/detect.sh" (--apply-filters optional)

detect.sh enumerates candidates, not adjudicated violations. With --apply-filters, stderr summary keys are raw / mech-filtered / candidates. Exit 1 = candidates remain; exit 0 = no candidates (not a clean bill of health — see limitations below).

Matched shapes:

  • Any path into a subdirectory under a skill (<X>/<any-subdir>/... — regardless of subdir name, including uppercase / single-char / digit-leading / underscore-leading segments) except scripts/ (entry-surface carve-out, see below)
  • Heading-anchor cites into SKILL.md body structure (<X>/SKILL.md#<anchor>)
  • Any *.schema.json file at any depth (<X>/<file>.schema.json)

Matched path prefixes: .claude/skills/<X>/..., plugins/<plugin>/skills/<X>/..., and relative forms (skills/<X>/... plugin-README short cites, plus ../-prefixed cites whose lexical resolution lands on a private skill surface — including sibling-skill links that never spell skills/ in the cite text).

Legal external cites: bare <X>/SKILL.md path (discouraged but legal — natural-language + slash invocation is canonical), <X>/<file>.json data files at skill root (data-file carve-out), and <X>/scripts/<entry> entry scripts (entry-surface carve-out below).

scripts/ entry-surface carve-out: a skill's scripts/ is its declared ENTRY surface per context/public-surface-contract.md. Harness / CI / hooks / workflow registries MAY path-cite a skill's entry scripts directly, so this inbound audit treats <X>/scripts/... cites as legal (like data files and bare SKILL.md) and never flags them. The skill-to-skill half of the asymmetry — a sibling SKILL.md citing another skill's scripts/ stays slash-only — is out of this inbound audit's scope; a consuming repo that wants it enforced wires its own outbound gate.

The script scans the consumer repo it runs in: every .claude/ child directory except skills/ (self-citation domain; opt in via --include-skills for skill-maintenance review) and worktrees/, plus .github/, docs/, .lefthook/, plugins/, root instruction/config files (AGENTS.md, CLAUDE.md, README.md, CONTRIBUTING.md, lefthook.yml), and .claude/ top-level files — each filtered to what exists.

Or invoke the skill (the agent applies the filter taxonomy below):

/docs-hygiene:audit-encapsulation detect

Filter taxonomy — legal hits

detect.sh output is broad without --apply-filters. Use the filter taxonomy below for KIND-1/2/3; --apply-filters drops only the mechanically-decidable subset (self-citation and worktree paths). Remaining rows are candidates — apply KIND-1/2/3, mirror-automation, glob-config, and plugin-cache judgment in-session before calling anything a violation.

Filter Hit shape Why legal
Self-citation A skill cites its own internals — absolute (.claude/skills/<X>/... / plugins/.../skills/<X>/...), bare skills/<X>/..., or a ../ cite that resolves into the same skill Intra-skill progressive disclosure (per context/public-surface-contract.md)
KIND-1 meta-prose A rule or doc describing the encapsulation contract itself, OR documenting skill internals as a worked example / historical narrative / empirically-verified quirk Self-referential explanatory prose, not citation
KIND-2 forced-cite Another tool's semantics structurally require a verbatim path (a path-scoped rule trigger, a watch-glob, a drift-gate comparing against a vendored copy) Citation IS the structural contract
KIND-3 self-test This skill's / a hook's own regression fixtures embed literal violation strings Exercise the filter; not real citations
Mirror-automation A scheduled-automation prompt (e.g. .claude/routines/<name>.md) cites .claude/skills/<name>/<private-path> when its basename matches the skill name Such prompts are cron arms of slash skills; mirror cites are part of the skill contract per the Public surface matrix
Glob-config Skill-internal path used as a glob/filter in an exclusion list, hooks allowlist, or .gitignore — not a content Read cite Path is structural filter syntax, not progressive-disclosure content (overlaps KIND-2; named separately for grading)
Plugin cache ~/.claude/plugins/cache/<plugin>/... Upstream territory; foreign contract. These paths never match the detector PATTERN (not under a skill root), so --apply-filters has no cache branch — classify as legal if they appear in a manual/agent sweep
Worktree path <worktree-root>/<n>/.claude/skills/<X>/... (per the repo's worktree convention — e.g. .worktrees/, .claude/worktrees/, .git/worktrees/) Worktrees share the tracked tree; same rules apply at the root path

Hits that survive ALL filters = illegal. Report.

Remediation paths

Path A — Promote content out (caller wants the data)

Use when: the caller needs data the private file contains, and the data is genuinely shared vocabulary or constraint.

  1. Apply /docs-hygiene:extract-ssot verify (the 6-gate refuse-fast check) to the private file's content
  2. If it passes: extract the content into a shared rule or convention doc outside the skill (e.g. .claude/rules/<topic>.md, or extend an existing one)
  3. Migrate the violator AND the original skill body to cite the new doc by heading
  4. Delete or shrink the private file
  5. Run /docs-hygiene:rename-references to sweep all syntactic forms

Path B — Route via /skill-name invocation (caller wants the behavior)

Use when: the caller wants the skill's behavior, not the data. A reference into the private body is a workaround because the skill has no public action covering the use case.

  1. Identify which skill action would deliver the desired behavior
  2. If the action exists: rewrite the caller to invoke /<skill-name> <action> <args>
  3. If the action does NOT exist:
    • Surface the gap to the user as a side note
    • File a tracking work item in the consumer's tracker for the missing public action
    • Leave the violation in place with a # TODO(audit-encapsulation): missing /<skill> <action> marker
    • Do NOT silently inline a workaround that makes the violation harder to find later

Refactor pass discipline

When invoked from another skill's execute pass (e.g. /docs-hygiene:extract-ssot execute):

Step Action
1 Run the detection grep against the caller list (or repo-wide)
2 Classify each match via the filter taxonomy: legal hit vs violation
3 For each violation, choose Path A (promote) or Path B (route via /name)
4 DO NOT preserve a violation because "it works today" — broken-window pattern
5 If Path B is needed but the action is missing, file a tracking work item and STOP — don't ship a partial fix
6 Run a /docs-hygiene:rename-references sweep after edits

A surfaced violation is never silently ignored: fix it, capture it as a side note to the user, or file a tracking work item.

Output shape (detect mode)

# Encapsulation audit — N violations

## Violations
| File | Line | Cite | Suggested path |
|------|------|------|----------------|
| ... | ... | `<skill>/<dir>/<file>` | A or B |

## Filtered (legal — sample for verification)
| File | Line | Filter applied |
|------|------|----------------|

## Summary
- Total raw hits: M
- Mechanically filtered: M-C
- Candidates (need taxonomy classification): C
- Adjudicated illegal after filters: N
- Path A candidates: a
- Path B candidates: b
- Missing public action (Path B blocked): c

## Recommended next step
`/docs-hygiene:audit-encapsulation fix <file>:<line>` for one-by-one OR `/docs-hygiene:audit-encapsulation file-issues` for batch work-item filing

Anti-patterns guarded

  • Single-violation tolerance — "it's only one place, no big deal." Encapsulation rot accumulates one violation at a time. Each is a binary contract break, not a Rule of Three threshold.
  • Silent workaround — the caller inlines the data instead of routing through the skill, hiding the violation from future audits. Mitigated by the # TODO(audit-encapsulation) marker requirement.
  • Filter laziness — flagging every grep hit without applying the filter taxonomy. False positives erode trust in the audit; users stop running it.
  • Wrong-direction Path A — promoting skill-internal content to a shared doc when the caller actually wanted skill behavior. Mitigated by the "data vs behavior" classification step in Path A vs Path B selection.

Sanity checks

When Check Evidence
Pre-detection git ls-files enumerable in scope Bash output
Post-detection Each raw hit classified per filter taxonomy OR illegal Diff against filter table
Pre-fix Path A vs Path B chosen with explicit data-vs-behavior reasoning Session notes or plan artifact
Post-fix Detection grep re-run; violation count decreased Bash output
Post-fix /docs-hygiene:rename-references sweep ran Tool output

What this skill does NOT do

  • Detect content duplication / Rule of Three clusters → /docs-hygiene:extract-ssot
  • Enforce code-side public/private (TypeScript export, .NET internal, Python _prefix) → language-level tooling (compiler, lint)
  • Refactor skill bodies that ARE legitimately self-contained (no external violations) → general refactoring, out of scope
  • File work items without classification — Path A vs Path B must be picked first
  • Auto-fix without user review — each Path B route changes call-site invocation; the user inspects the diff

Cross-references

  • context/public-surface-contract.md — canonical definition of what's public vs private, both carve-outs, the violation-shape table, and the CI / git-hook consumption techniques
  • /docs-hygiene:extract-ssot verify — 6-gate refuse-fast check for Path A "promote out" decisions
  • /docs-hygiene:extract-ssot execute — writes the SSOT and sweeps citations after a Path A migration
  • /docs-hygiene:extract-ssot — duplication-skill counterpart; an encapsulation violation is one of its failure modes (cross-cite)
  • /docs-hygiene:rename-references — load-bearing multi-pattern sweep after any heading change

Recheck triggers

Condition Action
context/public-surface-contract.md changes Re-sync the Public surface matrix here and the pattern comments in scripts/detect.sh
Scan scope changes in scripts/detect.sh (dirs, file globs, include flags) Update the Detection scan-scope sentence here so docs and script cannot drift
A new CI workflow or git hook needs skill logic Pick a technique from context/public-surface-contract.md "CI / git-hook consumption — entry surface, not internals"; prefer a skill scripts/ facade the hook invokes — no vendored copy
Violations recur across audits Promote the detect action to a scheduled cadence in the consumer repo (e.g. /loop or /schedule)
Anthropic ships a native skill-boundary linter Demote this skill to advisory or sunset it