Hook

W piątek rano odpaliłem /doctor w Claude Code. Zanim wpisałem jeden znak, prompt miał już 6000 tokenów. To nie model myślał. To nie był mój kontekst projektu. To były same opisy skili - wszystkie zainstalowane skille z personal, project i plugin scope, preloaded w system prompt na starcie sesji.

Miałem ~80 skili. Nie dlatego, że je zbieram - marketplace Claude Code sprawia, że instalowanie skili jest bezbolesne, a rozbudowana biblioteka realnie się przydaje. Koszt jest niewidzialny dopóki go nie zmierzysz.

To historia o tym, jak zbudowałem skills-radar - open-source serwer MCP, który rozwiązuje ten problem - w jeden dzień, dlaczego oczywiste podejście nie działa, i czego naprawdę wymaga produkcyjnie sensowne rozwiązanie.

Czego nikt nie naprawił

Pod koniec 2025 Anthropic wypuścił Tool Search Tool dla API. Toole oznaczone defer_loading: true są niewidzialne dopóki Claude nie zawoła wbudowanego tool_search_tool. Ich wewnętrzne liczby: 85% redukcji tokenów, accuracy Opusa 4.5 z 79.5% do 88.1% przy dużych bibliotekach toolów. Krótko potem to samo zostało wypuszczone dla MCP serverów w Claude Code.

Tylko że Tool Search jest dla MCP toolów. Skille to inny mechanizm - pliki w ~/.claude/skills/, ładowane przez Skill tool, nie przez MCP. Anthropic nie wypuścił jeszcze ekwiwalentu dla skili. Issue’y na GitHubie #16160 i #19105 wiszą otwarte.

Więc zbudowałem to. Nie dlatego, że nikt nie próbował - kilka projektów typu mcp-skill-server jest w obiegu - ale dlatego, że żaden z nich nie rozwiązuje problemu u korzenia.

Dylemat discovery

Naiwny RAG po skilach pada na pierwszej przeszkodzie:

Jeśli agent nie widzi, że skille istnieją, nigdy nie zapyta indeksu. Jeśli nigdy nie zapyta indeksu, lazy loading jest bezsensowny.

Większość community-owych projektów dostarcza jeden tool MCP - find_relevant_skill - i zakłada, że Claude zapyta go w każdej turze. Nie zapyta. Bez Tier-1 surface signala mówiącego agentowi “te skille istnieją i mniej więcej robią X, Y, Z”, retrieval na Tier-2 jest niewidzialny. MCP server zostaje nieużywany.

Tę lekcję wyniosłem z czytania prior artu (bobmatnyc/mcp-skillset, back1ply/agent-skill-loader, gotalab/skillport). Każdy łapał kawałki, ale żaden nie łączył: (a) wzorca search-then-load Anthropica, (b) hot-reloadu, (c) trust-tiered threat modelu, (d) air-gapped install path, (e) wsparcia dla wielu klientów.

Two-Tier Discovery - architektura

skills-radar rozdziela discovery na dwa komplementarne sygnały:

Tier 1 - mini-index, ~1k tokenów, zawsze preloaded. Płaska lista name + jednolinijkowy summary per skill, zapisywana do ~/.claude/SKILLS-INDEX.md i importowana w globalnym CLAUDE.md. Mówi agentowi co istnieje.

Tier 2 - load on-demand przez MCP. Dwa toole:

  • search_skills(query, top_k=5, tags=None) - hybrid retrieval (BM25 + dense embeddingi, waga 70/30) po description + when_to_use. Zwraca rankingowane matche z name / description / trust / score / scope.
  • load_skill(name) - pełny zsanityzowany SKILL.md, gdy agent zdecyduje się działać.

Body SKILL.md nigdy nie jest indeksowane do retrievalu - ładowane wyłącznie przy load_skill. Indeks zostaje mały, focused i accurate.

Wynik na moim 60-skilowym korpusie: 6000 tokenów → 1900 tokenów preloaded. ~68% redukcji. Koszt zostaje płaski przy skali do 500 skili.

