Przejdź do głównej zawartości

Hooki agenta — jedyny sposób na wymuszenie 'za każdym razem X'

Q13 · Rozszerzalność Ile masz aktywnych hooków (Stop, PreToolUse, PostToolUse, SessionStart…)?

Odpowiedź na maksa: “5+ zorkiestrowanych: auto-PR-watch, fix-on-review, wystawianie narzędzi MCP wewnątrz hooków.”

Hooki to jedyne miejsce w setupie AI, gdzie reguły stają się gwarancjami. Wszystko inne — CLAUDE.md, pliki pamięci, system prompt, slash commandy — to sugestia, którą model może zastosować, sparafrazować albo po cichu zapomnieć sześć tur w głąb pętli. Hook działa na poziomie shella: uruchamia się bezwarunkowo, harness blokuje lub przepuszcza wywołanie na podstawie kodu wyjścia, model nie ma głosu. Jeśli łapiesz się na pisaniu w pamięci “zawsze odpalaj formatter po edycji” albo “za każdym razem gdy skończysz ficzer, otwórz PR” — nie potrzebujesz lepszego prompta, potrzebujesz hooka.

W 2026 powierzchnia hooków wreszcie dogoniła ambicje. Claude Code ma 12+ zdarzeń cyklu życia. Cursor ma taki sam zestaw eventów plus dwa własne — beforeShellExecution i beforeMCPExecution — pokrywające dwie najryzykowniejsze powierzchnie narzędzi. Codex CLI nadal trzyma się starszego zdarzenia notify, ale na początku 2026 wprowadził pełnoprawny system hooks.json. Mur między “co agent chce zrobić” a “co mu pozwalasz” stał się przepuszczalny na Twoją korzyść — ale tylko jeśli zbudujesz hooki.

Senior IC spędzający 4–8 godzin dziennie z agentami bez hooków traci 30–60 minut dziennie na rzeczy, które hooki załatwiają za darmo: re-uruchamianie formattera, naprawianie kolejności importów, łapanie literówek rm -rf zanim trafią na dysk, ręczne otwieranie PR po każdym ficzerze. Próg “5+ zorkiestrowanych” nie chodzi o liczenie dla efektu — to moment, gdy harness zaczyna usuwać całe kategorie babysittingu z dnia.

Pełną punktację za Q13 dostajesz wyłącznie, gdy wszystkie cztery poniższe są prawdą:

  • Co najmniej 5 hooków aktywnych w cyklu życia agenta. Nie 5 komend w jednym hooku — 5 oddzielnych wpisów odpalających się na różnych eventach (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop lub odpowiednikach Cursor). Realny rozkład, nie jeden przerośnięty PostToolUse.
  • Co najmniej jeden hook jest zorkiestrowany — ustawia stan, który czyta następny. Klasyczny przykład: PostToolUse zapisuje znaczniki “feature complete”; Stop je czyta i jeśli warunki pasują, pushuje branch i otwiera PR. Skok z “hooka” do “stacka hooków.”
  • Co najmniej jeden hook bot/review-loop. Albo auto-PR-on-stop, albo fix-on-review (Stop hook odpytuje otwarty PR o nowe komentarze CodeRabbit/Copilot/człowieka i budzi agenta), albo PostToolUse re-odpalający testy i pingujący na Slacku. Hooki zamieniające agenta w pracownika pracującego, gdy odchodzisz od klawiatury.
  • Co najmniej jeden hook dotyka MCP. beforeMCPExecution w Cursor to oczywisty przypadek, ale ten sam kształt działa w Claude Code — PreToolUse matcher na mcp__* logujący do Honeycomb/Langfuse, wymagający zatwierdzenia dla serwerów wysokiego ryzyka (Stripe, GitHub-write, AWS) albo przepisujący wejście tool.

Cokolwiek mniej — “mam jeden Stop hook, który gra dźwięk” albo “skopiowałem security gate z tutoriala i zapomniałem o nim” — to środkowy tier w Q13.

