W sesji Claude Code odpala się 30 zdarzeń hooków. Tylko 3 mogą odpowiedzieć.

Kompletna lista zdarzeń hooków Claude Code, kiedy każde z nich się odpala, które 15 może blokować i reguła stdout, która po cichu połyka wyjście większości hooków. Referencja z pola walki, zbudowana na hookach działających w produkcji przez tysiące sesji agentów.

Dwie awarie wracają raz za razem, kiedy ludzie podpinają hooki do Claude Code, i nie są do siebie ani trochę podobne.

Pierwsza: dodajesz hooka, nic się nie dzieje. Bez błędu, bez ostrzeżenia, bez linijki w logu. Hook po prostu nigdy się nie uruchamia.

Druga: hook wyraźnie działa, widzisz jego efekty uboczne na dysku, ale wiadomość, którą wypisuje dla agenta, nigdy nie dociera. Agent zachowuje się tak, jakby hook nic nie powiedział.

Obie biorą się z tego samego miejsca. System hooków jest większy i mniej jednolity niż garstka zdarzeń, które opisuje większość artykułów, a reguły mówiące o tym, kto może odezwać się do agenta, nie są tymi, których byś się spodziewał. To jest referencja, której nam brakowało. Budujemy AgentsRoom na tych hookach, a wszystko poniżej jest albo zacytowane z oficjalnej referencji, albo zmierzone w produkcji.

Zdarzeń jest 30, nie sześć

Większość poradników omawia PreToolUse, PostToolUse, UserPromptSubmit, Stop, Notification i SubagentStop. Ta szóstka jest prawdziwa i to na niej opiera się większość użytecznej pracy. Jest też jedną piątą tego, co istnieje.

Pełna lista, pogrupowana według tego, co obserwują:

GrupaZdarzenia
SesjaSessionStart, SessionEnd, Setup
PromptUserPromptSubmit, UserPromptExpansion
NarzędziaPreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch
UprawnieniaPermissionRequest, PermissionDenied
TuraStop, StopFailure
Subagenty i zadaniaSubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle
KontekstPreCompact, PostCompact, InstructionsLoaded
ŚrodowiskoFileChanged, CwdChanged, ConfigChange
WorktreesWorktreeCreate, WorktreeRemove
InterfejsNotification, MessageDisplay
Elicitation MCPElicitation, ElicitationResult

Schemat osi czasu 30 zdarzeń hooków Claude Code w kolejności, w jakiej odpalają się podczas sesji agenta, od SessionStart przez UserPromptSubmit, PreToolUse, PostToolUse i Stop aż po SessionEnd, pokazujący, które zdarzenia mogą zablokować agenta.

Kolejność odpalania zdarzeń w jednej sesji. Blok narzędziowy powtarza się raz na każde wywołanie narzędzia, a cały blok promptu raz na turę.

Kilka z nich zmienia sposób myślenia o systemie. PostToolUseFailure istnieje, więc gałąź „czy narzędzie zadziałało" jest zdarzeniem, a nie czymś, co wnioskujesz z payloadu. PostToolBatch odpala się raz, po rozwiązaniu paczki równoległych wywołań narzędzi, i to jest właściwe miejsce, żeby uruchomić linter jeden raz zamiast raz na każdą edycję. InstructionsLoaded odpala się przy odczycie CLAUDE.md, co daje punkt zaczepienia do sprawdzenia, czy agent naprawdę załadował reguły, o których myślisz, że je załadował.

Reguła stdout, która połyka wyjście większości hooków

To najbardziej użyteczna rzecz na tej stronie.

Przy kodzie wyjścia 0 Claude Code parsuje stdout w poszukiwaniu pól JSON. Ale to, czy ten stdout kiedykolwiek zostanie pokazany agentowi, zależy od zdarzenia, a wyjątków jest krótka lista. Z referencji:

W przypadku większości zdarzeń stdout jest zapisywany do logu debug, ale nie jest pokazywany w transkrypcji. Wyjątkami są UserPromptSubmit, UserPromptExpansion i SessionStart, gdzie stdout jest dodawany jako kontekst, który Claude może zobaczyć i wykorzystać.

Trzy zdarzenia na trzydzieści. Jeśli zrobisz echo "uwaga: ta migracja jest destrukcyjna" z hooka PostToolUse i liczysz, że agent to przeczyta, nigdy tego nie zrobi. Twój tekst poszedł do logu debug.

