Przejdź do treści

Tłumaczenie maszynowe. Wersja angielska jest wiążąca; weryfikacja przez native speakera jeszcze nie nastąpiła.

Claude Code

Wewnątrz hooków PEP: polityka deny-closed w Claude Code

Autor Olivares AI 8 min do przeczytania

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:

MechanizmKształt wyjściowyZdarzenia
permissionDecisionhookSpecificOutput.permissionDecision (allow/deny/ask/defer)Przed użyciem narzędzia
permissionBehaviorhookSpecificOutput.decision.behavior (allow/deny)Żądanie uprawnień
topLevelDecisionPoziom najwyższy decision: "block" + reasonPrzesłanie podpowiedzi użytkownika, Rozszerzenie podpowiedzi użytkownika, Wstępne kompaktowanie, Zmiana konfiguracji, Wysyłka partii po użyciu narzędzia
continueFalsecontinue: false + stopReasonZadanie utworzone, Zadanie zakończone, Członek zespołu bezczynny
postToolUsedecision: "block" (blokuje dalsze przetwarzanie; narzędzie już działało)Po użyciu narzędzia
neutralBrak wymuszalnego blokuWszystkie 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ć.

Zamknięty na odmowę hak PEP: przepływ klasyfikacji zdarzeń i mechanizmu przewodowego

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, PostToolUse i 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:

  1. 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.
  2. 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ą.
  3. 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.
  4. 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.

Powiązane artykuły

Najczęstsze pytania

Dlaczego PEP domyślnie ustawia nieznane zdarzenia hook jako deny zamiast allow?

Ciche zezwolenie na nierozpoznane zdarzenie oznacza, że każdy nowy hak Claude Code, który pojawi się w przyszłej wersji, ominie egzekwowanie, dopóki ktoś ręcznie nie doda go do mapy klasyfikacji. To jest przeciwieństwo postawy bezpieczeństwa, jakiej wymaga zarządzane wdrożenie. Domyślne ustawienie deny-closed traktuje nieznane zdarzenie jako bramkę uprawnień: jest obserwowane, nigdy nie jest odrzucane, a jeśli żadna reguła polityki nie pasuje, jest odrzucane. Zapewnia to, że nowe zdarzenia cyklu życia są kontrolowane od chwili ich pojawienia się, a nie od momentu, gdy ktoś zauważy, że nie zostały.

Co się dzieje, jeśli PEP emituje niewłaściwy format sygnału dla odpowiedzi haka?

Claude Code milcząco to ignoruje. Każde zdarzenie hook honoruje konkretny schemat wyjściowy: PreToolUse oczekuje hookSpecificOutput.permissionDecision, PermissionRequest oczekuje hookSpecificOutput.decision.behavior, a inne zdarzenia kontrolne oczekują pola decyzyjnego najwyższego poziomu lub pola continuidade. Jeśli PEP zwróci poprawny obiekt JSON, ale w niewłaściwym schemacie dla danego zdarzenia, Claude Code traktuje to jako brak decyzji i kontynuuje. Dlatego mechanizm wire jest częścią kontraktu bezpieczeństwa, a nie elementem kosmetycznym: odmowa, której Claude Code ignoruje, nie jest odmową.

Sprawdź dostęp Twoich agentów

Olivares AI to otwarta platforma w modelu hostowanym samodzielnie dla całego środowiska AI w organizacji. Wdrożenie na własnej infrastrukturze zapewnia mapę dostępu, o którą od dawna proszą zespoły bezpieczeństwa i platformowe.