Powierzchnia hooków Claude Code (oficjalne docs) jest najbogatsza wśród agentowych CLI w 2026. Eventy mają trzy częstotliwości:

  • Raz na sesję: SessionStart, SessionEnd — wstrzykiwanie kontekstu środowiskowego, rozgrzewka cache, logowanie metadanych, dźwięk gdy wracasz do klawiatury.
  • Raz na turę: UserPromptSubmit, Stop, StopFailure, Notification. UserPromptSubmit to jedyny hook mogący zablokować lub wzbogacić prompt, zanim Claude go zobaczy. Stop odpala się przy zakończeniu generowania; może zapobiec zakończeniu i zmusić agenta do dalszej pracy.
  • Raz na wywołanie tool: PreToolUse, PostToolUse, SubagentStop plus eventy MCP. PreToolUse to jedyny hook mogący twardo zablokować tool (exit 2); PostToolUse ma wejście i odpowiedź tool na stdin.

Konfiguracja siedzi w ~/.claude/settings.json (globalna) albo .claude/settings.json (per-projekt, wcommitowana do repo). Realny kształt:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/deny-dangerous.sh"
}
]
},
{
"matcher": "mcp__github__.*",
"hooks": [
{
"type": "command",
"command": "echo \"GitHub MCP: $(jq -r '.tool_name')\" >> ~/.claude/logs/mcp-audit.log"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-and-typecheck.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/auto-pr-watch.sh"
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo \"session=$CLAUDE_SESSION_ID branch=$(git rev-parse --abbrev-ref HEAD)\" >&2"
}
]
}
]
}
}

Pole matcher to regex puszczany na nazwę narzędzia (Bash, Edit, Write, Read, Glob, Grep, Task albo mcp__<server>__<tool> dla MCP). Exit 2 z PreToolUse blokuje tool wraz z feedbackiem do Claude; exit 0 przepuszcza; każdy inny kod jest błędem hooka, który Claude loguje, ale nie blokuje.

Cursor wdrożył hooki pod koniec 2025 z tymi samymi nazwami eventów co Claude Code, plus dwoma własnymi (docs Cursor):

  • beforeShellExecution — odpala się, zanim agent uruchomi jakąkolwiek komendę shell. Matcher leci na stringa komendy. Ustaw failClosed: true, by blokować przy błędzie hooka.
  • beforeMCPExecution — odpala się przed każdym wywołaniem MCP. Hook dostaje komendę serwera, tool_name i tool_input w JSON. Najczystsze miejsce w setupie 2026 do audytu, gatingu lub przepisywania wywołań MCP — szczególnie dla Stripe, GitHub-write, AWS lub wszystkiego, co rusza pieniędzmi.

Konfiguracja Cursor siedzi w .cursor/hooks.json (projekt) lub ~/.cursor/hooks.json (globalna):

{
"hooks": {
"beforeShellExecution": [
{
"matcher": "rm -rf|DROP TABLE|kubectl delete",
"command": "~/.cursor/hooks/deny-dangerous.sh",
"failClosed": true
}
],
"beforeMCPExecution": [
{
"matcher": "stripe.*",
"command": "~/.cursor/hooks/require-approval.sh",
"failClosed": true
}
],
"stop": [
{
"command": "~/.cursor/hooks/auto-pr-watch.sh"
}
]
}
}

Od stycznia 2026 Cursor przeniósł hooki na in-process runner — 10–20× szybciej niż poprzedni model spawnujący shell — można wreszcie odpalać hooki robiące realną robotę (typecheck, lint, format) bez laga przy każdym tool.

Historia hooków w Codex CLI jest najmłodsza z trójki. Nadal wspiera legacy konfigurację notify (oryginalny “desktopowy toast, gdy agent skończy”) — ale na początku 2026 OpenAI wypuścił hooks.json obejmujący pięć eventów: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop. Kształt blisko odzwierciedla Claude Code, siedzi w ~/.codex/hooks.json lub .codex/hooks.json, a Codex przekazuje JSON na stdin / czyta JSON ze stdout. Legacy tablica notify w config.toml nadal działa dla wąskiego “fire-and-forget”:

~/.codex/config.toml
notify = ["bash", "-lc", "terminal-notifier -title 'Codex' -message 'Task complete'"]

Jeśli zaczynasz od zera w 2026, omiń notify i pisz hooks.json od razu. Jeśli już używasz notify, zostaw to w spokoju, ale dołóż nowy system na wierzch — koegzystują bez problemu.

Próg “5+ zorkiestrowanych” łatwo zbić, gdy zaczniesz od pięciu hooków, które zarabiają na siebie od pierwszego dnia:

1. Auto-format + typecheck po każdej edycji. PostToolUse matcher na Edit|Write odpalający formatter (Prettier/Biome/Ruff) i typechecker, oddający błędy do Claude. Agent zapomina o formatterze raz na ~5 edycji; hook usuwa kategorię.

.claude/hooks/format-and-typecheck.sh
#!/usr/bin/env bash
set -e
cd "$CLAUDE_PROJECT_DIR"
npm run format -- --write 2>&1 | tail -20 >&2
npm run type-check 2>&1 | tail -20 >&2

2. Blokowanie niebezpiecznych komend shell. PreToolUse matcher na Bash, który skanuje komendę pod kątem rm -rf /, DROP TABLE, kubectl delete -A, gh repo delete itd. i exituje 2, gdy coś takiego zobaczy.

.claude/hooks/deny-dangerous.sh
#!/usr/bin/env bash
cmd=$(jq -r '.tool_input.command' <<< "$(cat)")
if echo "$cmd" | grep -qE 'rm -rf (/|~|\\$HOME)|DROP TABLE|kubectl delete -A|gh repo delete'; then
echo "blocked: dangerous command pattern: $cmd" >&2
exit 2
fi
exit 0

3. Auto-PR-watch na Stop. Stop hook sprawdzający, czy working tree ma commity ponad origin/<default> i, jeśli tak, pushuje branch i otwiera PR przez gh. Najbardziej dźwigniowy hook w setupie — zamienia “kod napisany” w “kod dostarczony” bez konieczności pamiętania.

4. Fix-on-review na Stop. Ten sam Stop hook, drugi branch: jeśli otwarty PR ma nowe komentarze od ostatniej sygnatury, obudź agenta (przez asyncRewake albo re-prompt). Krok orkiestracji — hook czyta stan, który zapisał wcześniej.

5. Audyt MCP / gate zatwierdzeń. Albo PreToolUse w Claude Code z matcherem mcp__* logującym każde wywołanie, albo beforeMCPExecution w Cursor wymagający zatwierdzenia dla serwerów wysokiego ryzyka (Stripe, AWS, GitHub-write). Nawet czyste logowanie zwraca się przy pierwszym debugowaniu “dlaczego agent wywołał ten tool 80 razy?”.