Czemu dwa toole, a nie jeden? Bo skille są bardziej dyskretne niż toole. Gdy user mówi “use wcag-toolkit-lead”, nazwa jest oczywista - zawołaj load_skill bezpośrednio. Gdy mówi “audit my a11y”, intencja jest fuzzy - najpierw search_skills. Tool Search Anthropica daje jeden tool, bo toole zwykle wołane są po dokładnej nazwie; skille zarabiają drugi tool, bo ich użycie jest bardziej deklaratywne.

Threat model - bez kompromisów, od pierwszego dnia

Plik SKILL.md jest ładowany bezpośrednio do context window agenta jako instrukcje. Z perspektywy modelu różnica między system promptem a body skila jest w zasadzie nominalna - i jedno, i drugie kształtuje zachowanie. Złośliwy skill to wektor system-prompt injection.

Typowe surface’y ataku:

  1. Open-source kolekcje skili (np. 1000+ w awesome-agent-skills) - każdy może coś wrzucić, kontrola jakości różna
  2. Marketplace pluginów - przejęte konto maintainera shippuje złośliwy skill
  3. Skille z klonowanego repo - klonujesz repo i nagle jego .claude/skills/ są w twoich scan paths
  4. Twoje własne przyszłe pomyłki - wkleiłeś coś bez weryfikacji

Naiwny RAG ładuje którekolwiek z tych jako autorytatywne instrukcje. Tego nie chcemy.

skills-radar dostarcza cztery warstwy obrony, nakładane przy ingest:

Trust tier assignment. Każdy skill jest tagowany przy ingest jako TRUSTED (config-explicit) > VERIFIED (Anthropic-official plugin cache) > USER (~/.claude/skills) > UNTRUSTED (cokolwiek innego). Tier widoczny w response z load_skill, więc downstream agent może odmówić UNTRUSTED.

Walidacja frontmattera. Reserved-word rejection (anthropic, claude), format nazwy (≤64 znaków, lowercase + hyphens), wymagane pola, max size 64KB.

Sanityzacja body. Strip XML injection tagów (<system>, <override>, <jailbreak>, …), katalog regexów dla prompt-injection (configurable), opcjonalne stripowanie składni live-execution dla klientów spoza Claude Code.

Size cap. Cap UTF-8 byte-length per SKILL.md. Skille przekraczające cap są odrzucane całkowicie.

To nie sprawia, że community skille są bezpieczne do uruchamiania na ślepo - sprawia, że są mierzalne, z surface area widoczną dla agenta. W połączeniu z explicit trust tiers downstream agenty mogą wdrożyć policy typu “domyślnie odrzucaj UNTRUSTED, wymagaj explicit user opt-in”.

Stack - trzy ścieżki, jedno repo

Default install działa cross-platform na jednej maszynie. Dwie opcjonalne ścieżki dodają mocy: 100% lokalny stack na Apple Silicon (zero sieci, zero chmury, zero Ollamy) i cross-platform LLM-augmented stack przez Ollamę.

LayerDefault (cross-platform)Mac 100% local (MLX)Cross-platform LLM (Ollama)
RuntimePython 3.11+--
MCP SDKmcp (FastMCP)--
Transportstdio (Claude Code) / Streamable HTTP (production)--
Embeddersentence-transformers/all-MiniLM-L6-v2 (90 MB, 384-dim, CPU-fast)MLX Qwen3-Embedding-8B-4bit-DWQ (4096-dim, Apple Silicon)-
LexicalBM25 via rank_bm25--
Vector storeChromaDB (embedded, zero deps)Qdrant (production, reusable across projects - share an instance with sdet-brain)Qdrant
File watcherwatchdog 250 ms debounce--
Query rewriterNoOpMLX Qwen3-Coder-30B-A3B-Instruct-4bit - lazy load + LRU cacheOllama (gemma4:e4b), HTTP fallback
RerankerNoOpMLX Qwen3-Coder-30B-A3B - single-pass batch scoringOllama, per-pair scoring
TelemetryOff (strict opt-in)SameSame

Dlaczego dokładnie taki podział: defaulty są lekkie (90 MB model, zero infrastruktury), żeby open-source community mogło zainstalować przez pip install skills-radar i odpalić od razu. Dwie ścieżki dla power-userów są opt-in - nie puchną base install, ale są podpięte czysto, gdy przełączysz flagi w configu.

Design highlight to 100% lokalny stack na Apple Silicon. Ustaw:

embedder:
  backend: mlx
  model: mlx-community/Qwen3-Embedding-8B-4bit-DWQ
retrieval:
  rewriter:
    enabled: true
    backend: mlx
    model: mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit
  reranker:
    enabled: true
    backend: mlx
    model: mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit

…i cały pipeline - embedding, query rewriting, reranking - leci na twoim M-series GPU + Neural Engine. Bez Ollamy. Bez HTTP. Bez sieci. Identyczne zapytania trafiają w per-instance LRU cache; warm queries kosztują ~3 sekundy w MCP serverze (model w pamięci) zamiast 6+ przy zimnych wywołaniach z CLI.

Jakość retrievalu - liczby, które mają znaczenie

Polish fuzzy query to najczystsza demonstracja tradeoffu. Ten sam 60-skilowy korpus, to samo zapytanie, trzy konfiguracje:

Query: napisz mi post na LinkedIn o WCAG (mixed-language, ambiguous intent - może być “napisz posta o WCAG” albo “znajdź mi skill związany z WCAG”).

ConfigTop 5 (name, score)Verdict
Default sentence-transformers, NoOp rewritercontent-writing-lead (0.54), ffcss-migrate (0.49 false positive), wcag-toolkit-lead (0.42), wcag-dynamic-test (0.32), wcag-report (0.29)top-1 trafia, ale margines jest brzytwą; #2 to przypadkowy skill do migracji Tailwind→FFCSS
Default + Ollama rewriter (gemma4:e4b)content-writing-lead (czystszy top), inne a11y skille się pojawiająmargines się rozszerza, +100-300 ms latency
MLX rewriter (Qwen3-Coder-30B-A3B-Instruct-4bit)a11y-audit (0.71), a11y-orchestrator (0.65), a11y-fix (0.61), wcag-static-analyze (0.56), wcag-fix (0.44)wszystkie 5 hitów to a11y/WCAG, top-1 powyżej 0.7, ~9 s na cold CLI / ~3 s warm w MCP serverze

Dwie rzeczy do zauważenia. Po pierwsze - rewriter nie zachowuje pierwotnej intencji surface’owej. Z włączonym MLX rewriterem content-writing-lead w ogóle się nie pojawia. Rewriter znormalizował “post o WCAG” do keywordów typu web accessibility wcag accessibility standards accessibility standards - co jest OK dla intencji “znajdź mi skill związany z WCAG”, ale jest miss, jeśli rzeczywiście chciałeś pomocy z pisaniem. Tradeoff jest realny i udokumentowany; rewriter jest off by default.

Po drugie - dla angielskich technicznych zapytań default backend już jest solidny (top-1 powyżej 0.6 z czystą separacją). MLX stack zwraca się głównie przy fuzzy / multilingual / casual phrasing, gdzie mały embedder nie wyrabia. Jeśli twój zespół pisze zapytania w angielskim engineering-speak, możesz nigdy nie potrzebować MLX path. Jeśli jesteś mną i połowa promptów jest po polsku, MLX stack to różnica między “good enough most of the time” a “right every time”.

Lokalna telemetria opt-in

Dorzuciłem SQLite event log w ~/.local/share/skills-radar/stats.db. Trzy event kindy - search, load, index - każdy z relevantnymi polami (latency_ms, top1_score, trust tier itd.). Strict opt-in: domyślnie disabled, zero remote telemetry kiedykolwiek.

CLI skills-radar stats pokazuje:

  • Top loaded skills (najczęściej rzeczywiście fetchowane, nie tylko wyszukiwane - mocny sygnał użyteczności)
  • Top queries z częstotliwością
  • Miss rate - searche z top-1 score < 0.4 (skalibrowane z obserwacji: poniżej tego progu ranking jest niewiarygodny)
  • Recent events z detalem per-event

Miss rate powyżej 30% to sygnał, żeby włączyć Ollama rewriter albo upgrade’ować do MLX. Poniżej 15% default stack jest wystarczający.

TUI dashboard

