# Audit Skill Visibility

> Audit whether each installed skill is actually VISIBLE to the model, and diagnose why most of a fleet never gets used. A skill is invisible when the skill-listing context budget drops its description (Claude Code drops by a decay-weighted usage score, so an unused skill loses its matchable keywords and stays unused), when frontmatter is malformed or a description is missing, when skillOverrides or a disabled plugin hides it, or when disable-model-invocation keeps it out of context by design. Reports reachability, observed usage, and whether it is losing the budget contest, computing overflow from documented settings and withholding any verdict the data cannot support. Read-only; never disables, deletes, or edits a skill. Use when: 'why do I never use most of my skills', 'why does Claude never suggest this skill', 'are my skill descriptions being dropped', 'is my skill listing over budget', 'which skills can the model actually see', 'which skills are starved', 'I have too many skills to know when to use them',

- Skill: `melodic-software/audit-skill-visibility` (Agent Skill, multi-file: 13 files)
- Install (CLI): `npx skillmds@latest add melodic-software/audit-skill-visibility`
- Raw SKILL.md: https://api.skillmd.com/api/skills/melodic-software/audit-skill-visibility/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: melodic-software (https://skillmd.com/u/melodic-software)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/melodic-software/audit-skill-visibility

---


## Purpose

Answers one question: **can the model actually see each of my skills, and if not, why?**

That is the question behind the one operators usually ask, which is *why does most of
my skill fleet never get used?* A skill the model cannot see cannot be chosen, so
"unused" is very often a visibility failure wearing a preference costume.

**Visibility is Claude Code's own term** for this: `skillOverrides` is documented under
"Override skill visibility". This skill audits every way a skill loses it.

Claude Code budgets the model-visible skill listing at a fraction of the context
window (`skillListingBudgetFraction`, default 0.01) and, when it overflows,
**sheds descriptions from the lowest-scoring skills first**. Names always
survive, descriptions do not. A skill at zero usage scores zero, so it loses its
description, loses the keywords a request would match against, and stays at
zero. Unused is partly self-causing.

The budget fraction and per-entry cap are owned by
<https://code.claude.com/docs/en/settings> (`skillListingBudgetFraction`,
`skillListingMaxDescChars`); that page is authoritative and matches. **The drop
ORDER is not.** <https://code.claude.com/docs/en/skills> ("Skill descriptions are
cut short") says "starting with the skills you invoke least", still as of
2026-08-31; the binary ranks by a decay-weighted score and then walks the list
first-fit, so neither the ordering nor the guarantee holds. Take the ordering
from the binary: [reference/listing-scorer.md](reference/listing-scorer.md)
carries the counterexamples, the greps, and the stamp.

So the useful question is not *which skills are unused*. Claude Code already
reports that: the built-in `/skill-doctor` command when it resolves in your
session, and the bundled `/doctor` skill's checkup when that one does, each
behind its own gate, with the Stats tab carrying the `/skill-doctor` report in
an interactive session. It is **which skills are starved by
that loop and still wanted, versus genuinely unwanted, versus not observable at
all.**

## The refusal that defines this skill

A usage store younger than the window being asked about **cannot** distinguish
"never invoked" from "never observed". Reporting the second as the first libels
most of a fleet on any fresh install. A days-old install measured against
30-day and 90-day tiers puts nearly the whole fleet in a "never used" bucket.

This skill therefore computes an `observed_horizon`, clamps every window to it,
and routes any claim the span cannot support into a first-class `withheld`
section with its reason. **A declined verdict is reported, never omitted.**

## Run it

It collects live by default. No fixture required. Two inputs resolve
differently, and the difference decides which command you want:

- **Usage** comes from this machine regardless of where you run it
  (`~/.claude.json`, overridable with `--claude-json`).
- **The fleet being audited**, the denominator, is a plugins directory
  enumerated from disk, and it defaults to **`./plugins` relative to your
  current directory**, not to an installed root.

So from a plugins-layout repo (this marketplace, or any checkout with a
`plugins/` directory), bare audits the tree you are standing in:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/skills/audit-skill-visibility/scripts/audit_skill_visibility.py"
```

Anywhere else that exits non-zero with `no skills found`. Nothing is silently
audited. Name the fleet you mean:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/skills/audit-skill-visibility/scripts/audit_skill_visibility.py" \
  --plugins-root <dir>
```

`<dir>` is any directory holding one subdirectory per plugin, each with a
`skills/` directory, the layout both a marketplace checkout and an installed
plugin root use.

### Auditing the INSTALLED fleet

`--plugins-root` measures a directory. To measure what is actually installed
instead, read the plugin manifest:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/skills/audit-skill-visibility/scripts/audit_skill_visibility.py" \
  --installed
