Skip to content

audit-skill-visibility: --installed counts never-enabled plugins as loaded, inflating the fleet ~3x and every starvation verdict with it #4150

Description

@ksexton-sw

Summary

audit-skill-visibility's --installed mode counts plugins that are installed but absent from enabledPlugins as enabled, via its defaultEnabled fallback. On a machine with a large plugin cache this inflates the fleet roughly 3x. Because the fleet is the denominator for the listing-budget contest, every overflow_chars, starved_count, and per-skill starvation.verdict in the report is wrong by that factor.

Observed

Machine: Claude Code 2.1.270, Windows, ~/.claude user scope, 1M-context session.

$ audit_skill_visibility.py --installed --render json --context-window 1000000 --bytes-per-token 4
listing.demand_chars   = 300868
listing.budget_chars   = 200000
listing.overflow_chars = 100868
listing.starved_count  = 467
fleet.plugins_resolved = 204

Bucketing the same run's skills[] by how enabledPlugins resolves each plugin:

bucket skills competing demand
explicit true (63 plugins) 216 108,992
explicit false (37 plugins) 193 0
absent from enabledPlugins (~104 plugins) 752 190,015

The third bucket is 63% of reported demand. It should be zero.

Why the third bucket does not load

Three independent lines of evidence:

  1. A live session's own in-context skill listing contains no skill from any absent plugin. Every plugin skill present in the listing comes from the explicit-true set. Spot-checked: posthog, vercel, wix, netlify-skills, huggingface-skills, fastly-agent-toolkit, example-skills — 275 skills, zero appearances.

  2. Truncation cannot explain absence. SKILL.md states names always survive the budget and only descriptions are shed. An absent name therefore means the skill was never loaded at all.

  3. Per-plugin split matches exactly for enabled plugins. The audit reports claude-ops as 9 competing + 3 exempt-user-only. The live listing shows exactly those 9 at full description length, with lanes, plugins, and setup absent per their disable-model-invocation frontmatter. So the engine is correct for plugins it resolves as enabled — the defect is confined to the absent case.

Root cause

claude-plugins-official's .claude-plugin/marketplace.json declares defaultEnabled on none of its 295 entries. With no marketplace value and no plugin.json value, the resolver falls through to the product default (enabled) and marks the skill model-reachable.

That fallback is right for a plugin the user installed and then left alone, but not for one that is merely present in installed_plugins.json. The reachability model has no state distinguishing the two, so "in the cache" reads as "loaded."

Impact

Beyond wrong numbers, the report actively misleads. Acting on this run's output, I nearly disabled seven vendor plugins to reclaim a reported 96,058 chars of "pure cost, zero visibility." They cost nothing; the saving would have been zero. A report whose remedy is a no-op is worse than no report.

The withheld machinery is doing its job elsewhere in this run — this case simply is not routed into it.

Suggested fix

Give absence from every enabledPlugins scope its own reachability value rather than routing it through defaultEnabled. Candidates: a new not-enabled value, or withheld with a reason naming the scopes checked.

Whatever the value, such rows should contribute no demand_chars, the same way exempt-hidden does today — the existing eligibility machinery already has the right shape, this case just is not mapped onto it.

Worth confirming against the product first: an explicit false and a plain absence may not be the same thing to Claude Code, and the fix should encode whichever it actually does. A live session's own listing is the cheapest oracle.

Correct figures for this machine

For the record, counting only loaded plugins:

Demand 108,992 chars (~27k tokens) across 166 competing skills
Budget @ skillListingBudgetFraction: 0.05, 1M / 4 bpt 200,000 — fits
Budget @ 0.01 (documented default), 1M 40,000 — would overflow by 68,992
Budget @ 0.05, 200k window 40,000 — overflows by 68,992

Two things follow that the inflated run obscured: the 0.05 setting is load-bearing rather than precautionary, and the only real remaining overflow is on 200k-window models, i.e. Sonnet/Haiku subagents.

Environment

  • Claude Code 2.1.270, Windows 11, Git Bash
  • ~/.claude/settings.json: skillListingBudgetFraction: 0.05, skillListingMaxDescChars unset (default 1,536)
  • enabledPlugins: 100 entries (63 true, 37 false); installed_plugins.json: 262 manifest entries → 204 plugins resolved
  • SLASH_COMMAND_TOOL_CHAR_BUDGET unset; CLAUDE_CODE_DISABLE_1M_CONTEXT unset

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions