Jak Przyspieszyłem Hooki Claude Code 73x (Dispatcher Pattern)
Jak Przyspieszyłem Hooki Claude Code 73x
Jeśli budujesz z Claude Code i używasz hooków, prawdopodobnie zacząłeś tak samo jak ja: jeden hook na jeden problem. Działa świetnie — dopóki nie przestaje.
To historia jak mój system 115 hooków zjadał 4.7 sekundy overhead na każdej interakcji z agentem i jak naprawiłem to jedną zmianą architekturalną.
Problem: Śmierć Przez Tysiąc Hooków
Hooki w Claude Code to skrypty, które uruchamiają się automatycznie na eventach — przed użyciem narzędzia (PreToolUse), po zakończeniu agenta (SubagentStop), gdy użytkownik wysyła wiadomość (UserPromptSubmit) i inne.
Każdy hook rozwiązuje realny problem:
- master-enforcer blokuje edycję plików bez autoryzacji
- verify-build-success sprawdza czy kod się kompiluje po zmianach
- mandatory-routing kieruje poranną wiadomość do odpowiedniego workflow
Pułapka jest w tym, że każdy hook to osobny proces. Gdy SubagentStop się odpala, Claude Code uruchamia nowy interpreter Python dla każdego zarejestrowanego hooka. Przy 80ms na cold start, 34 hooki to 2.7 sekundy czystego overhead — zanim jakakolwiek logika sprawdzająca w ogóle się uruchomi.
Najgorsze? Odkryłem, że 30+ z tych 34 hooków natychmiast wychodziło. Sprawdzały nazwę agenta, widziały że to nie ich cel, i kończyły pracę. Trzydzieści procesów Python uruchomionych żeby nie zrobić nic.
Liczby (Przed)
| Event | Hooki | Overhead |
|---|---|---|
| SubagentStop | 34 | ~2.7 sekundy |
| PreToolUse | 14 | ~1.1 sekundy |
| UserPromptSubmit | 11 | ~880ms |
| Razem na cykl agenta | 59 | ~4.7 sekundy |
4.7 sekundy podatku na każdą interakcję z agentem. Przy systemie który robi setki wywołań agentów dziennie, to się sumuje do godzin zmarnowanego czasu.
Rozwiązanie: Dispatcher Pattern
Zamiast rejestrowania 34 osobnych skryptów, rejestrujesz jednego dispatcher'a który:
- Czyta jaki agent właśnie skończył pracę
- Sprawdza jakie checky dotyczą tego agenta
- Importuje i uruchamia tylko potrzebne checky in-process (bez spawnowania subprocesów)
- Zwraca jedną scaloną odpowiedź
Diagram Architektury
PRZED (34 osobne procesy):
┌──────────────┐
│ SubagentStop │
│ Event │──→ hook_1.py (spawn process) → exit
│ │──→ hook_2.py (spawn process) → exit
│ │──→ hook_3.py (spawn process) → check → exit
│ │──→ ...
│ │──→ hook_34.py (spawn process) → exit
└──────────────┘
Razem: 34 cold starty × ~80ms = 2,720ms
PO (1 dispatcher process):
┌──────────────┐ ┌─────────────────────┐
│ SubagentStop │ │ dispatcher.py │
│ Event │──→ │ czytaj agent name │
│ │ │ znajdź bundle │
│ │ │ importuj check_3() │
│ │ │ uruchom in-process │
│ │ │ zwróć wynik │
└──────────────┘ └─────────────────────┘
Razem: 1 cold start + wywołania in-process = ~25ms
Krok 1: Agent Tagging
Na SubagentStart, mały hook zapisuje nazwę agenta do pliku:
# agent-tagger.py (SubagentStart hook)
def main(input_data=None):
agent_name = input_data.get('agent_name', '')
with open('/tmp/.last-agent-name', 'w') as f:
json.dump({'agent': agent_name}, f)
Krok 2: Konfiguracja Bundle'i
Plik JSON mapuje każdego agenta do jego bundle'i z checkami:
{
"agent_bundles": {
"web-developer": ["universal", "code"],
"chief-of-staff": ["universal", "health"],
"impl-planner": ["universal", "planning"],
"DEFAULT": ["universal"]
},
"bundles": {
"universal": ["verify-agent-protocol.py", "knowledge-evaluation.py"],
"code": ["verify-build-success.py", "verify-no-dev-servers.py"],
"health": ["verify-morning-output.py", "verify-weight-gate.py"],
"planning": ["verify-confidence-scoring.py"]
}
}
Web-developer uruchamia 8 checków (universal + code). Agent planujący uruchamia 8 (universal + planning). Nieznany agent uruchamia tylko 5 (universal). Zamiast wszystkich 34 za każdym razem.
Krok 3: Dispatcher
Dispatcher czyta tag, ładuje config i importuje każdy check jako moduł Python:
# dispatch_subagent_stop.py
from dispatcher_utils import (
load_config, read_agent_name, import_hook,
run_check_safe, merge_responses
)
def main():
input_data = json.load(sys.stdin)
agent_name = read_agent_name(input_data)
config = load_config()
bundle_names = get_bundles_for_agent(agent_name, config)
check_files = get_checks_for_bundles(bundle_names, config)
results = []
for hook_file in check_files:
module = import_hook(hook_file)
exit_code, stdout, stderr = run_check_safe(module, input_data)
results.append((hook_file, (exit_code, stdout, stderr)))
final_exit, outputs, errors, blocks = merge_responses(results)
sys.exit(final_exit)
Krok 4: Bezpieczny Wrapper
Kluczowy element — każdy check uruchamia się w wrapperze który łapie SystemExit (żeby hooki wywołujące sys.exit() nie zabiły dispatcher'a):
def run_check_safe(hook_module, input_data, hook_name="unknown"):
stdout_capture = io.StringIO()
stderr_capture = io.StringIO()
exit_code = 0
try:
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
hook_module.main(input_data)
except SystemExit as e:
exit_code = e.code if e.code is not None else 0
except Exception:
exit_code = 0 # Nigdy nie blokuj przy crashu hooka
return exit_code, stdout_capture.getvalue(), stderr_capture.getvalue()
Krok 5: Adaptacja Istniejących Hooków
Każdy hook potrzebuje jednej zmiany żeby akceptować przygotowane dane wejściowe:
# Przed
def main():
input_data = json.load(sys.stdin)
# Po
def main(input_data=None):
if input_data is None:
input_data = json.load(sys.stdin)
To zachowuje pełną kompatybilność wsteczną — hooki dalej działają standalone gdy dostaną JSON przez stdin.
Liczby (Po)
| Event | Przed | Po | Przyspieszenie |
|---|---|---|---|
| SubagentStop | ~2.7s (34 procesy) | 23ms (1 proces) | 117x |
| PreToolUse | ~1.1s (13 procesów) | 18ms (1 proces) | 61x |
| UserPromptSubmit | ~880ms (10 procesów) | 23ms (1 proces) | 38x |
| Razem na cykl | ~4.7s | ~64ms | 73x |
Ta sama egzekucja. Te same sprawdzenia. Zero zmian w zachowaniu. Tylko mądrzejsza architektura.
Drugi Zysk: Konsolidacja Reguł
Przy optymalizacji hooków, zastosowałem tę samą zasadę do reguł instrukcyjnych — plików markdown które Claude Code ładuje przy każdej sesji.
Nagromadziłem 1,824 linie reguł w 13 plikach. Wiele zduplikowanych, część sprzeczna ze sobą. Badania nad compliance instrukcji AI pokazały:
- Optymalna ilość zawsze ładowanych instrukcji: 50-200 linii — powyżej tego compliance spada
- Efekt "Lost in the Middle" — instrukcje zakopane w długich plikach są ignorowane przez model
- Progressive disclosure jest lepsze — lekkie jądro ładowane zawsze, szczegóły ładowane na żądanie
Po audycie: 1,824 → 248 linii (-86%). Trzynaście plików zarchiwizowanych, pięć kluczowych zachowanych. Zachowanie agentów faktycznie się poprawiło bo pozostałe instrukcje były jasne, niesprzeczne i wystarczająco krótkie żeby model je w pełni przetworzył.
Kluczowe Zasady
1. Mierz Przed Optymalizacją
Zakładałem że moje hooki są wydajne bo każdy z nich był mały. Nigdy nie zmierzyłem skumulowanego kosztu spawnowania 34+ procesów na event. Zawsze mierz.
2. Przyrostowe Dodawanie Komplikuje System Wykładniczo
Każdy hook był racjonalny indywidualnie. 115-ty hook niczym się nie różnił od pierwszego. Ale koszt systemowy rósł liniowo podczas gdy wartość per-hook nie.
3. Dispatcher Pattern Ma Zastosowanie Wszędzie
Jeden punkt wejścia który routuje warunkowo jest prawie zawsze lepszy niż wiele niezależnych punktów wejścia. To dotyczy hooków, middleware, event handlerów i routów API.
4. Mniej Kontekstu = Lepsza Compliance AI
Dla systemów AI agentów specyficznie: model przetwarza twoje reguły lepiej gdy jest ich mniej. Duplikacja nie dodaje bezpieczeństwa — dodaje szum.
5. Architektura Ponad Dodawanie
Gdy twój system wydaje się wolny lub zawodny, odpowiedzią rzadko jest "dodaj kolejny check". Zwykle to "przeorganizuj jak checky są zorganizowane."
Jak To Zaimplementować
- Policz swoje hooki: sprawdź settings.json
- Zmierz overhead: dodaj timing do jednego hooka żeby zobaczyć koszt cold startu
- Zidentyfikuj marnotrawstwo: ile hooków wychodzi natychmiast dla niepassujących agentów?
- Stwórz dispatcher_config.json: zmapuj agentów do bundle'i z checkami
- Zaadaptuj hooki: dodaj parametr
input_data=Nonedo każdegomain() - Zbuduj dispatchery: jeden na high-frequency event (SubagentStop, PreToolUse)
- Zaktualizuj settings.json: zamień N wpisów na 1 wpis dispatcher'a
- Trzymaj backupy: stary settings.json to twój 2-minutowy plan rollbacku
Dispatcher pattern zamienił mój najwolniejszy komponent systemu w najszybszy. To samo podejście zadziała dla każdego setupu Claude Code z więcej niż 10 hookami.
FAQ
Ile hooków to za dużo?
Nie ma twardego limitu, ale jeśli masz więcej niż 10 hooków na SubagentStop, powinieneś rozważyć dispatcher'a. Overhead rośnie liniowo z ilością hooków.
Czy dispatcher pattern zmienia co jest egzekwowane?
Nie. Te same checky uruchamiają się dla tych samych agentów. Jedyna zmiana jest architekturalna — checky są importowane jako moduły Python zamiast spawnowane jako osobne procesy.
Co jeśli dispatcher crashnie?
Dispatcher opakowuje wszystko w top-level try/except który wychodzi z kodem 0. Crash dispatcher'a nigdy nie zablokuje twojego agenta. Indywidualne hooki można przywrócić w niecałe 2 minuty.
Czy mogę użyć tego z hookami nie-Pythonowymi?
Skrypty shell pozostają jako osobne rejestracje (nie da się ich importować jako moduły). Dispatcher pattern działa najlepiej gdy większość hooków jest w tym samym języku.
Jak dodać nowy check po implementacji dispatcher'a?
Dodaj plik z checkiem, dodaj go do odpowiedniego bundle'a w dispatcher_config.json i dodaj parametr input_data=None do jego main(). Żadna zmiana w settings.json nie jest potrzebna.
O Autorze
Dawid Gac
Edukator E-commerce & Przedsiebiorca
Dawid Gac to polski przedsiebiorca, edukator e-commerce i wspolzalozyciel EcomBrain. Pomaga przedsiebiorcom budowac i skalowac biznesy online przez swoj kanal YouTube, spolecznosc i coaching 1:1.