Defer Claude Code plugin-skill descriptions out of every-turn context, and search them on demand.
A userland implementation of anthropics/claude-code#45332 ("extend ToolSearch deferral to plugin skills"), which was closed not planned. No binary patching — it uses a documented settings key plus a small hook, so it survives Claude Code's auto-updates and is fully reversible.
Unofficial, community project. Not affiliated with Anthropic. Built and verified against Claude Code
2.1.144(native build); the settings key and hook contract it relies on are documented in the CLI itself.
Claude Code injects every installed skill's name + full description into an <available_skills> block on every turn. The cost scales linearly with how many plugins you have and you pay it whether or not any skill is relevant to the task.
Built-in tools got ToolSearch deferral in v2.1.69 (load names, fetch schemas on demand). MCP tools have defer_loading: true. Plugin skills got nothing.
In the author's setup, 270 skills were indexed — roughly 24,000 tokens of descriptions baked into every single prompt. The issue's own example cites ~9,350 tokens for 152 skills.
It splits the skill listing the way ToolSearch already splits tools — names stay, descriptions go on demand:
- Defer the descriptions. Sets the documented
skillListingMaxDescCharssetting low, so the per-turn block becomes- plugin:skill: …(names only). Skills remain fully invocable — theSkilltool resolves by name from the registry, independently of what's displayed. - Keep them discoverable. A local index (
skill-index.json) holds every skill's full description.tss-search "<query>"ranks them with IDF-weighted keyword matching (the same idea as the native ToolSearch matcher) and prints the matches with full descriptions. - Tell the model how. A
SessionStarthook injects a ~50-token pointer explaining that descriptions are deferred and how to search.
| Stock Claude Code | With this | |
|---|---|---|
| Per-turn skill cost | O(n) — every skill's full description | O(1) base (names) + O(k) on query |
| Discovery | Always present, always paid for | tss-search on demand |
| Invocation | Skill tool by name |
Skill tool by name (unchanged) |
| Prompt cache | Prefix shifts as skills change | More stable prefix |
270 skills indexed
full descriptions ≈ 97,000 chars (~24,000 tokens) per turn
names only ≈ 8,000 chars (~2,000 tokens)
deferred ≈ 89,000 chars (~22,000 tokens) off every turn
Savings scale with the number of enabled skills.
The index merges three sources, keyed by exact skill name, longest real description wins:
- Transcripts (authoritative) — Claude Code records the rendered listing into each session transcript as a
{type:"skill_listing"}attachment. This is exactly what was loaded for your environment, including bundledanthropic-skillsand CLI built-ins that aren't files on disk. - Disk —
SKILL.mdfrontmatter underplugins/cacheand~/.claude/skills, for full descriptions and anything not in a recent transcript. - Prior index — so a full description captured before install is never lost when later rebuilds only see truncated (
…) descriptions.
Requires Node 18+.
git clone https://github.com/dnh33/toolsearch-for-skills.git
cd toolsearch-for-skills
node install.mjs # cap = 1 (names only); maximum savings
node install.mjs --cap 60 # keep a short inline hint per skill instead
node install.mjs --dry-run # preview the exact changes, write nothingThe installer:
- copies
bin/+lib/to~/.claude/skill-tools/, - builds
~/.claude/skill-tools/skill-index.json, - backs up
~/.claude/settings.json, then setsskillListingMaxDescCharsand adds oneSessionStarthook (existing hooks are preserved; re-running is idempotent).
Open a new Claude Code session for it to take effect.
node ~/.claude/skill-tools/bin/tss-search.mjs "fill out a pdf form"
node ~/.claude/skill-tools/bin/tss-search.mjs "shopify +checkout" --max 5
node ~/.claude/skill-tools/bin/tss-search.mjs --list # all indexed names+term requires a word. Then invoke the chosen skill by its exact name with the Skill tool.
node uninstall.mjs # removes the setting, the hook, and ~/.claude/skill-tools
node uninstall.mjs --keep-cap # keep skillListingMaxDescChars, remove the restA settings.json backup is written before any change. Open a new session to return to full descriptions.
This was validated against Claude Code 2.1.144 (native build):
- Truncation is real. A headless session run with
skillListingMaxDescChars: 1produced a transcript whoseskill_listingattachment had 108/108 lines as- name: …. The mechanism is the CLI's ownskillListingMaxDescChars(settings schema: "Per-skill description character cap in the skill listing sent to Claude (default: 1536)"). - Full loop, end to end. A real session with the cap and the SessionStart hook active: the hook fired and injected the pointer, descriptions were truncated, the model ran
tss-search "deploy coolify"via Bash, then invokedSkill(skill="coolify")— which resolved with no error. Search → invoke works against the live CLI. - Invocation is unaffected — the
Skilltool validates names against the full registry, not the displayed list. - Search ranks correctly across diverse queries (pdf, pptx, xlsx, coolify, brainstorming, accessibility-review, optimize-images, commit-push-pr…).
- Install/uninstall preserve pre-existing hooks and are idempotent and reversible.
node --test # or: npm test42 zero-dependency tests (Node's built-in runner) cover frontmatter parsing (block scalars, quoting, line-wrapping, field aliases), the IDF/stem/stopword search ranking, the three-source discovery merge (transcript + disk + prior index, including truncation handling), and the full CLI lifecycle — build-index, search, the hook's JSON output, and install/uninstall against throwaway config dirs (dry-run writes nothing, idempotent re-install, existing hooks preserved, malformed settings.json aborts without clobbering, clean reversal).
skillListingMaxDescCharsis global — built-in/bundled skill descriptions are truncated too, not only plugins (the search index covers all of them, so discovery is preserved across the board).- Names are still listed each turn (~2K tokens here). Removing names entirely would require patching the signed, auto-updating native binary — explicitly out of scope; this trades the last ~10% for robustness.
- The index reflects skills seen in recent transcripts + on disk. A brand-new plugin is picked up on the next session (the hook rebuilds when the corpus changes).
- Over-inclusion across environments. The index is the union of skills seen recently, so it can list a skill that isn't enabled in the current session (e.g. a headless
-prun loads fewer skills than interactive, or a different project enables a different set). If the model invokes such a skill theSkilltool returns a recoverableUnknown skill: <name>— it just re-searches or picks another. It never blocks or crashes. Searching from the same kind of session you built the index in keeps names aligned.
See docs/DESIGN.md for the full design and the reverse-engineering notes that back every claim (the INq/Swq/csH skill-listing path, the validateInput invocation gate, and why the binary's own isSkillsAsToolsEnabled path is dead code).
MIT © dnh33