"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."
Resources
3Install
npx skillscat add melodic-software/claude-code-plugins/plugins-docs-hygiene-skills-audit-encapsulation Install via the SkillsCat registry.
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) exceptscripts/(entry-surface carve-out, see below) - Heading-anchor cites into
SKILL.mdbody structure (<X>/SKILL.md#<anchor>) - Any
*.schema.jsonfile 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 detectFilter 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.
- Apply
/docs-hygiene:extract-ssot verify(the 6-gate refuse-fast check) to the private file's content - 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) - Migrate the violator AND the original skill body to cite the new doc by heading
- Delete or shrink the private file
- Run
/docs-hygiene:rename-referencesto 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.
- Identify which skill action would deliver the desired behavior
- If the action exists: rewrite the caller to invoke
/<skill-name> <action> <args> - 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 filingAnti-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, .NETinternal, 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 |