```

It reads `~/.claude/plugins` by default (pass a directory to `--installed` to
point elsewhere). The two answers differ on purpose: a checkout can hold
plugins that are not installed, so the repo count and the installed count
disagree. Neither number is wrong; they answer different questions.

**The manifest lists one entry per install SCOPE, not per plugin.** A
marketplace installed at both `project` and `user` scope carries two entries
per plugin. This resolves to one entry per plugin, and the report states both
numbers so the collapse is auditable. Counting entries would inflate the fleet
and, since the fleet is the denominator, roughly double the reported overflow.

Where a plugin is installed at more than one scope, resolution follows the
documented precedence **`local > project > user`**, the record that loads is
the highest-precedence *applicable* one, **never the newest version installed**.
That rule and its "not the newest" warning are stated in this plugin's own
[`skills/plugins/context/scope-semantics.md`](../plugins/context/scope-semantics.md),
which verified it against the official plugins-reference docs. Getting it wrong
is not cosmetic: plugins pinned at different versions across scopes can ship
different skill *sets* and different `description` text. Superseded records are
listed under **Fleet resolution** so a pin being outranked is visible.

Applicability matters as much as precedence: `project` and `local` records load
**only** in the `projectPath` they name, so another project's records are
excluded and reported rather than counted. Current project comes from
`CLAUDE_PROJECT_DIR`, falling back to the working directory.

A marketplace whose source is a local `directory` loads from that **checkout**,
not from either cached `installPath`. Verified by a skill executing out of the
marketplace directory. For those, the plugin's root comes from the catalog's
declared `source`, since `plugins/<name>` is the common layout but not a rule.

`--render json` swaps the Markdown report for the machine-readable model, and
`--now <RFC3339>` pins the clock the horizon is measured against.

`--fixture <bundle.json>` reads a recorded collection instead of the live one, the reproduction path, used by the tests and for handing someone else's state to
the same engine. It is not needed to get a report.

Python 3.11+ is the only requirement. No third-party packages, matching
`inventory.py` and `install_state.py`.

## Reading the output

Three independent fields per skill; a single flat verdict would collapse
questions that demand different actions.

| Field | Answers | Phase |
|---|---|---|
| `observation` | What has actually been seen, within a stated horizon | **live** |
| `reachability` | Can the model ever select this skill | **live** |
| `starvation` | Is it competing for description budget, and likely losing | **live** |

`observation` values: `active` · `cooling` · `dormant` · `no-observation-in-horizon`
· `not-observable`. The last is the default whenever the data cannot support
better, and it is never a synonym for unused.

`reachability` values: `model-reachable` · `user-only` · `hidden` ·
`misconfigured` · `unknown`. Only `model-reachable` with no observation is a
starvation candidate. `user-only` means you type it by design, and
`misconfigured` is a fix. Each carries its causes, evidence, and a remedy.

**The reachability causes are not an official list.** No such list is published;
this catalogue is assembled from scattered documentation plus strings in the
shipped binary, and every row says so in its `provenance`. Do not present it to
a user as documented.

## Counting rules that are not obvious

- **Never sum sources.** Native counters and the JSONL store both record the
  same invocation, so adding them double-counts. At a given instant the count is
  the MAX across sources, while two events from ONE source at the same instant
  still count twice, because those are genuinely two invocations.
- **`pluginUsage` is not a skill signal.** It counts hook, agent, MCP, and LSP
  dispatch, and is seeded at install with `usageCount: 0` beside a current
  `lastUsedAt`. Recency from it is meaningless unless `usageCount > 0`.
- **Ambiguous attribution is reported, not guessed.** Two marketplaces shipping
  a same-named plugin collapse to one usage key; those rows are marked
  `ambiguous-attribution` rather than attributed to one of them.
- **Two possible usage keys per skill.** The stores hold the qualified
  `<plugin>:<leaf>` key and the bare leaf as separate rows. Both are collected; a
  bare key is attributed only when exactly one skill owns that leaf, ambiguous
  ones are withheld with their candidates.
- **The starvation band is decay-weighted, not a count**, and reports
  `score_basis: "unscored"` when nothing survives to weigh. Read
  [reference/listing-scorer.md](reference/listing-scorer.md) before changing that
  ordering or quoting it to a user.

## Scope boundary

| Question | Owner |
|---|---|
| Why is my fleet unused, starved, unwanted, or unobserved? Does skill B get invoked where skill A ran? | **this skill**, the second via `scripts/skill-pair-cooccurrence.sh`, co-occurrence and never attribution ([reference/pair-cooccurrence.md](reference/pair-cooccurrence.md)) |
| Which skills are unused vs their context cost, right now? | Claude Code's own built-in `/skill-doctor` command, when it resolves in your session; the bundled `/doctor` skill's checkup, when that one resolves; the Stats tab carries the `/skill-doctor` report in an interactive session |
| Is a repo's authored listing over budget? | `skill-quality`'s `check-listing-budget.sh` |
| What is installed and invocable? | `/claude-ops:inventory` |
| Is the telemetry pipeline healthy? | `/claude-ops:observability` |

Read-only. It never disables, deletes, or edits a skill, and it never
recommends deleting one it classified as misconfigured. That class is a
fix-me, not a removal candidate.

## Gotchas

- **A short horizon is the normal case, not an error.** Fresh installs, new
  machines, and ephemeral cloud containers all produce spans below the exposure
  floor. The correct output there is a withheld verdict, not a smaller number:
  if a run reports most of the fleet as cold on a days-old install, the report is
  wrong, not the fleet.
- **Do not "fix" a fixture that asserts everything is `not-observable`.** That is
  the honesty floor being tested, and it is the defect this skill exists to
  prevent.
- **Never sum two sources.** Native counters and the JSONL store record the same
  invocation; summing them doubles every count. The reconciliation is MAX across
  sources at an instant, and it is deliberately not MAX across an entire skill:
  two same-instant events from one source are two invocations.
- **OTEL and native counts legitimately disagree.** The native counter is
  debounced (one write per skill per 60 s, suppressing the timestamp refresh
  too); telemetry is not. Divergence is expected and must not be reconciled away.
- **`misconfigured` never renders as a removal candidate.** Several of its causes
  are silent misconfigurations, a skill that looks fine and can never be
  selected. The remedy is a fix.

