Dostępny na nowe rolePiotr Czerwiński

Blog · 22 września 2026 · 10 min czytania

Claude Code hooks w praktyce: jak utrzymać sesję agenta przy jednym temacie

Claude Code · agenci AI · context engineering · narzędzia deweloperskie

TL;DR: Moje sesje z agentem kodującym zamieniały się w jeden długi czat, który rozplątywał dziesięć niezwiązanych wątków, a pomiar moich transkryptów pokazał, że prawie wszystkie tokeny idą na tury powyżej 300K tokenów kontekstu. Spisana reguła "nowy temat, nowa sesja" nic nie dała, bo model tę regułę znał, a ja jej nie przestrzegałem. Pomógł dopiero mały element harnessu agenta: launcher, który uruchamia każdą sesję z zadeklarowanym celem, i hook wywoływany przy każdym prompcie. Hook blokuje prośby nie na temat, proponuje w zamian odłożenie ich do jednolinijkowej skrzynki i eskaluje przy 250K, 400K, 550K i 650K tokenów kontekstu, aż sesja zapisze notatkę przekazania (handoff) i się zakończy. W tym artykule opisuję projekt, progi i powód, dla którego je przesunąłem, oraz błąd w parsowaniu flag, który nauczył mnie ogólnej zasady dotyczącej launcherów.

Problem: sesje, które nigdy się nie kończą

Otwierasz sesję, żeby naprawić błąd. W połowie zauważasz problem z tekstem na innej stronie, potem pojawia się pytanie o analitykę, a potem sprawa SEO. Każda dygresja jest mała i w danej chwili rozsądna. Cztery godziny później sesja trzyma pół miliona tokenów kontekstu, a każda nowa tura czyta to wszystko od nowa.

Koszty da się zmierzyć. W miesiącu transkryptów 88% tokenów wejściowych poszło na tury, w których kontekst przekraczał już 300K, a tura przy 800K kosztuje mniej więcej osiem razy więcej niż tura przy 100K przy odpowiedzi tej samej długości. Cierpi też jakość: model, który przedziera się przez kontekst pełen niezwiązanych wątków, ma więcej rzeczy, które mogą go zmylić.

Oczywistą instrukcję miałem już w globalnym pliku z regułami: nowy temat oznacza nową sesję. Nie zadziałała, a powód warto nazwać wprost. Instrukcja jest skierowana do modelu, ale to ja zbaczam z tematu. Model nie ma prawa odmówić odpowiedzi na moje kolejne pytanie, a ja nie zatrzymam się, żeby otworzyć nowy terminal, kiedy praca idzie mi dobrze.

Czym jest agent harness i gdzie pasuje taki strażnik?

Agent harness (otoczka agenta) to wszystko wokół modelu, co wpływa na jego działanie: launcher, który go uruchamia, instrukcje i pamięć, które ładuje, narzędzia i uprawnienia, które dostaje, oraz hooki uruchamiane przy jego zdarzeniach. Model jest silnikiem; harness decyduje, co model widzi i kiedy się zatrzymuje. Wszystkie rozważane przeze mnie sposoby pilnowania granic sesji znajdują się w różnych częściach tej otoczki.

Mocniejsza instrukcja. Tania i już wypróbowana. Zaleta: zero dodatkowych narzędzi. Wada: zakłada, że model postawi się użytkownikowi w trakcie pracy, a tego nie robi niezawodnie. Do tego instrukcja siedzi w kontekście, który może zniknąć przy kompaktowaniu.

Ręczna dyscyplina z wbudowanymi komendami. Czyścisz kontekst przy zmianie tematu, kompaktujesz po dużym etapie. Zaleta: działa idealnie, kiedy się z niej korzysta. Wada taka sama jak w pierwszej opcji: wszystko zależy od tego, czy zauważę problem, a właśnie to zawodzi.

Niższy próg automatycznego kompaktowania. Zaleta: dzieje się samo. Wada: kompaktowanie streszcza historię w miejscu. Sesja toczy się dalej ze stratnym streszczeniem pięciu pomieszanych tematów, a o samym wątku nic nie zostaje zapisane w miejscu, z którego mógłbym wznowić pracę.