Istnieją dokładnie dwa sposoby, żeby podstawić agentowi tekst z dowolnego innego zdarzenia:

  1. Zakończ z kodem 2 i pisz na stderr. Przy kodzie wyjścia 2 Claude Code ignoruje stdout i cały JSON w nim zawarty, a stderr podaje agentowi jako komunikat błędu.
  2. Zakończ z kodem 0 i wypisz obiekt JSON niosący hookSpecificOutput.additionalContext.

Zwróć uwagę na asymetrię w pierwszym punkcie. Kod wyjścia 0 znaczy, że liczy się stdout, a stderr nie. Kod wyjścia 2 znaczy, że liczy się stderr, a stdout jest wyrzucany w całości. Pomylenie tych dwóch kierunków to powód, dla którego hook może wyglądać całkowicie poprawnie i mimo to być niemy.

Każdy inny kod wyjścia to błąd nieblokujący. W transkrypcji pojawia się komunikat <hook name> hook error z pierwszą linijką stderr, wykonanie trwa dalej, a pełny stderr ląduje w logu debug.

Schemat kodów wyjścia hooków Claude Code: kod 0 wysyła JSON ze stdout do agenta tylko przy trzech zdarzeniach, kod 2 blokuje akcję i wysyła stderr do agenta, każdy inny kod wyjścia to błąd nieblokujący zapisany do logu debug.

Który kanał dociera do agenta, w zależności od kodu wyjścia. Ścieżka przerywana to ta, o której ludzie zakładają, że istnieje, a której nie ma.

Dokładnie połowa z nich może blokować

Piętnaście zdarzeń zatrzymuje akcję przy kodzie wyjścia 2. Piętnaście ignoruje go i idzie dalej.

Mogą blokować: PreToolUse, PermissionRequest, UserPromptSubmit, UserPromptExpansion, Stop, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, ConfigChange, PostToolBatch, PreCompact, Elicitation, ElicitationResult, WorktreeCreate.

Nie mogą blokować: PostToolUse, PostToolUseFailure, PermissionDenied, StopFailure, Notification, SubagentStart, SessionStart, Setup, SessionEnd, CwdChanged, FileChanged, PostCompact, WorktreeRemove, InstructionsLoaded, MessageDisplay.

Praktyczna konsekwencja: zabezpieczenie zakłada się na PreToolUse, nigdy na PostToolUse. PostToolUse odpala się po tym, jak narzędzie zakończyło się sukcesem. Zakończenie tam z kodem 2 nie cofa zapisu, tylko wypisuje błąd, podczas gdy szkody już leżą na dysku. Jeśli chcesz zatrzymać rm -rf, masz dokładnie jedno miejsce, żeby to zrobić.

To, że PostToolBatch blokuje, a PostToolUse nie, warte jest drugiego spojrzenia. Znaczy to, że sprawdzenie na poziomie paczki wciąż może zatrzymać turę po wylądowaniu równoległych edycji, a to najbliższa rzecz do weta po zapisie, jaką ten system oferuje.

Matchery są dokładne, dopóki nagle nie przestaną

Pole matcher zmienia strategię ewaluacji w zależności od własnych znaków i nic ci nie mówi, którą ścieżką poszło.

MatcherEwaluowany jako
"*", "", albo pominiętydopasowuje wszystko
Tylko litery, cyfry, _, -, spacje, ,, |dokładny łańcuch albo lista dokładnych łańcuchów rozdzielona | lub ,
Cokolwiek innegoniezakotwiczone wyrażenie regularne JavaScript

Pułapką jest brak zakotwiczenia. Referencja mówi wprost, że wyrażenie jest testowane przez RegExp.prototype.test, który zwraca sukces przy dopasowaniu gdziekolwiek w wartości. Więc Edit.* dopasowuje Edit oraz NotebookEdit. Jeśli chodziło ci o jedno narzędzie, napisz ^Edit$.

Dwa zachowania zależne od wersji, warte poznania, zanim zaczniesz debugować nie to co trzeba:

  • Przecinki jako separatory i tolerancja na białe znaki wymagają Claude Code v2.1.191 lub nowszego.
  • Myślniki dołączyły do zestawu znaków dokładnego dopasowania w v2.1.195. Wcześniej matcher taki jak code-reviewer był traktowany jak niezakotwiczona regex, więc odpalał się także dla senior-code-reviewer.

