Zespół platformy umożliwia Claude Code w całej swojej organizacji inżynieryjnej. Aby egzekwować politykę dotyczącą wywołań narzędzi, łączą serwer hooków — lokalny proces, który odbiera zdarzenia hooka na stdin i zwraca decyzję w formacie JSON na stdout. Pierwszy tydzień wygląda dobrze. PreToolUse uruchamia się przed każdym wywołaniem narzędzia, hook zwraca deny dla wszystkiego, co dotyczy produkcji, a zespół wierzy, że egzekwowanie działa.
Następnie Claude Code wysyła nową wersję. PermissionRequest zaczyna uruchamiać się razem z PreToolUse. Serwer hooków zwraca te same dane JSON permissionDecision, które już wcześniej zwracał. Claude Code akceptuje odpowiedź, parsuje ją, nie znajduje żadnego pola, które rozpoznaje dla tego zdarzenia i postępuje tak, jakby żadna decyzja nie została podjęta. Odmowa jest cicho ignorowana. Punkt egzekwowania zespołu jest teraz ścianą z gap w środku, a nic w logach o tym nie informuje.
Oto problem, który musi rozwiązać zarządzany PEP: cykl życia hooka Claude Code nie jest jednym zdarzeniem z jednym formatem przewodowym. To w przybliżeniu 30 zdarzeń, z których każde ma inny schemat wyjściowy, który jest respektowany przez środowisko wykonawcze, a błąd w schemacie jest nieodróżnialny od braku odpowiedzi.
Mechanizm przewodowy jest kontraktem bezpieczeństwa
Powodem, dla którego jedno pole permissionDecision nie działa wszędzie, jest to, że zdarzenia hooków Claude Code ewoluowały niezależnie. Kształt wyjściowy, który pozwala na wywołanie narzędzia, nie jest tym samym kształtem, który pozwala na przesłanie podpowiedzi, co nie jest tym samym kształtem, który zatrzymuje zadanie.
Istnieje sześć odrębnych mechanizmów przewodowych:
| Mechanizm | Kształt wyjściowy | Zdarzenia |
|---|---|---|
permissionDecision | hookSpecificOutput.permissionDecision (allow/deny/ask/defer) | Przed użyciem narzędzia |
permissionBehavior | hookSpecificOutput.decision.behavior (allow/deny) | Żądanie uprawnień |
topLevelDecision | Poziom najwyższy decision: "block" + reason | Przesłanie podpowiedzi użytkownika, Rozszerzenie podpowiedzi użytkownika, Wstępne kompaktowanie, Zmiana konfiguracji, Wysyłka partii po użyciu narzędzia |
continueFalse | continue: false + stopReason | Zadanie utworzone, Zadanie zakończone, Członek zespołu bezczynny |
postToolUse | decision: "block" (blokuje dalsze przetwarzanie; narzędzie już działało) | Po użyciu narzędzia |
neutral | Brak wymuszalnego bloku | Wszystkie zdarzenia context/observe, plus odwrócone zdarzenia, takie jak Stop |
Jeśli PEP zwraca permissionDecision: "deny" w zdarzeniu PermissionRequest, Claude Code je ignoruje — to zdarzenie oczekuje decision.behavior, a nie permissionDecision. JSON jest prawidłowy, odpowiedź HTTP to 200, a odmowa nie istnieje. To nie jest teoretyczny przypadek brzegowy; jest to udokumentowany kontrakt przewodowy, zweryfikowany w stosunku do code.claude.com/docs/en/hooks.
Trójstopniowa klasyfikacja: gating, context, observe
Nie każde zdarzenie hook może blokować akcję. Próba odmowy SessionEnd lub Notification jest bezcelowa — te zdarzenia są informacyjne i nie niosą żadnej kontroli decyzji. Próba odmowy zdarzenia Stop jest gorsza niż bezcelowa: decision: "block" na Stop utrzymuje agenta w działaniu, co jest przeciwieństwem zatrzymania bezpieczeństwa.
PEP klasyfikuje każde rozpoznane zdarzenie hook do jednej z trzech kategorii:
Zdarzenia kontrolne niosą decyzję, która może zezwolić, odmówić lub zablokować działanie. Są to powierzchnie egzekwowania. Ale nie wszystkie zdarzenia kontrolne są równie egzekwowalne: Stop i SubagentStop są klasyfikowane jako zdarzenia kontrolne w własnej typologii Claude Code, ale ich semantyka blokowania jest odwrócona (blok = kontynuuj działanie), więc PEP traktuje je jako neutralne — nigdy nie emituje blokady, która utrzymywałaby agenta przy życiu wbrew intencjom operatora. Podobnie, Elicitation i ElicitationResult mają klasyfikację kontrolną, ale brak im mechanizmu działania w obecnej wersji, więc PEP domyślnie traktuje je jako neutralne, zamiast udawać, że egzekwowanie istnieje tam, gdzie go nie ma.
Kontekst zdarzenia pozwalają PEP wstrzykiwać additionalContext lub przepisywać wynik, ale nie mogą naprawdę zablokować działania. PostToolUse to wyjątek partial — może blokować dalsze przetwarzanie oznaczonego wyniku, ale narzędzie już zostało uruchomione. Reszta (PermissionDenied, MessageDisplay, SessionStart, Setup, SubagentStart, PostCompact, InstructionsLoaded, PostToolUseFailure) to obserwuj-z-kontekstem: przydatne do wzbogacania widoku modelu, ale nie do jego zatrzymywania.
Obserwuj zdarzenia (Notification, SessionEnd, StopFailure, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove) nie mają w ogóle kontroli decyzyjnej. PEP rejestruje je dla ścieżki telemetrycznej i inwentarza SIEM, odpowiada neutralnie i przechodzi dalej.
Ta klasyfikacja nie jest doradcza. Określa, czy decydent kompozycji-root stosuje regułę polityki (gating + enforceable), wstrzykuje kontekst (context), czy po prostu obserwuje (observe). Popełnienie błędu oznacza albo fałszywe egzekwowanie (zwracanie odmowy, która jest ignorowana), albo brak egzekwowania (traktowanie wydarzenia gating jako observe).
Mapa hookSpecs: pojedyncze źródło prawdy
Klasyfikacja, mechanizm przewodowy i flaga enforceability znajdują się w jednej mapie. To jest faktyczny kod używany przez konektor — zarówno renderer odpowiedzi HTTP, jak i decydent kompozycji-root odczytują z niej:
type hookSpec struct {
class string // "gating" | "context" | "observe"
mech hookMech
enforceable bool
}
var hookSpecs = map[string]hookSpec{
// GATING — wynik hooka może zastosować allow/deny/block do akcji.
"PreToolUse": {"gating", mechPermissionDecision, true},
"PermissionRequest": {"gating", mechPermissionBehavior, true},
"UserPromptSubmit": {"gating", mechTopLevelDecision, true},
"UserPromptExpansion": {"gating", mechTopLevelDecision, true},
"PreCompact": {"gating", mechTopLevelDecision, true},
"ConfigChange": {"gating", mechTopLevelDecision, true},
"PostToolBatch": {"gating", mechTopLevelDecision, true},
"TaskCreated": {"gating", mechContinueFalse, true},
"TaskCompleted": {"gating", mechContinueFalse, true},
"TeammateIdle": {"gating", mechContinueFalse, true},
"Stop": {"gating", mechNeutral, false}, // odwrócone
"SubagentStop": {"gating", mechNeutral, false}, // odwrócone
"Elicitation": {"gating", mechNeutral, false}, // niepodłączone w v1
"ElicitationResult": {"gating", mechNeutral, false},
// CONTEXT — additionalContext / przepisanie wyjścia, bez rzeczywistego blokowania.
"PostToolUse": {"context", mechPostToolUse, true},
"PostToolUseFailure": {"context", mechNeutral, false},
"PermissionDenied": {"context", mechNeutral, false},
"MessageDisplay": {"context", mechNeutral, false},
"SessionStart": {"context", mechNeutral, false},
"Setup": {"context", mechNeutral, false},
"SubagentStart": {"context", mechNeutral, false},
"PostCompact": {"context", mechNeutral, false},
"InstructionsLoaded": {"context", mechNeutral, false},
// OBSERVE — brak kontroli decyzji.
"Notification": {"observe", mechNeutral, false},
"SessionEnd": {"observe", mechNeutral, false},
"StopFailure": {"observe", mechNeutral, false},
"CwdChanged": {"observe", mechNeutral, false},
"FileChanged": {"observe", mechNeutral, false},
"WorktreeCreate": {"observe", mechNeutral, false},
"WorktreeRemove": {"observe", mechNeutral, false},
}
Trzydzieści zdarzeń, z których każde ma dokładnie jedną klasyfikację, jeden mechanizm przewodowy i jeden werdykt wykonalności. Renderer konsultuje hookMechFor(event), aby zdecydować, jaki kształt JSON wygenerować. Decydent konsultuje HookEnforcementFor(event), aby zdecydować, czy zastosować regułę polityki, wstrzyknąć kontekst czy obserwować. Obaj czytają z tej samej mapy, więc nie mogą się ze sobą nie zgadzać.
Domyślny tryb zamknięty na odmowę
Najważniejsza funkcja w pliku ma cztery linie długości:
func hookSpecFor(event string) hookSpec {
if s, ok := hookSpecs[event]; ok {
return s
}
return hookSpec{class: "unknown", mech: mechPermissionDecision, enforceable: true}
}
Wydarzenie, które nie znajduje się w mapie — ponieważ Claude Code wysłał nowe wydarzenie hook, którego złącze jeszcze nie sklasyfikowało — jest traktowane jako unknown, przypisywane do mechanizmu przewodu mechPermissionDecision i oznaczane jako egzekwowalne. Jest to domyślne ustawienie deny-closed: nierozpoznane wydarzenie jest bramką uprawnień, a nie cichym przepuszczeniem. Jeśli żadna reguła polityki nie pasuje, stosuje się skonfigurowane przez operatora domyślne stanowisko, które w zarządzanym wdrożeniu oznacza odmowę.
Alternatywa — domyślnie neutralnie lub obserwuj — oznaczałaby, że każde nowe wydarzenie hook wprowadzone przez Claude Code pozostaje poza kontrolą, dopóki ktoś go nie zauważy i nie doda do mapy. W modelu deny-closed nowe wydarzenie jest zarządzane od momentu jego wystąpienia, nawet jeśli klasyfikacja jest konserwatywna. Fałszywa odmowa dla nowego wydarzenia jest widoczna i możliwa do naprawienia; ciche zezwolenie jest niewidoczne i może utrzymywać się przez miesiące.
Struktura HookEnforcement eksportuje tę klasyfikację, aby decydent mógł rozróżniać trzy poziomy zdarzeń sterujących:
- Klasyczna brama (
PreToolUse,PermissionRequest,PostToolUsei każde nieznane zdarzenie): gdy żadna kontrolowana reguła nie pasuje, stosuje się domyślną politykę operatora (odmowa - zamknięta). - Brama cyklu życia (inne egzekwowalne zdarzenia sterujące, takie jak
UserPromptSubmit,TaskCreated): decydent stosuje zamiast tego bezpieczny domyślny dla każdego zdarzenia — neutralny dla zdarzeń UX/lifecycle, odmowa dla zdarzeń zmieniających stan. - Brama nieegzekwowalna (
Stop,SubagentStop,Elicitation): decydent zawsze zwraca neutralny wynik. Wydanie odmowy, którą Claude Code interpretuje jako „kontynuuj działanie”, byłoby przeciwieństwem bezpiecznego postępowania.
Dystrybucja: od klasyfikacji do floty
Prawidłowa klasyfikacja zdarzeń to połowa sukcesu. Drugą połową jest wdrożenie PEP na każdej instancji Claude Code. Connector zarządzanych ustawień przekształca konfigurację hooka w kształt oczekiwany przez Claude Code i dystrybuuje ją jako plik ustawień zarządzanych przez serwer — konfigurację niepodlegającą nadpisaniu, którą kontrolna płaszczyzna przesyła do każdego zarządzanego hosta.
Hook PEP jest dystrybuowany z pustym matcherem (dopasowuje wszystkie narzędzia — żadne narzędzie nie uchodzi punktowi egzekwowania) i łączy się go z allowManagedHooksOnly, aby zapobiec podważaniu zarządzanego PEP przez lokalne hooki dewelopera. Bez tego flagu hook na poziomie użytkownika mógłby zasłonić zarządzany hook, a egzekwowanie wyglądałoby na aktywne, podczas gdy w rzeczywistości mogłoby być obejściem. Walidacja w czasie tworzenia wychwytuje to: jeśli polityka dostarcza hook PreToolUse PEP bez allowManagedHooksOnly, konsola generuje ostrzeżenie antymanipulacyjne.
Ścieżka telemetrii jest oddzielna i nieograniczona planem: eksport OpenTelemetry dla Claude Code jest włączony za pomocą zarządzanych zmiennych środowiskowych (CLAUDE_CODE_ENABLE_TELEMETRY, klucze eksportera OTEL_*), dzięki czemu płaszczyzna sterowania może obserwować użycie subskrypcji bez pośredniczenia w wnioskowaniu ani dotykania poświadczeń subskrypcji. Przechwytywanie zawartości (OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_CONTENT) domyślnie jest wyłączone; włączenie go jest świadomym, oznaczonym wyborem, ponieważ wysyła zawartość promptów i narzędzi z maszyny dewelopera, tworząc obowiązek przechowywania i redakcji, za który płaszczyzna sterowania musi odpowiadać.
Co to oznacza dla zarządzanego wdrożenia
Zespół korzystający z Claude Code w połączeniu z zarządzanym PEP otrzymuje kilka właściwości, które mają znaczenie:
- Brak cichego zezwolenia. Każde zdarzenie hook jest klasyfikowane, a każde nierozpoznane zdarzenie domyślnie powoduje odmowę. Nowa wersja Claude Code nie może wprowadzać niezarządzanego zdarzenia cyklu życia.
- Poprawny kształt danych dla każdego zdarzenia. PEP nie zwraca ogólnego obiektu JSON, mając nadzieję, że Claude Code go zaakceptuje. Zwraca dokładny schemat wyjściowy, którego oczekuje konkretne zdarzenie, ponieważ odmowa w nieprawidłowym schemacie nie jest odmową.
- Uczciwe niewymuszanie. Zdarzenia, które nie mogą być wymuszone (zdarzenia obserwowane, zdarzenia z odwróconym sterowaniem) nie są udawane jako wymuszone. PEP je rejestruje, zwraca neutralne wartości i nie daje operatorowi fałszywego poczucia kontroli.
- Ochrona przed manipulacją podczas dystrybucji. Warstwa zarządzanych ustawień zapewnia, że hook PEP nie może być nadpisany i sygnalizuje konfiguracje, w których mogłoby dojść do obejścia.
PEP jest częścią egzekucyjną większego, zarządzanego modelu operacyjnego. Mapa dostępu, dziennik audytu i warstwa polityki jako kod wokół niego są omówione w przeglądzie produktu oraz w dokumentacji haków. Jeśli chcesz zobaczyć, jak kompozytują się ścieżka telemetrii i brama uprawnień, przegląd architektury przeprowadza przez oba.