Hook, który pilnuje granicy. Claude Code uruchamia hook UserPromptSubmit, zanim każdy mój prompt dotrze do modelu. Hook może zablokować prompt z powodem, który widzę w terminalu, albo przepuścić go z dodatkowym kontekstem dla modelu. Zaleta: działa na mnie, dokładnie w chwili, gdy zbaczam z tematu, i nie polega na pamięci. Wada: musi wiedzieć, do czego służy sesja, i to właśnie zapewnia launcher. Wybrałem tę opcję.

Launcher, który deklaruje cel sesji

Uruchomienie dobrze skonfigurowanej sesji wymagało kiedyś pamiętania o właściwych flagach: model, poziom rozumowania (effort), które serwery MCP załadować i po który skill sięgnąć. MCP, czyli Model Context Protocol, to standardowy sposób podłączania do agenta zewnętrznych narzędzi, takich jak analityka czy konsole wyszukiwarek. Każdy serwer dokłada do kontekstu definicje narzędzi, więc ładowanie wszystkich wszędzie to marnotrawstwo.

Launcher to krótki skrypt w shellu. Wpisuję komendę i zwykłymi słowami to, co chcę zrobić. Skrypt dopasowuje te słowa do profili zdefiniowanych osobno dla każdego repozytorium i składa pełną komendę: które serwery MCP, który model, jaki poziom effort, a do tego krótką wskazówkę doklejaną do system promptu, który skill załadować i od którego pliku zacząć. Uproszczony przykład, jeden wiersz na jeden cel sesji:

ProfilSłowa kluczoweMCPModelEffortWskazówka
codebłąd, poprawka, test, refaktorbraknajmocniejszyhighzacznij od planu w repo
seofraza, pozycja, sitemapseośrednihighzaładuj skill SEO
quickco to jest, gdzie jest, wyjaśnijbrakmałylowodpowiedz, nie edytuj

Kolumna z modelem trzyma się prostej drabinki (stan na wrzesień 2026): Claude Fable 5.1 wszędzie tam, gdzie błędna odpowiedź kosztuje więcej niż tokeny (plany, incydenty, kod produktu, wszystko, co czyta klient); Opus 5 dla profili, które ładują ciężkie serwery MCP, bo tam koszt to tokeny wejściowe z narzędzi, a nie głębokość rozumowania; Sonnet 5 dla szybkich pytań i mechanicznych edycji. Dwie zasady z praktyki: najpierw obniżaj effort, dopiero potem model, i wybieraj model na starcie sesji, zamiast zmieniać go w połowie. Cache promptu działa per model, więc zmiana oznacza ponowną zapłatę za cały kontekst.

Element, który umożliwia działanie hooka: launcher eksportuje wybrany profil do środowiska sesji. Każdy hook może wtedy zapytać, po co tę sesję uruchomiono. Okazało się, że to najbardziej przenośny pomysł w całym setupie. Każdy launcher, który zna cel sesji, może przekazać go hookom.

Jak utrzymać sesję agenta kodującego przy jednym temacie?

Przy każdym prompcie hook ocenia moją wiadomość względem słów kluczowych każdego profilu. Jeśli inny profil wyraźnie wygrywa (co najmniej dwa trafienia i więcej niż w bieżącym profilu), prompt zostaje zablokowany. Komunikat, który widzę, podaje pasujący profil i dokładną komendę do otwarcia takiej sesji, razem z modelem i poziomem effort, których by użyła. Niektóre profile są z założenia otwarte, na przykład planowanie, gdzie każdy temat jest dozwolony, a niektóre obejmują tematy sąsiednie, więc sesja z kodem nie blokuje pytania o incydent. Bardzo krótkie prompty przechodzą bez sprawdzania, podobnie jak slash commands, a krótki prefiks przepuszcza wszystko, gdy dygresja naprawdę jest częścią bieżącego zadania.

# illustrative shape of the hook contract
event = json.load(stdin)            # includes the prompt and the transcript path
if clearly_other_topic(event["prompt"], session_profile):
    stderr.write(reason_with_resume_command)
    exit(2)                         # block; the reason is shown to the user
note = context_brake(event["transcript_path"])
if note:
    print(json.dumps({"hookSpecificOutput": {"additionalContext": note}}))

Pierwsza wersja miała wadę, którą zobaczyłem dopiero po dwóch dniach używania. Wysyłanie każdej pobocznej myśli do osobnej nowej sesji mnożyło otwarte czaty i skończyło się kilkoma niedokończonymi sesjami, które walczyły o te same pliki. Dlatego blokada daje teraz drugie wyjście: prefiks, który odkłada pomysł na później. Z tym prefiksem prompt przechodzi, a model robi dokładnie jedną rzecz: dopisuje jedną linię do pliku-skrzynki z pilnymi zadaniami w repo (data, tag profilu, co i gdzie) i wraca do bieżącego zadania. W repozytoriach, które mają taką skrzynkę, model domyślnie odkłada tam poboczne pomysły, zamiast odsyłać mnie gdzie indziej. Później jedna dedykowana sesja przechodzi przez skrzynkę od góry i zamyka każdą pozycję linkiem do zmiany, która ją rozwiązała. Pomysł zostaje zapisany w dziesięć sekund, a bieżąca sesja trzyma się swojego tematu.

Progi kontekstu, które kończą się handoffem

Drugie zadanie tego samego hooka to hamulec kontekstu. Hook odczytuje z transkryptu zużycie tokenów przy ostatniej odpowiedzi i eskaluje w czterech krokach, z których każdy uruchamia się raz na sesję:

  • 250K, checkpoint. Zapisz bieżący stan wątku w dokumentacji repo i pracuj dalej.
  • 400K, miękki próg. Zacznij zamykać wątek i napisz handoff. Staram się przejść do nowej sesji gdzieś między tym a kolejnym krokiem.
  • 550K, twardy próg. Skończ w tej turze. Napisanie notatki przekazania jest obowiązkowe: cel, co zrobione, co zostało, otwarte decyzje, dokładny następny krok, pliki do otwarcia. Zrób commit, a potem wypisz jedną komendę, która uruchamia nową sesję wskazującą na tę notatkę.
  • 650K, stop. Żadnej dalszej pracy, tylko handoff i komenda.

Hook przekazuje każdy krok modelowi jako dodatkowy kontekst, a model zaczyna odpowiedź od jednolinijkowego ostrzeżenia, więc widzę je bez czytania czegokolwiek innego. Każdy komunikat niesie też regułę przeciw duplikatom: jedno kanoniczne miejsce na wątek, najpierw przeszukaj dokumentację, aktualizuj istniejący plik w miejscu, nowy plik z handoffem twórz tylko wtedy, gdy żadnego nie ma. Bez tej reguły handoffy zamieniały się w stos niemal identycznych notatek. Handoff w dokumentacji to najtańsza dostępna kontynuacja: następna sesja czyta jeden krótki plik, zamiast dziedziczyć pół miliona tokenów. To, jak układać te pliki, żeby wznawianie pozostało tanie, jest osobnym tematem, który opisuję w artykule o dokumentacji jako pamięci agenta.

Progi nie są tam, gdzie zacząłem. Pierwsza wersja hamowała przy 300K, prosto z pomiaru kosztów. W praktyce hamulec włączał się o wiele za wcześnie: na modelach z oknem miliona tokenów nie widziałem przy 300K spadku jakości, a hamulec przerywał sesje, które szły dobrze. Powyżej mniej więcej 550K rosną koszt i opóźnienie, a jakość zaczyna spadać, więc tam jest twarde zatrzymanie. Checkpoint przy 250K dba o to, żeby stan był zapisany na dysku na długo, zanim cokolwiek wymusi decyzję.

Ten sam hook działa pod Codex CLI od OpenAI, który przyjmuje ten sam kontrakt hooków. Codex sam kompaktuje kontekst przy około 258K, więc dostaje własne progi, wyłącznie z checkpointami: 120K, 200K i 245K. Po kompaktowaniu kontekst spada poniżej pierwszego progu, a hook uzbraja się ponownie, więc checkpointy znów się uruchamiają w kolejnym odcinku sesji.

Błąd, w którym nazwa modelu połknęła prompt

Launcher pozwala przekazać agentowi dodatkowe flagi, na przykład wznowienie ostatniej rozmowy. Raz uruchomiłem sesję z jawnym nadpisaniem modelu, a po nim treścią zadania. Sesja wstała na domyślnym modelu, wypisała ostrzeżenie, że nazwy modelu nie ma w katalogu, i miała okno kontekstu przycięte do 200K.

Przyczyną był parser argumentów. Każdą nierozpoznaną flagę traktował jako przełącznik bez wartości. Nazwa modelu po fladze wpadła więc do treści zadania, a ponieważ przekazywane flagi trafiały tuż przed prompt, agent dostał dwie flagi modelu. Do drugiej z nich całe zadanie przykleiło się jako nazwa modelu.