W AgentsRoom zawężamy nasz własny hook atrybucji plików przez Write|Edit|MultiEdit|NotebookEdit, co zostaje na ścieżce dokładnego łańcucha i dopasowuje te cztery narzędzia i nic więcej. Hooki cyklu życia, które instalujemy, nie mają żadnego matchera, bo zawsze nas dotyczą.

Hooki może definiować sześć miejsc, i one się łączą

Odruchem jest szukanie kolejności pierwszeństwa. Nie ma jej i to jest właśnie ciekawe.

LokalizacjaZasięg
~/.claude/settings.jsonwszystkie twoje projekty, lokalnie na twojej maszynie
.claude/settings.jsonjeden projekt, do zacommitowania
.claude/settings.local.jsonjeden projekt, gitignorowany przez Claude Code
Managed policy settingscała organizacja, kontrolowane przez admina
hooks/hooks.json pluginudopóki plugin jest włączony
Frontmatter skilla albo agentadopóki komponent jest aktywny

Z referencji:

Wpisy hooków łączą się między poziomami ustawień, zamiast się nawzajem zastępować: ustawienia użytkownika, projektu i lokalne dokładają własne hooki, nie usuwając tych zarządzanych, a ustawienie disableAllHooks nie może wyłączyć hooków zarządzanych spoza ustawień zarządzanych.

Więc hook projektowy nigdy nie nadpisuje globalnego, tylko dokłada się do niego. Sześć źródeł, wszystkie addytywne. Formater PostToolUse zdefiniowany w twoich ustawieniach użytkownika i ponownie w projekcie uruchamia się dwa razy na każdą edycję, a jedynym objawem jest wrażenie, że wszystko chodzi wolno.

Schemat pokazujący sześć lokalizacji ustawień Claude Code, które mogą definiować hooki, łączących się addytywnie w jeden zbiór hooków zamiast nadpisywać się nawzajem.

Sześć źródeł, jeden połączony zbiór. Nic tutaj niczego nie nadpisuje.

To wyjaśnia też, dlaczego .claude/settings.local.json jest właściwym miejscem, żeby narzędzie zainstalowało hooka w czyimś projekcie. Jest ograniczony do projektu, Claude Code go gitignoruje i ładuje się bez żadnej flagi CLI. To tam AgentsRoom zapisuje swoje wpisy, dzięki czemu zacommitowany przez użytkownika .claude/settings.json nigdy nie jest ruszany, a jego koledzy z zespołu nigdy nie dziedziczą ścieżki związanej z konkretną maszyną.

Czego nauczyło nas uruchamianie hooków w produkcji

AgentsRoom instaluje hooki w każdym otwieranym projekcie, żeby deterministycznie śledzić status agentów i przypisywać edytowane pliki do właściwego agenta. Kilka rzeczy widać dopiero w tej skali.

Nieznane nazwy zdarzeń są po cichu ignorowane. Nie ma tego w dokumentacji, a my na tym polegamy. Kiedy dodajemy nowe zdarzenie cyklu życia do naszego instalatora, użytkownicy na starszym CLI dostają settings.local.json zawierający nazwę zdarzenia, o której ich binarka nigdy nie słyszała. Nic się nie psuje, nic nie ostrzega, wpis jest pomijany. To właśnie sprawia, że instalator można bezpiecznie wydać przed wydaniem CLI. To także, nieuchronnie, powód, dla którego literówka daje kompletną ciszę zamiast błędu.

agent_id to sposób, żeby wiedzieć, że jesteś wewnątrz subagenta. Pole jest obecne tylko wtedy, gdy hook odpala się wewnątrz wywołania subagenta. Znaczy to więcej, niż brzmi: Stop odpala się, gdy subagent kończy swoją turę, nie tylko agent główny. Naiwna reguła „oznacz sesję jako zakończoną przy Stop" oznacza całą sesję jako skończoną przy pierwszym powrocie dowolnego subagenta. Dokładnie z tego powodu pomijamy zdarzenia tury niosące agent_id.

Nie czytaj transcript_path dla bieżącej tury. Referencja ostrzega, że transkrypcja jest zapisywana asynchronicznie i może być opóźniona względem rozmowy w pamięci, więc najnowszych wiadomości może tam jeszcze nie być, kiedy twój hook się odpala. Stop i SubagentStop dostają last_assistant_message właśnie po to, żebyś nigdy nie musiał ścigać się z plikiem.