skills-radar tui startuje real-time read-only dashboard na rich.Live z czterema panelami:

┌─ skills-radar v0.3.0a0 · 60 skills · 5/5 paths · embedder=mlx · store=qdrant ─┐
├─ Trust tier breakdown ────────────────────────────────────────────────────────┤
│ TRUSTED    ████████░░░░░░░░  31                                                │
│ VERIFIED   ███████████░░░░░  38                                                │
│ USER       ░░░░░░░░░░░░░░░░   0                                                │
├─ Top queries · miss 12% of 17 ─┬─ Recent events · live ─────────────────────┤
│ wcag accessibility audit    3  │ 18:42  search  0.79  wcag audit  (132ms)    │
│ napisz post na linkedin     2  │ 18:41  load    perf-vue-runtime  (48ms)     │
│ vue memory leak             2  │ 18:40  search  0.67  vue memory leak  ...   │
├─ Top loaded skills ────────────┤                                              │
│ a11y-orchestrator           4  │                                              │
│ perf-vue-runtime            3  │                                              │
│ content-writing-lead        2  │                                              │
└────────────────────────────────┴──────────────────────────────────────────────┘

Stream recent events jest color-coded: zielony przy top-1 ≥ 0.6, żółty ≥ 0.4, czerwony < 0.4. Badge miss-rate w panelu top queries używa tego samego schematu. Możesz mieć to otwarte na drugim monitorze podczas pracy i obserwować jakość searcha w czasie rzeczywistym - bezcenne przy tuningu hub-tagów albo decyzji, kiedy włączyć rewriter.

Hot reload

watchdog obserwuje wszystkie skonfigurowane paths. Każdy created / modified / deleted / moved SKILL.md triggeruje single-record update w indeksie, z debouncingiem 250 ms, żeby zlepić bursty zapisów z edytora. Dodaj nowy SKILL.md, zapisz, zapytaj przez Claude Code natychmiast - bez restartu, bez komendy reindex.

To jest differentiator vs każdy prior art, który ewaluowałem. Nikt inny tego nie ma dobrze. back1ply/agent-skill-loader zbliża się, ale używa substring searcha, który nie skaluje. bobmatnyc/mcp-skillset w ogóle tego nie ma.

Deployment produkcyjny

Dla shared / multi-client / Docker deploymentów skills-radar serve --transport http chodzi na Streamable HTTP zgodnie z guidance MCP Python SDK: stateless_http=True, json_response=True dla horizontal scalability za load balancerem.

Wbudowany Dockerfile jest multi-stage: builder stage instaluje deps i pre-bake’uje model embeddingowy, więc runtime stage startuje w ~2 sekundy zamiast pobierać model 30-60 sekund przy first-run. Runtime jako non-root (uid 1000), z offline HF Hub flagami, więc działa w air-gapped środowiskach. Defaulty wewnątrz kontenera są strict: tier UNTRUSTED + strip_live_exec=true - community skille zamontowane przez Dockera nie powinny móc odpalać komend host-level.

docker-compose.yml montuje twoje ~/.claude/skills i plugin cache read-only i persystuje store ChromaDB jako named volume. Healthcheck POST-uje prawdziwy MCP initialize handshake (nie tylko GET - Streamable HTTP wymaga JSON-RPC body) i raportuje healthy w ~1 sekundę.

Co bym zrobił inaczej