Poprawka miała cztery części. Nadpisanie modelu stało się flagą, którą obsługuje sam launcher i która ma pierwszeństwo przed kolumną profilu. Flagi przyjmujące wartość i flagi bez wartości są wymienione osobno dla każdego CLI, bo ta sama krótka litera oznacza w obu narzędziach coś innego. Nieznane flagi bez wartości dają teraz ostrzeżenie z prośbą o formę --flag=value, a flaga wymagająca wartości, po której nic nie ma, kończy się błędem. Treść zadania zawsze trafia za samotne --, które parsery obu CLI traktują jako koniec opcji. Sprawdziłem to promptem, który sam zaczyna się od --model. Ogólna zasada dla każdego launchera: wartość flagi nigdy nie może wpaść do tekstu pozycyjnego, a tekst pozycyjny zawsze idzie po --.

Mniejsza lekcja z pisania pod stary bash dostarczany z macOS: w trybie strict funkcja kończąca się fałszywym jednolinijkowym warunkiem zwraca błąd i zabija skrypt bez żadnego komunikatu.

Ograniczenia i co powiedziałbym komuś, kto buduje to jutro

Dopasowywanie słów kluczowych jest toporne. Przepuszcza dygresje ujęte nietypowymi słowami i czasem blokuje wiadomość, która należy do bieżącego zadania. Dlatego istnieje prefiks wymuszający i dlatego próg to dwa trafienia zamiast jednego. Hamulec kontekstu zależy od tego, czy transkrypt wiernie zapisuje zużycie. Handoff jest tak dobry, jak to, co napisze model, więc warto go przeczytać przed startem kolejnej sesji. A launcher pomaga tylko wtedy, gdy uruchomisz go z katalogu głównego repo: uruchomiony z katalogu domowego, raz po cichu wrócił do ogólnego zestawu profili bez skilli i dokumentacji repo. Teraz ostrzega przed tym.

  • Pilnuj granic u człowieka, nie u modelu. Z tematu zbacza użytkownik; hook przy wysłaniu promptu to miejsce, które to widzi.
  • Daj sesjom zadeklarowany cel. Launcher, który eksportuje przeznaczenie sesji, upraszcza każdego późniejszego strażnika.
  • Zaproponuj tańsze wyjście niż nowa sesja. Odłożenie pomysłu w jednej linii pozwala utrzymać skupienie bez mnożenia otwartych czatów.
  • Kończ długie sesje spisanym handoffem. Wznowienie z krótkiej notatki jest lepsze i od kompaktowania, i od ciągnięcia pełnego kontekstu.
  • Ustalaj progi na podstawie używania, potem je przesuwaj. Moja pierwsza liczba pochodziła z tabeli kosztów i w praktyce okazała się zbyt agresywna.
  • Tekst pozycyjny umieszczaj po --. Inaczej każdy wrapper, który przekazuje flagi, prędzej czy później zje prompt.

Pytania, na które odpowiada ten wpis

Jak powstrzymać sesję Claude Code przed rozrastaniem się bez końca?
Użyj hooka UserPromptSubmit, który odczytuje rozmiar kontekstu z transkryptu sesji i eskaluje po przekroczeniu ustalonych progów, na przykład checkpoint przy 250K tokenów i twarde zatrzymanie przy 550K. Przy twardym zatrzymaniu agent zapisuje notatkę przekazania (handoff) w dokumentacji repo i wypisuje jedną komendę, która wznawia pracę w nowej sesji.
Czym jest agent harness?
Agent harness to wszystko wokół modelu, co wpływa na jego działanie: launcher, który go uruchamia, instrukcje i pamięć, które ładuje, jego narzędzia i uprawnienia oraz hooki uruchamiane przy jego zdarzeniach. Model jest silnikiem, a harness decyduje, co model widzi i kiedy się zatrzymuje.
Czy hook w Claude Code może zablokować prompt?
Tak. Hook UserPromptSubmit uruchamia się, zanim prompt dotrze do modelu; wyjście z kodem 2 blokuje prompt i pokazuje użytkownikowi stderr hooka jako powód. Jeśli zamiast tego hook wypisze JSON z additionalContext, prompt przechodzi dalej, a model dostaje dodatkowe instrukcje.