A Claude Code skill that audits and curates your skills catalog — decide what belongs in the always-on catalog (real skills) and what doesn't (episodic lessons), apply the cut safely and reversibly, and measure the result with an interactive HTML recap. Claude Code now provides the trimming levers natively; this skill is the methodology for using them well.
╔═══════════════════════════════════════════════╗
║ 🚓 C O N T E X T P O L I C E 🚨 ║
║ ▛▀▀▀▜ pull over — that catalog's over budget ║
║ ▙▄☆▄▟ -48% 🪙 saved/turn · ×N on fan-out ║
╚═══════════════════════════════════════════════╝
Claude Code injects the whole catalog of installed skills + agents into context on every turn — and into every subagent you spin up. That's fine with a handful of skills. But if you run a claudeception-style learning loop that mints ~1 new skill per session, your ~/.claude/skills/ quietly balloons to 800+ entries... and every single one is force-loaded, forever, paid again on every fan-out.
The bill (measured on a real ~925-skill catalog): ~248k tokens of skill descriptions per full injection — ~24× over a 1% context budget. As of v2.1.105 (2026-04-13; verify against your install with /doctor) Claude Code enforces that 1% budget natively (skillListingBudgetFraction): it collapses the least-used descriptions to bare names rather than pay the full bill, so ~248k is the opt-in ceiling, not the silent default (see "Claude Code now does part of this natively" below). It's still re-read every turn and multiplied across concurrent subagents, and small-context agent types (like claude-code-guide) can even overflow on launch with "Prompt is too long" at 0 tokens.
context-police is the audit & curation toolkit for it. The trimming levers themselves (skillOverrides, disable-model-invocation, the listing budget) are now native Claude Code features — the durable value here is deciding what to trim (episodic lessons vs real skills), applying it safely, and measuring the outcome:
- 📏 Measure the real per-turn / per-subagent cost (from the cached-prefix token floor).
- ✂️ Trim it — per-project with the verified
skillOverrideslever, or globally withdisable-model-invocation: true— without deleting a single skill (everything stays on disk +/name-invocable). - 📊 Report it — emit a self-contained, clickable HTML recap of exactly what got hidden and why.
- 🧠 Fix the root cause — recognize that most of the bloat is episodic lessons mis-stored as force-loaded skills, then curate them out of the always-on catalog with
disable-model-invocation(triaged by description intent, not name shape). A tempting alternative — surfacing the hidden lessons via an on-demand retrieval hook instead of force-loading — was tested to ground and killed (a base-rate wall: precision-when-firing <0.3% for both keyword and embedding retrievers). The curation flip pays regardless; the hook does not.
It's allow-by-default the whole way down: a skill is only ever hidden when it's clearly irrelevant to your current work, and every move is reversible.
Good news, not bad: Claude Code shipped a native version of this skill's core thesis — in v2.1.105 (released 2026-04-13). Version and defaults below are as of that release — verify against your install with /doctor (its Skills check shows the live values). Two settings do the work:
| Setting | Default (as of v2.1.105) | What it does |
|---|---|---|
skillListingBudgetFraction |
0.01 (1%) |
Caps the catalog at ~1% of the context window. When it's over budget, the least-used skills' descriptions collapse to bare names — still /name- and model-invocable, Claude just can't see why to reach for them. |
skillListingMaxDescChars |
1536 |
Per-skill cap on description + whenToUse (joined with " - "); anything longer is truncated mid-word, silently killing any trigger phrase past the cut. This is /doctor's "N descriptions exceed the per-entry cap" line — gate it at publish time with check_skill_descriptions.py. |
So /doctor now tells you something like "563 skill descriptions will be dropped (10.8%/1% of context)… opting in would cost ~111k tokens every session." Read it right:
- That warning is the budget protecting you — it's context-police running automatically. The ~111k is the bill you'd pay if you opted in to full descriptions; the 1% default silently absorbs it.
⚠️ Do NOT "fix" the warning by raisingskillListingBudgetFraction. That's the anti-pattern — it pays the ~111k every turn and burns rate limits faster. (That figure is in the same ballpark as this skill's earlier ~122k full-description estimate — measured on the current, already-trimmed catalog, so consistent rather than an exact match.)- The aligned fix is the opposite: shrink the catalog so the budget drops irrelevant skills, not useful ones — exactly what this skill's
disable-model-invocationsweep + per-project"off"do./doctor's drop-count is what remains after those levers fire.
The native budget makes context-police more useful, not less: /doctor is now the canonical readout for the cost this skill was built to measure, and the skill tells you which lever actually helps versus the one that backfires.
The catalog-bloat problem and the curation methodology aren't Claude-specific — the Agent Skills open standard (SKILL.md) is shared by Claude Code, Cursor, Codex, Copilot CLI, and Gemini CLI. The always-on listing of skill names+descriptions scales with N on all of them; what differs is whether the harness has a native budget to bound it (researched 2026-06-17):
| Harness | Native listing budget? | Per-skill disable (keep manually-invocable) | Sub-agent ×N? |
|---|---|---|---|
| Claude Code | ✅ skillListingBudgetFraction 1% + skillListingMaxDescChars 1536 (defaults as of v2.1.105 — /doctor shows yours) |
skillOverrides, disable-model-invocation |
yes |
| Codex | ✅ ~2% / 8 000-char cap (descriptions shorten, then omit-with-warning) | allow_implicit_invocation:false / enabled=false |
yes |
| Cursor 2.4 | ❌ none documented | disable-model-invocation: true + paths glob |
unverified |
| Copilot CLI | ❌ none | disable-model-invocation / user-invocable:false + /skills |
no (sub-agents inherit no skills) |
| Gemini CLI | ❌ none | /skills disable + @-invoke |
unverified |
So: on Claude Code and Codex the budget bounds the cost automatically; on Cursor, Copilot CLI, and Gemini CLI there's no documented budget — context-police's manual curation is still the live answer. And disable-model-invocation is part of the standard, so it works verbatim on Cursor and Copilot CLI, not only Claude Code. See the skill's "Porting to another harness" section for the recipe.
Just describe the symptom and Claude reaches for the skill:
You: my subagents are dying with "Prompt is too long" and I have like 800 skills installed.
Claude: [reads context-police] That's the force-loaded catalog. Let me first probe whether it's a
universal overflow or just one small-context agent type, then I'll measure the cost and draft
a per-project skillOverrides denylist (allow-by-default, fully reversible) you can review.
You: do it for this project.
Claude: [picks domain-irrelevant skills — bio DBs, cloud one-offs, single-incident traps that can
never match this stack — sets them "off" in .claude/settings.json, protects your real stack,
then renders an interactive HTML recap of every decision] Restart CC and the catalog shrinks.
The lever is a plain map in your project's .claude/settings.json:
"off" drops the skill from the model-invocable catalog (reclaims its tokens) — the SKILL.md is untouched and still /name-invocable. Set it back to "on" (or delete the entry) to undo. Takes effect on the next Claude Code restart.
Git clone (always works):
git clone https://github.com/wan-huiyan/context-police.git ~/.claude/skills/context-policeClaude Code plugin (marketplace):
/plugin marketplace add wan-huiyan/context-police
/plugin install context-police@wan-huiyan-context-policeEither way, restart Claude Code so the skill is picked up.
| 🙀 Without context-police | 😺 With context-police | |
|---|---|---|
| Catalog cost | CC's 1% budget silently collapses ~half your descriptions to bare names — you don't see which, or what the ~100k+/turn opt-in bill is | Measured via /doctor + the cached-prefix floor; catalog trimmed ~48% per-project so the budget drops irrelevant skills, not useful ones; the ×N fan-out multiplier made explicit |
| Small subagents | claude-code-guide overflows: "Prompt is too long" at 0 tokens |
Probe → it's that agent type's window, not a universal break; trim the noise |
| Cutting noise | Delete skills (lossy, irreversible) or enabledPlugins per-project (footgun — replace-semantics nukes everything you didn't relist) |
skillOverrides per-project (scoped, reversible) — nothing deleted, all still /name-invocable |
| Knowing what changed | A diff of a settings file nobody reads | A clickable, searchable HTML recap of every skill by decision + reviewer reason |
| The real problem | Catalog keeps growing ~1 skill/session, forever | Recognized: most bloat is lessons, not skills → curate them out of the catalog by description intent (a retrieval-hook replacement was tested and killed) |
The "Without" column isn't a strawman — enabledPlugins per-project and bulk-deleting really are the obvious-but-wrong moves; this skill documents why and what to do instead.
- The verified levers, with the gotchas spelled out:
skillOverrides(per-project, in.claude/settings.json) — the right tool for scoping noise to one project. NotenabledPluginsper-project (settings precedence: project replaces user, so it would disable every plugin you didn't relist).disable-model-invocation: true(SKILL.md frontmatter) — the verified global lever: drops a skill's name from the catalog (reclaims the full per-skill cost) while keeping it/name-invocable +rg-reachable.- Two corrections to the naive plan:
"name-only"only reclaims tokens for a skill the native budget is still showing with a description (a most-used one) — the least-used tail is already collapsed to bare names, so for them it's a no-op; only"off"reclaims the name. Andfind-skills/search-skillsearch external marketplaces only — they do not re-surface your hidden local skills.
- A safe wide-denylist method (the conservative cut → ~48%): anchored
startswithmatching (never substring —"ml-"would eathtml-...), an explicit PROTECT allowlist for your real stack, a review-panel vetting that keeps ON anything any reviewer flags (union, not intersection — because a wrongly-hidden relevant skill is the only harm). - A publish-time description-cap gate (
scripts/check_skill_descriptions.py) — the upstream half: stop an oversized description from shipping at all. Zero-dependency, exit 1 on violation, drop it in CI. See below. - An interactive HTML recap (
scripts/render_treatment_report.py) — arcade-styled, self-contained, opens fromfile://, with clickable tiles → a searchable explorer of every skill by decision (off / on / kept / added / override + reason). - The durable root-cause analysis — why the bloat is a knowledge base in the wrong substrate, the two distinct strategies that follow (curation vs retrieval-hook-replacement), and the measured reason only one of them works.
Everything else here curates a catalog you inherited. This is the upstream half — keep an oversized description from shipping in the first place.
python3 scripts/check_skill_descriptions.py . # gate a skill repo (exit 1 on violation)
python3 scripts/check_skill_descriptions.py . --triggers # what truncation is destroying
python3 scripts/check_skill_descriptions.py . --context 1000000 --jsonGoing over the cap doesn't cost tokens — the budget is a hard cap. It costs descriptions. And truncation
isn't intelligent: the harness keeps full[:1535] and appends an ellipsis. A description is trigger text,
so every when the user says "…" phrase past that character position is already dead — the skill won't
fire on it, and nothing reports the loss.
That inverts the usual worry. The instinct is "if I trim the description, will the skill still work?" — but
an over-cap description is already trimmed. The only question is whether you choose what survives, or the
harness chooses by character position. --triggers lists the phrases past the cut, so a deliberate trim is
verifiable: re-run until that section is empty.
Measured on a real 18-plugin install (2026-08-04): 12 skills over cap, 30 trigger phrases invisible. One
skill had lost all 11 triggers for an entire documented feature — "budget mode", "cheap review",
"token-efficient review" and the rest — so a fully-documented feature couldn't be invoked by any of its own
trigger phrases.
Not a body-size check. The body lazy-loads only when the skill fires; the description is resident every turn. A 1,620-char description with a tiny body passes a body-size linter and fails here; the reverse also holds. Independent checks — run both. A body-size pass actively gives false comfort: on
agent-review-panela schliff run (75→86) left the description at 1,501 chars — 35 under the cap, and the next feature commit pushed it to 2,004 with no complaint, because schliff does not measure descriptions.
A gate is itself a claim. Three rounds of audit across seven repos found that the gate's own guarantees were narrower than the sentences describing them — in each case the check ran, went green, and was believed. Each fix below ships with a negative control: break it on purpose, watch it go red.
1. Wrap corruption was scored over model-invocable skills only. corrupt was built from the
non-disabled subset, so a hyphen break inside a disable-model-invocation: true skill was neither
printed nor failed. In agent-traffic-control 74 of 94 skills are disabled — CI was blind for
most of the repo, which is exactly why its four real corruptions had to be found through --json
rather than the gate. The cap check legitimately skips disabled skills (they consume no listing
budget); corruption is different — the description is still read when the skill is invoked by
name, and it ships corrupt the moment the skill is re-enabled. Now scored over every skill, with
disabled hits listed in their own group.
2. APPROACHING CAP hid the urgent cases inside the merely-large ones. WARN_FRACTION = 0.75
spans everything from 1,152 chars upward, so on a real install that bucket held 29 skills
spanning 23 to 340 chars of headroom — same colour, no ordering. This repo's own skill sat at
cap−3 in there while publishing "leave 30–50 chars of headroom", and nothing distinguished it.
New NO HEADROOM tier for anything under MIN_HEADROOM (40), sorted tightest-first, with the
remaining slack printed on every row.
3. Vendored copies rot, and a feature grep cannot see it. Six repos copy this script in.
publish-skill guarded its copy with a test named "the vendored gate is current with upstream,
not a stale fork" that asserted three substrings were present. Its copy was a stale fork — the
drift was inside a function whose name never changed, so all three greps passed and the test
stayed green through the entire drift. SKILL.md now prescribes a pinned sha256 over the file
with its vendoring note stripped, and requires naming the test for what it actually proves.
4. --compare is blind to backticked literals. extract_triggers() matches double-quoted and
curly-quoted spans only. publish-skill's 2,385 → 1,503 trim removed three backticked error
literals from its description and --compare reported 0 DROPPED. An empty table is not proof.
SKILL.md now documents the blind spot and ships a description-scoped hand-diff recipe — scoped
to the frontmatter, because a whole-file diff finds nothing when the body still carries them.
The pattern underneath all four: when a guard's name is a claim, check the claim against the assertion — then break it on purpose. Same round also found a golden-file test whose missing-snapshot branch ran
writeFileSync(...); assert.ok(true), a description-parity comment saying "the four published surfaces" above a loop over five (with a sixth ungated entirely), and a coverage table quoting figures its own committed harness does not reproduce.
Detecting is the easy half. Trimming trigger text is where a careless fix does real damage. Validated
end-to-end on agent-review-panel (2,703 → 1,505 chars, 25 dead triggers recovered):
- Run
--triggersfirst. You aren't deciding whether to cut — the harness already cut. You're deciding what survives. - Compress synonym runs; never delete concepts. The model generalizes from
"cheap review"to"frugal review"; it cannot generalize from a phrase it never sees. - Cut prose, not trigger vocabulary. Implementation detail belongs in the body.
- Keep the NOT-for list — that's precision, and it stops false firing.
- Measure against
old[:cap-1], what the model actually saw — not the full oversized source. The wrong baseline makes every honest trim look like a regression. - Expect the first attempt to regress. The first pass measured 11 better / 18 same / 10 worse —
you'll optimize the distinctive triggers and quietly drop natural-language phrases. Set-difference the
word sets, restore exactly what the regressed prompts need, re-cut. The shipped second pass measures
12 better / 27 same / 0 worse against the committed harness. (The first-pass figure came from an
intermediate state that was never committed and does not reproduce; the second-pass one does — see
agent-review-panelCHANGELOG v3.8.2, which corrects an earlier "13 / 25 / 1".) - Track separation, not just positive coverage — score the negative prompts too, so the trim doesn't buy recall with false firing (+0.2605 → +0.3183).
- Leave ~30–50 chars of headroom. Landing at cap−2 is one edit from breaking again.
Walk the description length across git history to find the breach commit. On agent-review-panel that exposed
the sharpest failure mode of all: v3.7.1's entire stated purpose was "broaden budget-mode triggers for
discoverability" — and all five phrases it added landed past the cut. It shipped, was documented,
changelogged, and delivered exactly nothing.
Adding triggers to an already-over-cap description isn't a no-op — it's a silent no-op that reads as a feature. Check the cap before writing a discoverability release.
In CI:
- run: python3 scripts/check_skill_descriptions.py . --no-colorExit 0 clean · 1 over cap · 2 bad path (so a typo fails loudly instead of passing as a no-op).
After you apply a treatment, render a clickable HTML report of exactly what happened:
# Resolve across all three install roots. A plugin install creates neither of the first two.
S="${CLAUDE_PLUGIN_ROOT:+${CLAUDE_PLUGIN_ROOT}/skills/context-police/scripts/render_treatment_report.py}"
[ -f "$S" ] || S="$HOME/.claude/skills/context-police/scripts/render_treatment_report.py"
[ -f "$S" ] || S="$(find -L "$HOME/.claude/plugins/cache" -mindepth 7 -maxdepth 7 \
-path '*/context-police/*/skills/context-police/scripts/render_treatment_report.py' 2>/dev/null \
| awk -F/ '{print $(NF-4)"\t"$0}' | sort -V -k1,1 | tail -1 | cut -f2-)"
if [ -f "$S" ]; then
python3 "$S" \
--settings .claude/settings.json \
--skills-dir ~/.claude/skills \
--decisions panel-decisions.json \
--title "My Project" \
--out skill-treatment.html
else
echo "render_treatment_report.py: not found - tried \$CLAUDE_PLUGIN_ROOT/skills/context-police/scripts/, ~/.claude/skills/context-police/scripts/, and the plugin cache"
fiWhy the resolver? A
/plugin installputs the script under~/.claude/plugins/cache/<marketplace>/context-police/<version>/skills/context-police/scripts/—~/.claude/skills/context-police/never exists, so a single hardcoded root silently misses and the recap step does nothing while the summary still reads clean. The version is ranked on its own path segment ($(NF-4)), not the whole path, because the marketplace name comes first and would otherwise decide the ordering.
- Data-driven & honest-by-construction: it reads the OFF set straight from your
settings.json, enumerates the skills universe, and computes the bare-name token estimate (Σ(len(name)+3)/4, paid every turn + per subagent). - The
--decisionsfile is optional ({"pulls":[…],"adds":[…],"override":[…]}); omit it for a plain off/on drill-down, pass it to surface the review-panel reasons. - All data is inlined — no server, no build. On macOS,
open skill-treatment.html.
skillOverrides is a per-project symptom fix. The real growth driver is that the mint loop adds ~1 skill/session and force-loads them all forever — and most of those are episodic lessons (single-incident gotchas like flask-flash-silently-dropped-without-base-render). Those aren't skills; they're lessons, and lessons belong in a searchable archive surfaced on demand — not the always-loaded catalog. Two strategies follow from that, and conflating them is the trap:
1. Curation — the real, measured win (and it's now executed). Flip the episodic traps to disable-model-invocation: true: they leave the always-on catalog (reclaiming tokens) while staying /name-invocable and rg-reachable. This pays regardless of any retrieval mechanism. The one prerequisite is a correct trap/procedure classifier — by description intent, not name shape. A hyphen-count heuristic mislabeled 171/886 skills: name-invoked procedures (auto-review-loop, a feature-evaluator) that would be wrongly hidden, and genuine reactive traps kept force-loaded forever because of name markers like worktree/handoff. The discriminator: does the agent go looking for it by name (procedure → keep) or does it only help if surfaced reactively to warn of a mistake (trap → curation candidate)? On a real ~886-skill catalog this curated to 434 traps / 452 procedures, and 404 traps were flipped — after a blind second-rater + 2-of-3 majority tie-break rescued 33 mislabeled procedures from the hide-list (the dangerous direction is a useful playbook mislabeled as a trap).
2. Retrieval-hook-as-replacement — tested to ground, killed. The appealing alternative was a two-trigger retrieval hook (UserPromptSubmit + PostToolUse) that indexes the SKILL.md corpus in place and injects only the top-K relevant traps as additionalContext — letting you hide traps "safely" because the hook would surface them when needed. It can't. A keyword score floor fires on ~99.6% of all turns at any threshold, and every specificity gate (distinctive-token count, IDF-sum, score margin, distinctive-coverage) fails the same way. The load-bearing reason is a base-rate wall: genuine-trap moments are ~0.1% of all triggers, so even a perfect gate would fire ≤0.1% of the time — making precision-when-firing <0.3%, ~99.7% noise at the strictest setting. Embeddings — the one untested precision lever — were then tested and fail identically (semantic cosine over user_prompt: ~23% recall at ~99% firing; best realized precision ≈0.4%). It's arithmetic, not embedder quality. So the shadow hook was removed, and the lever is curation (Strategy 1) plus the agent's own grep-lessons-on-task-start discipline.
Don't sell "the hook makes it safe to hide traps" — that claim is false. Hiding traps is a curation cost/benefit call decided on catalog-cost grounds, not a recall gain.
This skill leans cautious, on purpose. The honest caveats:
- Allow-by-default → the only failure mode is a wrongly-hidden relevant skill. A missed cut is just unrealized savings (harmless); an over-eager cut hides something you wanted. That asymmetry is why the wide-denylist method uses a PROTECT allowlist and a union-not-intersection review panel. It still isn't zero-risk — review the OFF set.
- The global
disable-model-invocationlever rests on an unmeasured premise. Hiding a skill's name assumes bare-name auto-recall is already marginal at scale — that's docs-derived reasoning, not a measured counterfactual (every transcript ever recorded had force-load ON). Treat the global mass-hide as a tradeoff (measured benefit: ~4.9k bare-name → ~122k full-desc tokens/injection — in the same ballpark as/doctor's reported ~111k to opt back into full descriptions (measured on the current, already-trimmed catalog) — every turn × every subagent × every project; unmeasured cost: passive name-recognition), not a slam dunk. The flip is a cost/benefit call you own. - The retrieval-hook replacement was tested and KILLED — don't expect it to make hiding "safe". A keyword score floor fires on ~99.6% of all turns, and every specificity gate (distinctive-token count, IDF-sum, score margin, coverage) hits the same base-rate wall: genuine-trap moments are ~0.1% of triggers, so precision-when-firing is <0.3%. Embeddings fail identically (~23% recall at ~99% firing). It's arithmetic, not a tuning gap — so the shadow hook was removed and curation is the only lever. Two real prerequisites do gate curation: fix the classifier by description intent (a hyphen-count heuristic mislabeled 171/886 skills), and independently re-rate a single-rater hide-list before any destructive sweep (a blind second-rater + tie-break rescued 33 procedures wrongly marked as traps).
- Don't blow away an existing relevance-curated
skillOverrideswhen you add the global flip. They hide different things: the global flag hides traps everywhere (keeps/name); a per-projectskillOverrides"off" map is usually a relevance hide (bio/research skills irrelevant to this repo) that intent labels can't reconstruct. Surgical merge:new_off = old_off − (globally-flipped traps) − (rescued procedures). "name-only"is a no-op for any standalone skill the native budget has already collapsed to a bare name (the least-used tail) — it only reclaims tokens for the most-used skills still showing a description; andfind-skills/search-skillwon't resurface your hidden local skills. Don't rely on either as a safety net.- It can't read minds about your stack. The denylist is a draft for you to review, not an auto-apply.
- Takes effect on restart — the catalog is injected at session start.
No overclaiming: the symptom fix (skillOverrides) is solid and reversible; the root-cause fix is curation (now measured and executed — 404 traps flipped), and the retrieval-hook idea that once looked like the answer is honestly reported as tested-and-dead.
| Dependency | Required? | Without it |
|---|---|---|
| Claude Code | ✅ required | n/a — this is a CC skill |
python3 (3.8+) |
optional | the levers + method still work by hand; you just can't render the HTML recap |
rg / standard CLI |
optional | used for verifying a hidden skill is still reachable |
No third-party Python packages — the recap script is stdlib-only.
The skill ships twice: the repo root (SKILL.md + scripts/) is the source of truth, and plugins/context-police/skills/context-police/ is a byte-identical copy for the plugin marketplace. Don't edit the plugin copy directly — edit the root, then sync:
scripts/dev/sync_plugin_copy.sh # copy root SKILL.md + scripts/ into the plugin
scripts/dev/sync_plugin_copy.sh --check # verify only; exits non-zero on driftCI (npm test, zero-dependency node --test) fails if the copies drift or the manifests disagree. scripts/dev/ is dev tooling and is excluded from the plugin copy.
✅ Quality checklist — what this skill guarantees
- Every lever is verified against
code.claude.com/docs(the settings-precedence +disable-model-invocationbehavior was empirically confirmed, not assumed). - The denylist method is allow-by-default and reversible — nothing is deleted; entries flip back to
"on". - The HTML recap is honest-by-construction — numbers are computed from your actual
settings.json+ skills dir, not hand-entered. - The root-cause curation is gated on a fixed-by-intent classifier + an independent re-rate of the hide-list — the skill asks for both before any destructive sweep.
- Tradeoffs (unmeasured global-hide premise, the tested-and-killed retrieval hook) are stated up front, not buried.
- token-torch — usage dashboard that quantifies the savings this tool produces: its "Catalog savings" panel reads the
disable-model-invocationoutput directly. - memory-hygiene — the right substrate for the episodic lessons this tool evicts from the always-on catalog.
- claude-ecosystem-hygiene — the bundle that distributes context-police alongside its sibling hygiene tools.
concurrent-session-curating-shared-global-dir— the shared~/.claude/skills/dir grows live across parallel sessions.claude-code-subagent-agenttype-overrides-session-model— a different subagent-context gotcha (a workflowagentTypesilently pins a cheap model).- claudeception — the skill-minting loop that causes the bloat in the first place (this skill is its cleanup crew).
- v2.4.0 — The recap step was unreachable on a plugin install.
SKILL.md(and this README) told the agent to runpython3 ~/.claude/skills/context-police/scripts/render_treatment_report.py. A/plugin installnever creates that path — the script lands under~/.claude/plugins/cache/<marketplace>/context-police/<version>/skills/context-police/scripts/— so on the plugin install path the command just failed, and the usual "log it and continue" response means the recap silently does nothing while the run still reads clean. Both call sites now resolve across all three install roots ($CLAUDE_PLUGIN_ROOT→~/.claude/skills/→ a version-rankedfindover the plugin cache) and printnot found - tried <paths>instead of a bare "not installed", which has already been misread by a human as proof a skill was absent.CLAUDE_PLUGIN_ROOTalone does not fix this: it is frequently unset in the shell a step runs in, and it points at the calling plugin's own root so it can never reach a sibling. The version is ranked on its own path segment (awk '{print $(NF-4)}'), not the whole path — the marketplace segment precedes the version, so a plainsort -Vover full paths would letaaa-mkt/2.5.0lose tozzz-mkt/1.0.0. Newtests/plugin-path-resolution.test.mjsextracts the resolver straight out ofSKILL.mdand runs it against a fixture cache, so a regression fails CI rather than being caught by eye. - v2.3.0 — The gate's own guarantees were narrower than the sentences describing them. Four fixes, each with a negative control — see The gap this gate had. (1) Wrap corruption is now scored over EVERY skill, disabled included, and fails the build; it was scoped to the model-invocable subset, so in a repo where 74 of 94 skills are disabled the check was silently blind to most of it — which is why four real corruptions there had to be found via
--jsoninstead of CI. Disabled hits print in their own group. (2) NewNO HEADROOMtier (MIN_HEADROOM = 40):WARN_FRACTION = 0.75lumped a description with 23 chars of slack in with one that had 340 — 29 skills, one bucket, one colour — and this skill sat at cap−3 inside it. Sorted tightest-first, remaining slack on every row. (3) SKILL.md now prescribes a pinned sha256 for vendored copies rather than a feature grep: a "not a stale fork" test built from three substring assertions stayed green on a copy that genuinely was a stale fork, because the drift was inside a function whose name never changed. (4) SKILL.md now documents that--compareonly sees double-quoted spans — backticked literals are invisible, and one repo's trim dropped three of them while--comparereported 0 DROPPED — and ships a description-scoped hand-diff recipe. Also corrects theagent-review-panelseparation figure quoted in the procedure (+26.0 → +32.0 pts→+0.2605 → +0.3183) and the second-pass split (13 / 25 / 1→12 / 27 / 0); those came from an uncommitted one-off and do not reproduce against the harness that now ships in that repo.--jsongainsmin_headroom,counts.critical_headroom, and per-skillcritical/headroom. - v2.2.2 — This skill was three characters from breaking its own rule. The description was 1,533 chars against the 1,536 cap: under it, so nothing was truncated and no trigger was lost — but with 3 chars of headroom, while step 8 of this skill's own trimming procedure says "Leave headroom (~30–50 chars). A trim landing at cap−2 is one edit from breaking again." Retrimmed to 1,483 (53 headroom) by compressing prose only:
the listing of skill names+descriptions→the name+description listing,native harness features→native,one skill's description→a description,only the levers differ→only levers differ. Verified the way this skill tells you to:--comparereports 0 dropped, 0 narrowed with both quoted trigger phrases ("Prompt is too long","name-invoked → restore") intact; the content-word set loses onlydescriptions,features,from,names,one— plurals and filler from those four edits — and gains nothing; no backticked literal dropped; re-wrapped withbreak_on_hyphens=Falseand the gate reports noBROKEN BY LINE-WRAP. Both copies ofSKILL.mdstay byte-identical (plugin-copy-synctest green).- Note on the numbering below. PR #6 was squash-merged as
eedad0fwith the subject "(v2.1.0)", but the branch had already gone 2.1.0 → 2.2.0 → 2.2.1 internally, somainjumped 2.0.0 → 2.2.1 in a single commit and the v2.1.0 entry below is the only record of all three.v2.2.0added wrap-corruption detection and--compare;v2.2.1(6f854ea) fixedfind_wrap_corruption()false-positiving ondescription: >-. Neither is a git tag — this repo's newest tag isv2.0.0, so every "v2.2.x" reference anywhere in this ecosystem means aplugin.json/marketplace.jsonversion, never a release.
- Note on the numbering below. PR #6 was squash-merged as
- v2.1.0 — the publish-time description-cap gate (
scripts/check_skill_descriptions.py) + the measured trimming procedure. Adds the upstream half: measuredescription+whenToUseagainstskillListingMaxDescChars(1536) and fail CI before an oversized description ships. The finding that motivated it: truncation silently kills trigger phrases — the harness keepsfull[:1535]and drops the rest, so any"…"trigger past that position can never fire. Measured on a real 18-plugin install: 12 skills over cap, 30 trigger phrases invisible, one skill having lost all 11 triggers for a fully-documented feature.--triggersnames them, making a deliberate trim verifiable rather than a leap of faith.- New: "Trimming an over-cap description SAFELY" — the 9-step measured procedure, validated end-to-end on
agent-review-panel(2,703 → 1,505 chars, 25 triggers recovered). Key results baked in: compress synonym runs rather than delete concepts; score againstold[:cap-1](what the model actually saw) and not the full source; expect the first attempt to regress (measured 11-better/18-same/10-worse, fixed to 13/25/1 by set-differencing the dropped words); track positive-vs-negative separation, not just recall. - New: regression archaeology — walk the description length across git history to find the breach commit. On
agent-review-panelthis exposed the sharpest failure mode of the whole class: a release whose entire purpose was "broaden triggers for discoverability" added five phrases that all landed past the cut. Adding triggers to an over-cap description is a silent no-op that reads as a feature. - Also corrects the setting's name throughout — it is
skillListingMaxDescChars, notmaxSkillDescriptionChars(nosettings.jsonkey matches the latter; verified against the v2.1.221 binary).
- New: "Trimming an over-cap description SAFELY" — the 9-step measured procedure, validated end-to-end on
- v2.0.0 — harness-agnostic reframe. Led with the portable problem + curation methodology, demoted the Claude Code levers to a clearly-labeled implementation section, added a researched cross-harness landscape (Cursor / Codex / Copilot CLI / Gemini CLI — who has a native budget, who still needs manual curation;
disable-model-invocationis part of the open standard and works verbatim on Cursor + Copilot CLI) and a "porting to another harness" recipe, and folded the retrieval-hook / 122k / forward-sweep work into a compact History footnote. Net: as a Claude-Code "fix the cost" tool the native budget made ~half of it redundant; reframed as "manage skill-catalog cost in any auto-minting harness," its durable relevance is broader. - v1.10.0 — the
disable-model-invocationdual-role + reverse-audit lesson. The flag is also the correct config for a user slash-command (it stops the model auto-firing/changelog,/lfg,/setup… while keeping/name) — not just a trap-hide. So a reverse audit that flags "name-invoked → restore" is a false-positive machine: a full body-read audit of all 487 hidden skills flagged 17 "wrongly hidden," but on a deterministicargument-hint/allowed-toolscheck ~16 were correctly-configured commands (restoring them would let the model auto-fire commands and re-bloat the catalog). Genuine restores ≈ 1. Also: a conservative re-rated forward extension confirmed 0 new safe traps — post-budget the hide-sweep is largely played out; the remaining value is reading/doctorright, the per-projectofflever, and not over-hiding. Added classification-rigor rules (intent-not-name, blind re-rate, deterministic-over-LLM). - v1.9.0 — Claude Code shipped the native catalog budget (
skillListingBudgetFraction1% +skillListingMaxDescChars1536, surfaced by/doctor) in v2.1.105 (2026-04-13) — ~7 weeks before this skill was first written; the original "docs-derived, not measured" note was about this exact mechanism, now verified. Documented it, made/doctorthe canonical readout, flagged raising the budget fraction as the anti-pattern (its ~111k opt-in cost is consistent with — same ballpark as — the old ~122k estimate), and corrected two now-false claims — "standalone skills inject as bare names" and "name-onlyis a blanket no-op" — which only held before the budget made description-dropping usage-ranked. - v1.7.0 — the root-cause work resolved: curation works, the retrieval-hook replacement doesn't.
- Separated the two strategies (curation vs retrieval-hook-replacement) and fixed the trap/procedure classifier to triage by description intent, not hyphen count (171/886 mislabeled).
- Proved the keyword hook can't replace force-load across five gate families — a base-rate wall (genuine-trap moments ~0.1% of triggers), precision-when-firing <0.3%. Embeddings then tested and fail identically (~23% recall @ ~99% firing). Shadow hook removed.
- Curation flip executed — 404 traps
disable-model-invocationd after a blind second-rater + 2-of-3 tie-break rescued 33 mislabeled procedures; claudeception mint-default flipped so new traps mint hidden. Added the "don't clobber a relevance-curatedskillOverrides" merge rule.
- v1.5.0 — root-cause arc: lessons-as-skills → recall-gated retrieval hook; shadow-mode recall@K findings (51% recall / 99.6% injection → not live-ready); S11 corrections (classifier mislabel, subagent leg, embeddings deferred).
- v1.4.0 — renamed
skills-catalog-context-cost-skilloverrides-scoping→context-police; added the interactiverender_treatment_report.pyrecap. - v1.x — the verified
skillOverrides+disable-model-invocationlevers, the precedence gotcha, the safe wide-denylist method, and the overhead-measurement recipe.
MIT © Huiyan Wan
Built with 🪙 and a tiny pixel siren. Meow meow ^.^
{ "skillOverrides": { "alphafold-database": "off", // bio DB — irrelevant to a web app "scanpy": "off", // single-cell genomics — irrelevant "some-one-off-flask-trap": "off", // single-incident lesson, not a reusable skill "react-router-v7-migration": "on" // PROTECT: this is your real stack } }