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 UserPromptSubmit hook — 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. init puts everything in always_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 UserPromptSubmit hook that keyword/regex-classifies prompts against a skill-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-insskillOverrides, disableBundledSkills, skillListingBudgetFraction, skillListingMaxDescChars in settings.json (reference). Static config, restart-bound, no awareness of what you're working on.
  • Profile/config switchers — various dotfile setups swap ~/.claude profiles 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