To pięć hooków. Z auto-PR-watch i fix-on-review dzielącymi stan w ~/.claude/auto-pr-state/<hash>.json masz też “zorkiestrowane”. Dorzuć SessionStart i beforeMCPExecution gate na płatnościach — jesteś ponad progiem.

  1. Wybierz dwa hooki, które zarabiają pierwszego dnia. Auto-format-on-edit i deny-dangerous-bash. Bezdyskusyjne, fail-safe, demonstrują wartość przed inwestycją w orkiestrację. Zwykłe skrypty shell w .claude/hooks/.

  2. Podłącz je w settings.json. Poziom projektu (.claude/settings.json, wcommitowany) dla hooków zespołowych; poziom użytkownika (~/.claude/settings.json) dla osobistych (Slack, dźwięki). Cursor odwzorowuje to z .cursor/hooks.json i ~/.cursor/hooks.json.

  3. Zweryfikuj, że hooki się odpalają. W Claude Code uruchom /hooks, by zobaczyć aktywną konfigurację. Wyzwól ręcznie każdy event: edycja pliku (PostToolUse), nieszkodliwa komenda bash (PreToolUse przepuszcza), rm -rf /tmp/test-bad (PreToolUse blokuje). Czytaj stderr — tak się je debuguje.

  4. Dodaj trzeci hook: auto-PR-watch na Stop. Wyznacz default branch przez gh repo view --json defaultBranchRef --jq .defaultBranchRef.name — nigdy nie hardkoduj main ani master. Hook sprawdza niespchniete commity, tworzy feature branch, jeśli jesteś na default, pushuje, gh pr create. Plik stanu (~/.claude/auto-pr-state/<repo-hash>.json) z ostatnim branch/PR/SHA.

  5. Dodaj czwarty hook: fix-on-review. Ten sam Stop hook, drugi branch: jeśli istnieje PR, odpytuj gh pr view <PR#> --comments, API komentarzy inline i gh pr checks <PR#>. Jeśli coś jest nowsze niż sygnatura, zakończ wiadomością budzącą Claude.

  6. Dodaj piąty hook: audyt MCP lub gate zatwierdzeń. Claude Code: PreToolUse matcher na mcp__* logujący do pliku albo POSTujący do observability. Cursor: beforeMCPExecution gatingujący serwery wysokiego ryzyka (stripe.*, aws.*) za interaktywnym promptem, z failClosed: true.

  7. Przetestuj orkiestrację jawnie. Pełny ficzer end-to-end: kod, commit, Stop hook otwiera PR, bot reviews landują, Stop hook budzi agenta, merge. Pętla powinna działać, gdy patrzysz, nie sterujesz. Napraw bugi — zorkiestrowane hooki padają w zaskakujący sposób pierwsze kilka razy.

  8. Dodaj SessionStart hook do wstrzykiwania kontekstu. Tanie, użyteczne, podbija licznik: zaloguj metadane sesji, branch, git status do stderr, by trafiło do transkryptu. Dwie linie shella, natychmiastowy zwrot.

  9. Udokumentuj stack w CLAUDE.md. Powiedz przyszłemu sobie, co robią hooki, gdzie są, jak je wyłączyć. Nieudokumentowane hooki dostają winę, gdy odpalają się w zaskakujący sposób.

  10. Przeglądaj co kwartał. Hooki gniją. Formatter zmienia flagi, serwer MCP jest deprecjonowany, gh wypuszcza breaking change. Co kwartał odpal każdy hook z known-good inputem.

  • Brak obsługi błędów. Hook padający bez stderr to duch — Claude leci dalej (albo nie) i nie wiesz dlaczego. Każdy hook powinien set -euo pipefail i drukować jednolinijkowe podsumowanie do stderr.
  • Drogie hooki na gorącej ścieżce. PreToolUse na Bash zajmujący 4 sekundy to 4-sekundowy podatek od każdej komendy. Profiluj; >200ms na gorącym evencie przenieś gdzie indziej albo cache’uj.
  • Blokujące hooki bez timeoutu. Hook wiszący na sieci/zatwierdzeniu blokuje całą pętlę. Ustaw timeout (HTTP hooki Claude Code mają timeout; command hooki owiń w timeout 10s). Decyduj per hook: timeout = blok czy przepuść.
  • Nieskończone pętle między hookami. Stop hook re-promptujący agenta, który wyzwala kolejny Stop, to klasyczna pułapka. Bramkuj re-prompty sygnaturą (branch, head_sha, max_comment_id); odpalaj tylko gdy się zmieniła.
  • Hooki wyciekające sekrety. Hooki widzą tool input/output na stdin — w tym sekrety. Nigdy nie echouj payloadu do publicznego logu. Redaktuj znane wzorce (sk-…, ghp_…) najpierw.
  • Project hooki bez graceful degradation. Jeśli zespół wcommitował .claude/settings.json z hookiem wołającym pnpm, każdy bez pnpm ma zepsuty setup. Wykrywaj brakujące zależności i pomijaj z warningiem.
  • Hooki walczące z agentem zamiast mu pomagać. Hook exitujący 2 na każdej edycji src/ to wyłączanie agenta. Hooki kodyfikują reguły prawie zawsze prawdziwe, nie wrażenie, że agentowi dziś nie można ufać.
  • Potrafisz wymienić z pamięci co najmniej 5 hooków aktywnych w Twoim setupie i na którym evencie odpala się każdy z nich.
  • .claude/settings.json (albo ~/.claude/settings.json) ma jawne wpisy na co najmniej 3 różnych eventach — nie 5 hooków upchniętych na PostToolUse.
  • Co najmniej jeden hook czyta stan, który zapisuje inny hook (auto-PR-watch + fix-on-review, albo session-start logger + stop reporter).
  • Masz co najmniej jeden hook dotykający MCP — albo logujący wywołania mcp__* w Claude Code, albo beforeMCPExecution gate w Cursor.
  • Twój auto-PR albo auto-review hook w ciągu ostatnich 7 dni faktycznie otworzył PR albo zaadresował komentarz bota bez Twojej interwencji.
  • Każdy hook ma stderr, który można przeczytać po jego odpaleniu, oraz politykę kodu wyjścia, którą umiesz opisać.
  • Twoje hooki są udokumentowane w CLAUDE.md (co robią, gdzie są, jak je wyłączyć).
  • Możesz wyłączyć dowolny hook w mniej niż 10 sekund (zakomentowanie bloku w settings.json) i faktycznie zrobiłeś to raz, by coś zdebugować.