dsh-skill-router
English | 中文
A DeepSeek Harness plugin for the problem nobody warns you about: the skill catalog is announced in full, every session.
DSH injects a name + description line for every model-invocable skill. That is the right design — the model can only pick a skill it can see — but the cost grows linearly and silently. On one real profile measured here (2026-11-13):
| model-invocable skills in the catalog | 573 |
| characters per request | 161,761 (≈ 43k tokens) |
| skills actually invoked in 1,557 sessions | 32 (5.6 %) |
| share of catalog characters spent on never-invoked skills | 94.2 % |
The obvious fix is to mark long-tail skills disable-model-invocation: true. That works — until you discover the two side effects:
- The model can no longer see them at all. Nothing tells it that a page-layout skill exists, so it writes the page from scratch.
- The built-in
skilltool refuses to load them. It requiresmodelInvocable, and archived skills are not. You getskill "x" is not available for model invocation.
This plugin closes both gaps.
What it does
| Tool | Behaviour |
|---|---|
search_skills(query, limit?) |
Searches an index of archived skills. Results carry the upstream source when known (owner/repo#ref @sha). Chinese keywords are expanded through a small alias table, so 幻灯片 finds html-ppt. |
load_skill(name, max_chars?) |
Reads any skill body by name, ignoring the invocation policy, so archived skills load exactly like catalog ones. |
rebuild_skill_index(include_catalog?) |
Rescans the local skill roots, records which skills are archived, attributes them to their upstream repository, and rewrites ~/.dsh/skills-INDEX.json + .md. No external script. |
archive_skills(names, reason?) |
Adds disable-model-invocation: true to the frontmatter and writes a ledger entry. Idempotent. |
restore_skills(names) |
Removes that line again — but only if the file still matches the hash recorded at archive time. If you edited the skill afterwards, it refuses instead of overwriting. |
promote_skill(name, project_root?) |
Copies a skill into <project>/.dsh/skills/<name>, where it is announced only inside that project. Refuses if the target exists, or above 2000 files / 50 MB. |
demote_skill(name, project_root?) |
Removes that copy again. It only deletes a directory the ledger says this plugin created. |
archive + restore are a pair; promote + demote are a pair. Nothing deletes a skill.
How it works
- The index only contains skills that are already archived, so it is roughly the long tail and nothing else.
load_skillreadsSKILL.mddirectly. It does not go through the tool that enforces the invocation policy — that is the whole point.- The ledger lives at
~/.dsh/skill-router/ledger.jsonand recordsbeforeSha256for every archive, which is what makesrestoresafe. - Skills shipped by other plugins live inside their packages, have no
SKILL.mdunder a skill root, and therefore cannot be archived.search_skillswill not find them either; disable the plugin if you want them gone.
Install
From GitHub:
dsh plugin --profile <profile> add github:lion231226/dsh-skill-router
Or through the app: Settings → Plugins → install from a package / GitHub source.
On DSH Desktop the profile is managed by the app — install it from the app's plugin manager rather than the CLI, which would target a different profile.
Quick start
rebuild_skill_indexonce, so the index reflects your machine.archive_skills("html-ppt, card-xiaohongshu, gsap-core")— or archive whatever you never use.- Ask for something those skills cover. The model sees
skill-librarianin its catalog, callssearch_skills, thenload_skill. - If you need one back in the catalog permanently:
restore_skills("html-ppt").
Recommended companion: keep one small skill in your catalog whose description states that an index exists and when not to search it (pure arithmetic, common knowledge, one-file edits). Without that line the model has no reason to believe anything is missing. The plugin's own skill-librarian skill is that line.
Safety
- No deletions. Archive and restore only add or remove one frontmatter line; promote and demote only copy or remove the copy the ledger recorded.
- Restore refuses on drift. If a skill changed after archiving,
restore_skillsreports it rather than overwriting your edit. - Promote is bounded. 2000 files / 50 MB per skill, and an existing destination is never replaced.
- No network. Nothing is fetched, no telemetry, no credential access.
Tests
npm test
27 assertions across three files: plugin wiring with a mock context (7 tools register, required parameters normalise correctly, disposers release), index/manage logic in temp directories (iteration, hash-checked restore, tamper refusal, promote/demote, limits, ledger backfill), and search quality against a real 448-entry index.
Compatibility
- Host:
dsh >= 0.2.0-rc.1. Host-only plugin — no browser UI, no client bundle. - Reads
~/.dsh/skills-INDEX.json; falls back to~/.dsh/skills-INDEX.md. @deepseek-ai/dsh-toolsis imported at runtime but deliberately not declared as a dependency: declaring it would let pnpm hoist a second copy into the profile, and a stale copy shadowing the host's own is the classic way a DSH plugin breaks an entire profile. The host provides it.
License
MIT
No comments yet. Be the first to write one.