skill-valet
Parks the skills you're not using. Fetches them the moment you need them.
A single stdlib-only Python script + a UserPromptSubmit hook that
automatically manages which Claude Code user skills are loaded —
mid-session, per prompt, no restart.
The problem
Every skill in ~/.claude/skills/ costs context every single turn —
its name and description are injected into the per-turn skill list whether
you're using it or not. Install a 19-skill SEO suite, a design toolkit, and
a database pack, and suddenly a session about fixing a Python bug is paying
a skill-list tax on all of them, all the time. Claude also gets worse at
picking the right skill when the menu is huge.
The discovery
Claude Code re-scans the skills directories every turn (this is
documented: changes under
~/.claude/skills/ "take effect within the current session"). Which means:
A
UserPromptSubmithook — which runs before each turn — can move skill directories in and out of~/.claude/skills/and re-shape the very next turn's skill list. Mid-conversation. Automatically.
skill-valet is that hook.
How it works
you hit Enter
│
▼
UserPromptSubmit hook fires ──► skill_valet.py (~22 ms, fail-open)
│
├─ 1. read prompt from stdin JSON
├─ 2. keyword-match prompt against your GROUPS
│ "fix the tailwind hero section" ──► matches group: frontend
│
├─ 3. UN-PARK matched groups ~/.claude/skills-off/design-kit
│ (os.rename — atomic, instant) │ mv
│ ▼
│ ~/.claude/skills/design-kit
│
├─ 4. PARK groups idle ≥ park_after turns (default 20)
│ ~/.claude/skills/seo-suite ──mv──► ~/.claude/skills-off/seo-suite
│
└─ 5. save state atomically, print one line only if something moved:
[skill-valet] on: frontend(3) | parked: seo(19)
│
▼
turn runs with a lean skill list
- Un-parked skills are visible from the next turn — and since the hook runs before your prompt is processed, "next turn" is effectively this conversation, one message later.
- Unmatched prompts change nothing. No keyword hit → no moves. The valet only acts when it's confident.
- Skills you never assign to a group are never touched.
initputs everything inalways_on; you opt skills into management.
Install (3 steps)
1. Copy the script
mkdir -p ~/.claude/skill-valet
cp scripts/skill_valet.py ~/.claude/skill-valet/
2. Run init — scans your skills, writes a safe starter config
python3 ~/.claude/skill-valet/skill_valet.py init
Then edit ~/.claude/skill-valet.json: move the skills you want managed
from always_on into groups (see Config reference below).
3. Add the hook to ~/.claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/skill-valet/skill_valet.py"
}
]
}
]
}
}
(If you already have UserPromptSubmit hooks, add the inner object to your
existing array.)
Optional: copy skill/ into ~/.claude/skills/skill-valet/ so Claude
itself knows how to run status and explain what the valet did.
CLI
python3 skill_valet.py init # scan skills, write starter config
python3 skill_valet.py status # groups: ON/PARKED/PARTIAL, idle turns
python3 skill_valet.py on <group> # force a group in now
python3 skill_valet.py off <group> # force a group out now
python3 skill_valet.py all-on # restore every managed skill
python3 skill_valet.py pause # kill switch: hook becomes a no-op
python3 skill_valet.py resume # re-enable the hook
Config reference — ~/.claude/skill-valet.json
{
"park_after": 20,
"always_on": ["my-precious-skill", "another-untouchable"],
"groups": {
"frontend": {
"skills": ["design-kit", "theme-tools", "canvas-art"],
"keywords": ["css", "tailwind", "landing page", "component", "ui"]
},
"seo": {
"skills": ["seo-audit", "seo-schema", "seo-sitemap"],
"keywords": ["seo", "sitemap", "backlink", "serp", "search console"]
}
}
}
| Key | Meaning |
|---|---|
park_after |
Park a group after this many turns without a keyword hit (default 20). |
always_on |
Skills the valet must NEVER move. Wins over group membership. init puts every discovered skill here. |
groups.<name>.skills |
Directory names under ~/.claude/skills/ that this group manages. |
groups.<name>.keywords |
Case-insensitive, whole-word phrases. Any hit in a prompt activates the group and resets its idle clock. |
FAQ
Why not manage plugins and MCP servers too? They're bound at session start — Claude Code doesn't re-scan them per turn. Only user skill directories are re-scanned, so only those can be switched mid-session. Plugins/MCP need a profile switcher + restart; that's a different (and complementary) tool.
Why does the valet never touch hooks? By design. Hooks are the deterministic safety layer of a Claude Code setup — the thing that blocks dangerous commands and enforces rules. A tool that silently moved hook files would be a security hazard. skill-valet has no code path that reads or writes hooks.
What if the script crashes? Hook mode is fail-open: any error → print nothing, exit 0. Your prompt is never blocked, never delayed beyond the try. Worst case, skills stay where they were.
Multiple Claude Code sessions at once?
Caveat: directory moves are global. Parking a group affects all
concurrent sessions on the machine, not just the one whose prompt triggered
it. If you routinely run parallel sessions on very different topics, use a
generous park_after or pause the valet in one of them.
Does an un-parked skill work on the same prompt that triggered it? No — the skill list for the current turn was already built. It's available from the next message. In practice you rarely notice: the first "let's do some SEO work" message un-parks the group, and the actual work happens over the following turns.
What about skillOverrides / disableBundledSkills in settings.json?
Those are static and restart-bound. skill-valet is dynamic: the set of
loaded skills follows the conversation.
Benchmarks
Measured on an M3 Pro, Python 3.12: ~22 ms per prompt end-to-end
(stdin read → classify → rename dirs → atomic state write). The rename is
os.rename within the same filesystem — metadata-only, effectively free.
You will not feel the hook.
Prior art (honest edition)
Nothing we could find does automatic per-prompt, mid-session skill loading/unloading, but adjacent tools exist and deserve credit:
- Skill Activation Hook
(claudefa.st) — the closest relative. Also a
UserPromptSubmithook that keyword/regex-classifies prompts against askill-rules.json. But it recommends: it injects "consider using skill X" context so Claude reliably activates already-loaded skills. It never changes which skills are loaded, so it doesn't reduce skill-list bloat — skill-valet moves the directories themselves. - Claude Code built-ins —
skillOverrides,disableBundledSkills,skillListingBudgetFraction,skillListingMaxDescCharsinsettings.json(reference). Static config, restart-bound, no awareness of what you're working on. - Profile/config switchers — various dotfile setups swap
~/.claudeprofiles between sessions. Effective for plugins + MCP, but coarse and restart-bound. - vercel-labs/skills #634 — an open feature request for first-class skill enable/disable management, which suggests the itch is real.
If you know of prior art we missed, open an issue — we'll list it.
License
MIT © 2026 Shameer Deen
No comments yet
Be the first to share your take.