Hooki są jedynym wiarygodnym sygnałem statusu. Zanim pojawiły się hooki, skrobaliśmy PTY, żeby ustalić, czy agent myśli, czeka, czy skończył. To przestaje działać w chwili, w której CLI renderuje przez alternatywny bufor ekranu terminala, a to właśnie robi /tui fullscreen. Hooki odpalają się identycznie pod każdym rendererem. Jeśli budujesz cokolwiek, co obserwuje agenta z zewnątrz, to jest warstwa, na której należy budować, a skrobanie zostaje w najlepszym razie jako rozwiązanie awaryjne.

async: true nic nie kosztuje. Komenda hooka może zadeklarować async: true i agent na nią nie czeka. Nasz hook wysyła POST na lokalny endpoint z limitem 2 sekund i wraca; opóźnienie tury agenta pozostaje bez zmian, nawet gdy aplikacja odbierająca jest zamknięta. Jeśli twój hook tylko obserwuje i nigdy nie decyduje, zrób go asynchronicznym i przestań za niego płacić.

Nigdy nie pozwól hookowi wypisać śmieci do terminala. Nasz skrypt połyka każdy wyjątek, również na najwyższym poziomie. Nieobsłużony traceback z hooka nie zawodzi po cichu: wypisuje pythonowy stos wywołań do sesji terminala użytkownika, w środku jego pracy.

Limity czasu

Wartości domyślne są hojne, z trzema wyjątkami, które hojne nie są:

Typ hookaDomyślny limit czasu
command, http, mcp_tool600 s
prompt30 s
agent60 s
UserPromptSubmit (command, http, mcp_tool)30 s
MessageDisplay (command, http, mcp_tool)10 s
SessionEnd1,5 s dzielone między wszystkie hooki, podnoszone do dłuższego timeout ustawionego dla pojedynczego hooka, maksymalnie do 60 s

Budżet SessionEnd to ten, który ludzi zaskakuje. To budżet dzielony, a nie przydział na hooka, więc trzy hooki sprzątające dzielą między siebie 1,5 sekundy, o ile nie podniesiesz go jawnie.

Wersja skrócona

  • Istnieje 30 zdarzeń. Sześć jest znanych.
  • stdout dociera do agenta wyłącznie przy UserPromptSubmit, UserPromptExpansion i SessionStart. Wszędzie indziej użyj kodu wyjścia 2 ze stderr albo additionalContext w JSON.
  • 15 zdarzeń blokuje przy kodzie wyjścia 2, 15 go ignoruje. Zabezpieczenia idą na PreToolUse.
  • Matchery są dokładnymi łańcuchami, dopóki znak specjalny nie zamieni ich w niezakotwiczoną regex.
  • Sześć źródeł ustawień łączy się addytywnie. Nic niczego nie nadpisuje.
  • Literówka w nazwie zdarzenia zawodzi w kompletnej ciszy.

Jeśli wolisz oglądać, jak te zdarzenia się odpalają, zamiast o nich rozmyślać, właśnie to zbudowaliśmy: AgentsRoom pokazuje każde odpalenie hooka dla każdego agenta, projektu i uruchomienia, w dziesiątkach równoległych agentów i sesjach subagentów. Hooki, które konfigurujesz we własnych ustawieniach, działają dokładnie tak, jak je napisałeś, bo pod spodem chodzi prawdziwe CLI.

Czytaj dalej

Pobierz AgentsRoom

Uruchamiaj swoich agentów AI (Claude, Codex, Antigravity CLI, OpenCode, Aider, Grok Build, Mistral Vibe, Kimi Code) na wszystkich projektach z jednego okna.

Za darmoPobierz AgentsRoom

Aplikacja towarzyszaca: monitoruj agentów w podrozy

Użyj Claude, Codex, Antigravity CLI lub innego dostawcy AI.

Zainstaluj rozszerzenie
Chrome Web Store

Wysyłaj bugi i prośby bezpośrednio do swojego publicznego backlogu.

Spojrzenie na AgentsRoom w akcji.

Wiele projektów
Multi-provider
Wielu agentów
Status na żywo
Diff i commit
Aplikacja mobilna
Podgląd na żywo
Zespoły agentów
Testy w przeglądarce
Dev oparta na backlogu
Biblioteka promptów
Biblioteka umiejętności
Zobacz wszystkie funkcje