Cztery rzeczy z perspektywy czasu:

  1. Zacząć od threat modelu, nie od retrievalu. Pisałem sanityzację od pierwszego dnia, ale 60% efortu poszło najpierw na retrieval. Problem retrievalu jest ciekawy; threat model jest tym, co czyni narzędzie deployowalnym. Gdybym zaczynał od nowa, najpierw napisałbym sanitize.py i trust tiers, testy do obu, dopiero potem retrieval.

  2. Przetestować BM25 przed założeniem, że hybrid jest konieczny. Moja intuicja mówiła, że czysty BM25 będzie omijał za dużo semantic matchy. Przy krótkich technicznych opisach (50-300 znaków) sam BM25 daje 70-80% rezultatu. Hybrid retrieval zwraca się dopiero przy fuzzy / multilingual queries. Dla hyper-minimalnej wersji BM25-only zashippowałby się tydzień wcześniej.

  3. Flaga disable-model-invocation: true jest bardziej użyteczna, niż początkowo myślałem. Skille oznaczone manual-only są automatycznie filtrowane z search_skills - okazuje się, że niemała frakcja skili to template’y / reference docs, które nie powinny się auto-triggerować. Honorowanie tej flagi od pierwszego dnia jest tanie; retrofitting jest upierdliwy.

  4. Latencja MLX to złe miejsce na premature optimization. Pierwsza wersja MLX rewritera poszła prosto we wzorzec “score every candidate one at a time” (mirroring Ollama). Dla rerank’a 20 kandydatów to 20 osobnych inferencji - minuty per query. Single-pass batch (jeden prompt enumerujący wszystkich kandydatów, model zwraca linie N=score, parsowane regexem) zwija to do jednej inferencji, ~5-15 s dla całej puli. Koszt: parser regex. Reward: usable latency. Ta sama lekcja stosuje się szerzej - przy lokalnych LLM batchuj tyle, ile pozwala context window, zanim zaczniesz tunować rozmiar modelu.

Ekonomia skali

Dla usera z 80 skilami:

StrategyPer-session costWorst case (use 1 skill)
Native Claude Code skill listing~6,000 tokens~6,000 tokens
skills-radar Two-Tier Discovery~1,000 tokens (mini-index)~1,000 + ~2,000 = ~3,000 tokens
skills-radar - multiple loads (5 skills)~1,000 + 5 × 2,000 = ~11,000 tokens(rare - most sessions load 0-2)

Net: w realnym przypadku (1-2 skille loaded per sesja) skills-radar oszczędza ~3-5k tokenów na sesję. Przy skali (500 skili) natywne podejście staje się nieużywalne; koszt skills-radar zostaje płaski.

To nie tylko cost story - to quality story. Badania Anthropica o transformer attention pokazują, że długi kontekst degraduje jakość odpowiedzi. Szczuplejszy prompt daje modelowi szansę zostać focused.

Co zostaje otwarte

Kilka kawałków świadomie zostawionych na następny milestone:

  • FAISS store backend. Lżejszy niż ChromaDB (jeden plik, zero schemy), użyteczny jako fallback w restrictive środowiskach, gdzie nawet SQLite-backed vector DB jest za dużo.
  • Voyage / OpenAI embedder backends. Cloud BYOK jako opcja dla power-userów, którzy chcą best-in-class embedding quality bez lokalnego modelu 4 GB.
  • Auto-discovery z GitHub repo (np. awesome-agent-skills). Jedna komenda CLI ściąga publiczną kolekcję skili do tieru UNTRUSTED z explicit confirm per-skill.
  • Crypto signing dla tieru VERIFIED. Dziś VERIFIED jest path-based (Anthropic-official plugin cache). Cryptographically signed skille z trust manifestem to naturalny następny krok dla community skill ecosystems.
  • LLM-based prompt-injection scanner. Rozszerza katalog regexów. Mały lokalny model (np. gemma4:e4b przez Ollamę) klasyfikuje podejrzane body, których regexy nie złapały. Opt-in.
  • Sandbox bundled_files. Dziś bundled files są enumerowane w response z load_skill. Przyszła wersja mogłaby opcjonalnie sandbox’ować read-only pliki referencowane jeden poziom w głąb, żeby agenty mogły je bezpiecznie pobierać.
  • Multi-language taksonomia hub-tagów. Rekomendowany słownik hub-tags (a11y, perf, qa itd.) musi zostać opublikowany i przyjęty, żeby filtered search był użyteczny przy skali korpusu.

Repo + instalacja

pip install skills-radar
skills-radar config-init
skills-radar index
claude mcp add skills-radar -- skills-radar serve --transport stdio --watch

Zrestartuj Claude Code. /mcp pokazuje skills-radar connected. Odpal skills-radar mini-index i zaimportuj ~/.claude/SKILLS-INDEX.md w globalnym CLAUDE.md. To cały setup.

Source: github.com/darco81/skills-radar. Licencja MIT. Zbudowane w jeden majowy piątek 2026 między innymi rzeczami.