Hooki Claude Code: zabezpieczenia, bramki i kontekst na żywo
ENHooki Claude Code to procedury obsługi (handlery): polecenia powłoki, endpointy HTTP, narzędzia MCP, prompty lub agenci, które Claude Code uruchamia w stałych zdarzeniach cyklu życia, takich jak PreToolUse, PostToolUse i Stop. Hooki działają niezależnie od tego, czy model przestrzega instrukcji, więc hook PreToolUse może zablokować polecenie, a hook Stop nie pozwoli zakończyć tury, dopóki testy nie przechodzą.
Ta strona jest dla deweloperów i tech leadów, którzy odpowiadają za .claude/settings.json w repozytorium. Problem, który rozwiązuje: twój CLAUDE.md mówi „nigdy nie edytuj migracji” i „uruchom testy przed zakończeniem”, a w zeszłym tygodniu agent i tak zrobił jedno i drugie. Linijka w CLAUDE.md to rada, którą model waży względem całej reszty kontekstu. Hook to kod, który uruchamia się przy każdym pasującym zdarzeniu.
Tabelę zdarzeń i reguły kodów wyjścia na tej stronie sprawdzono w Claude Code v2.1.283 dnia 2026-10-03. Pole onFailure i zmianę w hookach promptowych sprawdzono w v2.1.296 dnia 2026-10-12.
Co daje gotowa do produkcji konfiguracja hooków
Dział zatytułowany „Co daje gotowa do produkcji konfiguracja hooków”- Regułę decyzyjną: kiedy kontrola należy do hooka, kiedy do reguły uprawnień, a kiedy do
CLAUDE.md. - Pięć skryptów: cztery bramki (blokada destrukcyjnych poleceń, ochrona ścieżek, formatowanie i lint każdej edycji, odmowa zakończenia przy czerwonych testach) oraz jeden, który na starcie każdej sesji dodaje kontekst na żywo.
- Kontrakt kodów wyjścia i JSON-a, łącznie z dwiema pułapkami, które po cichu wyłączają bramkę.
- Testy na nagranych danych wejściowych (fixtures), które w CI dowodzą, że każda z czterech bramek działa, więc nikt nie musi recenzować hooków, czytając je.
- Hooki promptowe i hooki w tle dla kontroli, których skrypt nie wyrazi albo które trwają zbyt długo, żeby blokować agenta.
Kiedy reguła powinna być hookiem, a nie linijką w CLAUDE.md?
Dział zatytułowany „Kiedy reguła powinna być hookiem, a nie linijką w CLAUDE.md?”Umieść każdą regułę w najsłabszym mechanizmie, który wciąż ją egzekwuje. Silniejsze mechanizmy kosztują więcej opóźnień i utrzymania, a hooki promptowe kosztują też tokeny.
| Mechanizm | Egzekwowany? | Do czego | Przykład |
|---|---|---|---|
CLAUDE.md | Nie, doradczy | Konwencje i kontekst, które model powinien znać | „Używamy pnpm, nie npm.” |
Reguła uprawnień (permissions.deny) | Tak, stały wzorzec | Stała lista narzędzi, poleceń lub ścieżek | Deny Bash(curl *), deny Edit(.env) |
Hook poleceniowy (command) | Tak, obliczany | Kontrole wymagające logiki, treści pliku lub stanu repozytorium | Lint edytowanego pliku; blokada DROP TABLE w każdej postaci |
| Hook promptowy lub agentowy | Tak, oceniany przez model | Kontrole wymagające osądu, a nie dopasowania wzorca | „Czy końcowa wiadomość podaje polecenie testowe, które uruchomiono?” |
Z tej tabeli wynikają dwie rzeczy. Po pierwsze: jeśli politykę da się wyrazić statyczną regułą, użyj reguły uprawnień. Oficjalna dokumentacja hooków nazywa filtr if w definicji hooka „best-effort” i do twardego allow i deny zaleca system uprawnień. Po drugie: hooki to właściwe miejsce na wszystko, co sprawdza wynik: formatter, linter i zestaw testów. Te kontrole zamieniają „agent mówi, że skończył” w dowód. Mechanizm uprawnień opisuje strona Uprawnienia i sandboxing.
Które zdarzenia hooków w Claude Code mogą blokować?
Dział zatytułowany „Które zdarzenia hooków w Claude Code mogą blokować?”Claude Code wywołuje 33 zdarzenia hooków: te same 33 w v2.1.283 i v2.1.296 (sprawdzone 2026-10-12). Wszystkie wymienia oficjalna dokumentacja hooków. Większość repozytoriów używa tych:
| Zdarzenie | Kiedy się wywołuje | Co robi kod wyjścia 2 |
|---|---|---|
SessionStart | Start, wznowienie, wyczyszczenie lub kompakcja sesji | Nie blokuje. Zwykły tekst na stdout przy kodzie 0 trafia do kontekstu Claude |
UserPromptSubmit | Wysyłasz prompt | Blokuje i usuwa prompt |
PreToolUse | Przed każdym wywołaniem narzędzia | Blokuje wywołanie; stderr trafia do Claude jako powód |
PermissionRequest | Wywołanie narzędzia wymaga decyzji o uprawnieniu | Ignorowany. Odmawiaj przez obiekt decision w JSON-ie |
PostToolUse | Po udanym wywołaniu narzędzia | Nie cofnie wywołania. Pokazuje stderr Claude, który to poprawia |
PostToolBatch | Po paczce równoległych wywołań narzędzi | Zatrzymuje pętlę przed kolejnym wywołaniem modelu |
Stop / SubagentStop | Claude lub subagent kończy odpowiedź | Każe pracować dalej |
TaskCompleted | Zadanie jest oznaczane jako ukończone | Nie pozwala oznaczyć zadania |
PreCompact | Przed kompakcją kontekstu | Blokuje kompakcję |
ConfigChange | Plik ustawień zmienia się w trakcie sesji | Blokuje zmianę (poza ustawieniami zarządzanymi) |
Notification | Claude Code potrzebuje twojej uwagi | Nie blokuje; służy do powiadomień |
Nowsze zdarzenia (PreModelSwitch, FileChanged, WorktreeCreate, Elicitation i inne) rozwiązują węższe problemy. Zanim na którymś zbudujesz, sprawdź dokumentację.
Jak hook odpowiada Claude Code?
Dział zatytułowany „Jak hook odpowiada Claude Code?”Hook poleceniowy dostaje zdarzenie jako JSON na stdin i odpowiada kodem wyjścia oraz stdout.
- Kod 0 oznacza sukces. W większości zdarzeń stdout trafia tylko do logu debugowania. W
SessionStart,UserPromptSubmit,UserPromptExpansioniPostModelSwitchzwykły tekst ze stdout staje się kontekstem, który Claude widzi. Jeśli stdout zaczyna się od{i kończy na}, Claude Code parsuje go jako JSON. - Kod 2 blokuje w zdarzeniach, które mogą blokować, a stderr staje się powodem. Żaden JSON na stdout nie uchyli blokady wywołanej kodem 2.
- Każdy inny kod, łącznie z 1, nie blokuje, chyba że decyzję podejmuje poprawny JSON na stdout. Claude Code pokazuje komunikat „hook error” i akcja idzie dalej. Hook polityki, który kończy się
exit 1, nie jest bramką. Wyjątkiem jestWorktreeCreate: każdy niezerowy kod przerywa tworzenie worktree. Żeby zawodzący hook poleceniowy lub HTTP blokował, ustawonFailure(następna sekcja).
JSON daje dokładniejszą kontrolę niż kod wyjścia. PreToolUse odpowiada wewnątrz hookSpecificOutput polem permissionDecision o wartości allow, deny, ask lub defer (defer działa tylko w claude -p, w aplikacjach, które wstrzymują i wznawiają sesję; sesje interaktywne go ignorują). Gdy kilka hooków się nie zgadza, pierwszeństwo ma deny > defer > ask > allow. PostToolUse, Stop i SubagentStop używają pól najwyższego poziomu {"decision": "block", "reason": "..."}. Każde zdarzenie, które wstrzykuje kontekst, używa hookSpecificOutput.additionalContext. Ciąg dłuższy niż 10 000 znaków trafia do pliku, a Claude dostaje ścieżkę i podgląd pierwszych 2000 znaków, więc wstrzykiwany kontekst trzymaj krótki.
Spraw, by zawodzący hook blokował akcję
Dział zatytułowany „Spraw, by zawodzący hook blokował akcję”Domyślnie hook, który zawodzi, nadal przepuszcza akcję. Od v2.1.295 (na 2026-10-12 tylko kanał latest) ustaw "onFailure": "block" w hooku command lub http, żeby zawodził w trybie fail closed; wartością domyślną jest "continue". Za porażkę liczy się każde z tych zdarzeń:
- hook nie może wystartować, na przykład brakuje skryptu;
- kończy się kodem innym niż 0 i 2, nawet jeśli wypisał JSON z
allow, więc decyzje w JSON-ie zwracaj z kodem 0; - połączenie hooka HTTP się nie udaje albo odpowiedź ma status inny niż 2xx;
- hook osiąga swój
timeout; - jego wyjściowy JSON nie daje się sparsować albo nie przechodzi walidacji schematu.
Porażka robi wtedy to, co kod 2 w danym zdarzeniu: porażka PreToolUse blokuje wywołanie narzędzia, porażka UserPromptSubmit blokuje prompt, a porażka PermissionRequest odrzuca żądanie.
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "node", "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js"], "onFailure": "block" } ] } ] }}Żeby to sprawdzić, zmień nazwę check-command.js i poproś Claude o ls: wywołanie zostanie zablokowane, a błąd zawiera failed; blocking because onFailure is "block". Pole nie działa w hookach Stop, SubagentStop, TaskCompleted i TeammateIdle, gdzie kod 2 odsyła Claude do pracy, ani w hookach poleceniowych działających w tle (async lub asyncRewake). Źródło: dokumentacja hooków Claude Code, sprawdzona 2026-10-12.
Gdzie definiuje się hooki i jak działa matcher?
Dział zatytułowany „Gdzie definiuje się hooki i jak działa matcher?”Hooki ze wszystkich lokalizacji się sumują. Żadna nie zastępuje innej.
| Lokalizacja | Zasięg | Wspólna dla zespołu? |
|---|---|---|
~/.claude/settings.json | Wszystkie twoje projekty | Nie |
.claude/settings.json | Ten projekt | Tak, commituj go |
.claude/settings.local.json | Ten projekt, tylko ty | Nie, w gitignore |
| Ustawienia zarządzane (managed policy) | Cała organizacja | Tak, ustawiają je administratorzy |
Plugin hooks/hooks.json | Dopóki plugin jest włączony | Tak, razem z pluginem |
| Frontmatter skilla lub subagenta | Dopóki skill lub subagent jest aktywny | Tak, w pliku |
Pole matcher nie zawsze jest wyrażeniem regularnym. Matcher złożony z liter, cyfr, _, -, spacji, przecinków i | to dopasowanie dokładne: Bash pasuje tylko do narzędzia Bash, a Edit|Write dokładnie do tych dwóch. Każdy inny znak zamienia go w niezakotwiczone wyrażenie regularne JavaScriptu, więc Edit.* pasuje też do NotebookEdit. Żeby objąć wszystkie narzędzia serwera MCP, napisz mcp__github__.*. Samo mcp__github to dokładny ciąg, który nie pasuje do niczego.
Żeby filtrować po argumentach, a nie po nazwie narzędzia, dodaj do definicji hooka pole if w składni reguł uprawnień, na przykład "if": "Bash(git push *)" albo "if": "Edit(*.sql)". Oszczędzasz w ten sposób uruchomienia procesu przy każdym innym wywołaniu.
Odwołuj się do skryptów przez ${CLAUDE_PROJECT_DIR}, żeby ścieżka działała także po tym, jak Claude wykona cd. Z ustawionym "args": [] polecenie uruchamia się w formie exec, bez powłoki, i ścieżka nie wymaga cudzysłowów.
Zbuduj pięć hooków, których potrzebuje większość repozytoriów
Dział zatytułowany „Zbuduj pięć hooków, których potrzebuje większość repozytoriów”Zapisz każdy skrypt w .claude/hooks/, nadaj mu chmod +x i zacommituj. Każdy skrypt poza session-context.sh wymaga jq.
Blokuj destrukcyjne polecenia powłoki
Dział zatytułowany „Blokuj destrukcyjne polecenia powłoki”#!/usr/bin/env bash# .claude/hooks/block-destructive.sh (PreToolUse, matcher "Bash")set -euo pipefail# Fail closed: without jq this gate cannot read the command, so block.command -v jq >/dev/null || { echo "jq is missing; install it before running Bash." >&2; exit 2; }cmd=$(jq -r '.tool_input.command // empty')
deny() { jq -nc --arg r "$1" '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: $r}}' exit 0}
shopt -s nocasematchrm_rf='(^|[;&|[:space:]])rm[[:space:]]+-[a-z]*(rf|fr)'sql_drop='(drop|truncate)[[:space:]]+(table|database|schema)'force_push='git[[:space:]]+push.*(--force|[[:space:]]-f([[:space:]]|$))'
[[ $cmd =~ $rm_rf ]] && deny "rm -rf is blocked. Delete the specific files by path."[[ $cmd =~ $sql_drop ]] && deny "Destructive SQL is blocked. Write a migration and stop for review."[[ $cmd =~ $force_push ]] && deny "Force push is blocked. Push a new commit instead."exit 0Powód odmowy trafia do Claude, więc pisz go jako następną instrukcję, a nie jako zarzut. Claude czyta „Delete the specific files by path” i robi dokładnie to.
Traktuj ten hook jak próg zwalniający, a nie jak granicę. rm -r -f, rm --recursive --force i find . -delete przechodzą przez te wzorce. Dopasowanie wzorców łapie typowe zapisy, ale nie wszystkie, więc wszystko, co nigdy nie może się wykonać, zablokuj dodatkowo regułami permissions.deny i sandboxem.
Chroń ścieżki, których agent nie może edytować
Dział zatytułowany „Chroń ścieżki, których agent nie może edytować”Zwykle są to migracje, sekrety i testy akceptacyjne. Testy akceptacyjne są najważniejsze: agent, który może edytować test, sprawi, że przejdzie każda zmiana. Pełny wzorzec opisuje ochrona wyroczni.
#!/usr/bin/env bash# .claude/hooks/protect-paths.sh (PreToolUse, matcher "Edit|Write")set -euo pipefailcommand -v jq >/dev/null || { echo "jq is missing; install it before editing files." >&2; exit 2; }file=$(jq -r '.tool_input.file_path // empty')rel=${file#"$CLAUDE_PROJECT_DIR"/}
case "$rel" in db/migrations/*|.env|.env.*|package-lock.json|tests/acceptance/*) echo "Protected path: $rel. Describe the change you need in your reply; a human applies it." >&2 exit 2 ;;esacexit 0Formatuj i lintuj każdą edycję
Dział zatytułowany „Formatuj i lintuj każdą edycję”Hook PostToolUse nie cofnie edycji, ale kod 2 pokazuje Claude wynik lintera, a Claude poprawia go w następnym kroku.
#!/usr/bin/env bash# .claude/hooks/format-and-lint.sh (PostToolUse, matcher "Edit|Write")set -uo pipefailfile=$(jq -r '.tool_input.file_path // empty')[[ -f $file ]] || exit 0bin=$CLAUDE_PROJECT_DIR/node_modules/.bin
case "$file" in *.ts|*.tsx|*.js|*.jsx) "$bin/prettier" --write "$file" >/dev/null 2>&1 if ! out=$("$bin/eslint" --max-warnings=0 "$file" 2>&1); then echo "ESLint failed on $file. Fix these before moving on:" >&2 head -n 40 <<<"$out" >&2 exit 2 fi ;;esacexit 0Nie pozwól zakończyć pracy przy czerwonych testach
Dział zatytułowany „Nie pozwól zakończyć pracy przy czerwonych testach”To ten hook zamienia „gotowe” w twierdzenie poparte dowodem. Uruchamia się tylko wtedy, gdy drzewo robocze ma niezacommitowane zmiany, blokuje pierwsze zakończenie przy czerwonych testach i przekazuje sprawę człowiekowi, jeśli ponowna próba też się nie uda.
#!/usr/bin/env bash# .claude/hooks/tests-before-stop.sh (Stop)set -uo pipefailretrying=$(jq -r '.stop_hook_active')
# Nothing changed in the working tree: nothing to verify.[[ -z $(git status --porcelain) ]] && exit 0
if out=$(npm test --silent 2>&1); then exit 0; fitail_out=$(tail -n 40 <<<"$out")
if [[ $retrying == "true" ]]; then jq -nc '{systemMessage: "Tests still fail after one retry. Stopping for a human."}' exit 0fijq -nc --arg r "The test suite fails. Fix it before you finish. Last 40 lines:$tail_out" '{decision: "block", reason: $r}'stop_hook_active ma wartość true, gdy Claude już kontynuuje pracę z powodu hooka stop. Claude Code ogranicza też hooki stop do ośmiu kolejnych kontynuacji (limit podniesiesz zmienną CLAUDE_CODE_STOP_HOOK_BLOCK_CAP), ale jedna ponowna próba i eskalacja są tańsze i szybciej mówią ci, że potrzebny jest człowiek.
Daj każdej sesji kontekst na żywo
Dział zatytułowany „Daj każdej sesji kontekst na żywo”Stdout z SessionStart staje się kontekstem. Używaj go do faktów, które się zmieniają, a stałe konwencje trzymaj w CLAUDE.md.
#!/usr/bin/env bash# .claude/hooks/session-context.sh (SessionStart, matcher "startup|resume|compact")branch=$(git branch --show-current)echo "Current branch: $branch"echo "Uncommitted files: $(git status --porcelain | wc -l | tr -d ' ')"res=$(gh run list --branch "$branch" --limit 1 --json conclusion \ -q '.[0].conclusion' 2>/dev/null)echo "Last CI run on this branch: ${res:-unknown}"compact w matcherze wstrzykuje kontekst ponownie po kompakcji, gdy pierwsza kopia została już streszczona. Pisz go jako stwierdzenia faktów. Tekst sformułowany jak polecenia systemowe spoza rozmowy może uruchomić w Claude obronę przed prompt injection.
Zarejestruj wszystkie pięć w .claude/settings.json
Dział zatytułowany „Zarejestruj wszystkie pięć w .claude/settings.json”{ "hooks": { "SessionStart": [ { "matcher": "startup|resume|compact", "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-context.sh", "args": [] }] } ], "PreToolUse": [ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh", "args": [] }] }, { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-paths.sh", "args": [] }] } ], "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format-and-lint.sh", "args": [], "timeout": 60 }] } ], "Stop": [ { "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/tests-before-stop.sh", "args": [], "timeout": 300 }] } ] }}Stop nie obsługuje matchera (sprawdzone w v2.1.283), więc nie ma pola matcher. Otwórz /hooks w sesji, żeby sprawdzić, czy każdy hook się załadował i z którego pliku ustawień pochodzi. Menu jest tylko do odczytu; hooki zmieniasz, edytując JSON.
Udowodnij, że każdy hook działa, bez czytania go
Dział zatytułowany „Udowodnij, że każdy hook działa, bez czytania go”Hook to kod, który pilnuje innego kodu, więc potrzebuje własnych testów. Nikt nie powinien zatwierdzać zmiany hooka, czytając wyrażenie regularne. Przepuść nagrany JSON zdarzenia przez każdy skrypt i sprawdź wynik. Przypadki dla lintera i bramki Stop podstawiają atrapy eslint i npm przed prawdziwe programy, więc zestaw działa w kilka sekund i testuje hook, a nie twój linter. Przypadek rm -r -f celowo zapisuje znaną lukę: jeśli rozszerzysz regex, zmień oczekiwany wynik na deny.
#!/usr/bin/env bash# .claude/hooks/test-hooks.sh: run in CI beside the unit testsset -uexport CLAUDE_PROJECT_DIR=$PWDH=$CLAUDE_PROJECT_DIR/.claude/hooks # the same path the settings file usestmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXITfail=0
decision() { local out; out=$(printf '%s' "$2" | "$H/$1" 2>/dev/null) [[ -n $out ]] || out='{}' jq -r '.hookSpecificOutput.permissionDecision // "none"' <<<"$out"}exit_code() { printf '%s' "$2" | "$H/$1" >/dev/null 2>&1; echo $?; }expect() { [[ $2 == "$3" ]] || { echo "FAIL: $1 (got $2, want $3)"; fail=1; }; }stub() { printf '#!/bin/sh\necho stub output\nexit %s\n' "$2" >"$1"; chmod +x "$1"; }
bash_call() { jq -nc --arg c "$1" '{tool_name: "Bash", tool_input: {command: $c}}'; }edit_call() { jq -nc --arg f "$1" '{tool_name: "Edit", tool_input: {file_path: $f}}'; }
# PreToolUse: block-destructive.shexpect "rm -rf is denied" "$(decision block-destructive.sh "$(bash_call 'rm -rf dist')")" denyexpect "force push is denied" "$(decision block-destructive.sh "$(bash_call 'git push -f origin')")" denyexpect "npm test is allowed" "$(decision block-destructive.sh "$(bash_call 'npm test')")" none# Known gap, recorded on purpose: split flags slip past the pattern.expect "rm -r -f is NOT caught" "$(decision block-destructive.sh "$(bash_call 'rm -r -f dist')")" none
# PreToolUse: protect-paths.shexpect "migration edit blocked" "$(exit_code protect-paths.sh "$(edit_call "$PWD/db/migrations/007.sql")")" 2expect "source edit allowed" "$(exit_code protect-paths.sh "$(edit_call "$PWD/src/app.ts")")" 0
# PostToolUse: format-and-lint.sh against a stub ESLint that reports an errorlint=$tmp/lint; mkdir -p "$lint/node_modules/.bin"; : >"$lint/app.ts"stub "$lint/node_modules/.bin/prettier" 0stub "$lint/node_modules/.bin/eslint" 1expect "lint error exits 2" \ "$(CLAUDE_PROJECT_DIR=$lint exit_code format-and-lint.sh "$(edit_call "$lint/app.ts")")" 2
# Stop: tests-before-stop.sh in a dirty repository where `npm test` failsrepo=$tmp/repo; mkdir -p "$repo" "$tmp/bin"; git -C "$repo" init -q; : >"$repo/new.txt"stub "$tmp/bin/npm" 1stop() { (cd "$repo" && printf '{"stop_hook_active": %s}' "$1" | PATH=$tmp/bin:$PATH "$H/tests-before-stop.sh" 2>/dev/null); }expect "red tests block the stop" "$(stop false | jq -r '.decision // "none"')" blockexpect "retry hands over to a human" "$(stop true | jq -r 'has("systemMessage")')" true
exit $failPotem weryfikuj w trzech warstwach, od najtańszej do najbardziej realistycznej:
-
Przypadki testowe w CI. Uruchamiaj
.claude/hooks/test-hooks.shw tym samym jobie co testy jednostkowe. Zmiana hooka bez nowego przypadku testowego domyślnie nie przechodzi code review. Recenzent czyta listę przypadków, a nie regex. -
Jeden test dymny na żywo dla każdego hooka. Uruchom
claude --debug hooksi poproś Claude orm -rf dist. Powód odmowy powinien pojawić się w transkrypcie. Log debugowania trafia do~/.claude/debug/<session-id>.txt, a nie do terminala;--debug-file <path>zapisuje go tam, gdzie wskażesz. -
Obserwuj pierwsze prawdziwe uruchomienie. Wypatruj komunikatu „hook error” przy pierwszym wywołaniu każdego hooka. To jedyny ślad literówki w ścieżce skryptu, co opisuje sekcja o awariach.
Kto zatwierdza: traktuj .claude/settings.json i .claude/hooks/ jak konfigurację produkcyjną. Dodaj je do CODEOWNERS, żeby każdą zmianę zatwierdzał tech lead. Wdrożenie w całej organizacji opisuje zarządzanie wspólnymi hookami.
Hooki promptowe i agentowe, gdy skrypt nie potrafi zdecydować
Dział zatytułowany „Hooki promptowe i agentowe, gdy skrypt nie potrafi zdecydować”Hook promptowy ("type": "prompt") wysyła JSON zdarzenia i twój prompt do szybkiego modelu, domyślnie Haiku, który odpowiada {"ok": true} albo {"ok": false, "reason": "..."}. W Stop odpowiedź ok: false wraca do Claude jako powód i tura trwa dalej. Ten hook wyłapuje komunikaty „wszystko gotowe”, za którymi nie stoi żaden dowód:
{ "hooks": { "Stop": [ { "hooks": [ { "type": "prompt", "prompt": "Here is the hook input: $ARGUMENTS. Read last_assistant_message. If it claims that tests pass, a bug is fixed or work is complete, it must name the command that was run and quote its result. If it does, respond {\"ok\": true}. If it does not, respond {\"ok\": false, \"reason\": \"Run the verification command and report its output before finishing.\"}. If no such claim is made, respond {\"ok\": true}.", "timeout": 30 } ] } ] }}Jeśli łączysz go z tests-before-stop.sh, pamiętaj, że stop_hook_active ma wartość true po kontynuacji tury przez którykolwiek z hooków; jeśli potrzebujesz osobnych ponownych prób dla każdego hooka, licz je w pliku tymczasowym kluczowanym przez session_id.
Od v2.1.294 (na 2026-10-12 tylko kanał latest) Claude Code lepiej ocenia hook promptowy Stop lub SubagentStop napisany jako polecenie, na przykład „Carry on if the build is broken”, więc Claude rzadziej kończy pracę za wcześnie. Prompt opisujący sprawdzalny warunek, jak ten powyżej, nadal daje najczytelniejszy werdykt do testowania.
Każdy hook promptowy to dodatkowe wywołanie modelu przy każdym pasującym zdarzeniu, więc trzymaj je na Stop i SubagentStop, a nie na każdym wywołaniu narzędzia. Hook agentowy ("type": "agent") uruchamia subagenta, który przez maksymalnie 50 tur może używać Read, Grep i Glob, zanim podejmie decyzję. Hooki agentowe są eksperymentalne, więc nie opieraj na nich bramek, na które liczysz. Wbudowane polecenie /goal to promptowy hook Stop ograniczony do sesji; zajrzyj do przebiegów sterowanych celem, jeśli chcesz tej samej pętli bez pisania konfiguracji.
Uruchamiaj wolne kontrole w tle
Dział zatytułowany „Uruchamiaj wolne kontrole w tle”Hook synchroniczny dodaje swój czas działania do każdego wywołania narzędzia, do którego pasuje. Sprawdzanie typów albo wolny plik testów uruchom w tle:
"async": trueuruchamia polecenie w tle i przekazuje Claude jegoadditionalContextlubsystemMessagew następnej turze. Niczego nie może zablokować."asyncRewake": trueteż działa w tle, ale przy kodzie 2 od razu budzi Claude, przekazując stderr hooka, nawet gdy sesja jest bezczynna. Pasuje do sprawdzania typów w tle, które przy błędzie musi natychmiast przerwać agentowi pracę.
W przebiegu claude -p Claude Code zabija przy zamykaniu każdy hook asynchroniczny, który jeszcze działa. W CI zostaw kontrolę synchroniczną albo uruchom ją jako osobny krok joba.
Czy hooki działają w CI i w niezaufanych repozytoriach?
Dział zatytułowany „Czy hooki działają w CI i w niezaufanych repozytoriach?”Hooki poleceniowe działają z pełnymi uprawnieniami twojego użytkownika. Wynikają z tego dwie reguły:
- Sesje interaktywne wstrzymują każdy hook z plików ustawień, dopóki nie zaakceptujesz okna zaufania (workspace trust) dla katalogu.
- Sesje
claude -pi Agent SDK nigdy nie pokazują tego okna, a mimo to od razu uruchamiają hooki z plików ustawień repozytorium (jego reguły allow pozostają wyłączone).
Dlatego nigdy nie uruchamiaj claude -p na kodzie, którego nie napisałeś, gdy w środowisku jest sekret. Dotyczy to pull requestów z forków, workflowów pull_request_target i repozytoriów, które dopiero oceniasz. Takie przebiegi startuj z --bare --setting-sources "" --strict-mcp-config; samo --settings '{"disableAllHooks": true}' nie wystarczy, bo serwery z .mcp.json repozytorium i tak wystartują. Zaufane skrypty pobieraj z gałęzi domyślnej, a niezaufany checkout trzymaj w katalogu, który agent tylko czyta. Pełny wzorzec opisują Claude Code w CI/CD i model zagrożeń dla agentów. Organizacja może ustawić allowManagedHooksOnly w ustawieniach zarządzanych, żeby działały tylko hooki zatwierdzone przez administratorów.
Kiedy hooki Claude Code zawodzą i jak z tego wyjść
Dział zatytułowany „Kiedy hooki Claude Code zawodzą i jak z tego wyjść”Bramka kończy się kodem 1 i nic nie jest blokowane. Blokuje tylko kod 2 albo deny w JSON-ie. Naprawa: przeszukaj skrypty pod kątem exit 1 i upewnij się, że błąd set -e nie może wystąpić na ścieżce odmowy. Brak jq kończy się kodem 127 i przepuszcza wywołanie, dlatego oba skrypty PreToolUse powyżej najpierw sprawdzają jq i bez niego kończą się kodem 2.
Literówka w ścieżce po cichu wyłącza bramkę. Skrypt z błędną ścieżką w ogóle się nie uruchamia: w formie powłoki kończy się kodem 127, w formie exec błędem uruchomienia procesu. Claude Code traktuje jedno i drugie jako błąd nieblokujący, pokazuje komunikat „hook error” i wywołanie narzędzia idzie dalej. Naprawa: używaj ${CLAUDE_PROJECT_DIR} w formie exec, a w przypadkach testowych wywołuj skrypty tą samą ścieżką, której używa plik ustawień. Od v2.1.295 (na 2026-10-12 tylko kanał latest) ustaw też "onFailure": "block", żeby skrypt, który nie może wystartować, blokował wywołanie zamiast je przepuszczać.
JSON nie ma żadnego efektu. Coś wypisało tekst przed JSON-em, często profil powłoki albo gadatliwe narzędzie, więc stdout nie zaczyna się już od {. Naprawa: wszystko poza końcowym JSON-em kieruj na stderr albo do /dev/null, a w logu debugowania szukaj „treating as plain text”.
Hook PreToolUse przekracza timeout, a wywołanie i tak się wykonuje. Hook poleceniowy po timeoucie nie zwraca decyzji, więc wywołanie przechodzi zwykłą ścieżką uprawnień. Naprawa: hooki PreToolUse mają być szybkie i lokalne, bez wywołań sieciowych, a te krytyczne wspieraj regułą deny w uprawnieniach. Od v2.1.295 (na 2026-10-12 tylko kanał latest) "onFailure": "block" w hooku poleceniowym lub HTTP blokuje wywołanie, gdy hook przekroczy timeout.
Ochrona ścieżek ma dziurę w kształcie Basha. Zablokowanie Edit i Write na db/migrations/ nie powstrzyma sed -i ani cat > przez Bash. Naprawa: dodaj odpowiadającą regułę permissions.deny i pracuj w sandboksie, żeby zapisy poza dozwolonymi ścieżkami kończyły się błędem na poziomie systemu.
Hook Stop się zapętla albo spowalnia każdą turę. Pełny zestaw testów przy każdym zakończeniu może trwać minuty. Naprawa: pomijaj czyste drzewo, uruchamiaj tylko testy powiązane ze zmienionymi plikami i przy ponownej próbie eskaluj do człowieka, tak jak robi to skrypt powyżej.
Wstrzyknięty kontekst jest nieaktualny po wznowieniu. Gdy wznawiasz sesję przez --continue lub --resume, Claude Code odtwarza tekst z hooków ze środka sesji zamiast uruchamiać je ponownie. Naprawa: wszystko, co zależy od czasu, umieść w hooku SessionStart, który przy wznowieniu uruchamia się znowu.
Musisz natychmiast wyłączyć hooki. Ustaw "disableAllHooks": true w ustawieniach albo uruchom pojedynczy przebieg przez claude --settings '{"disableAllHooks": true}'. Żadna z tych opcji nie wyłącza hooków zarządzanych, chyba że ustawienie pochodzi z ustawień zarządzanych.
Dokąd dalej z hookami Claude Code
Dział zatytułowany „Dokąd dalej z hookami Claude Code”- Wymaganie wstępne: konfiguracja Claude Code opisuje pliki ustawień i ich pierwszeństwo, na których opierają się hooki.
- Następny krok: skryptowanie Claude Code uruchamia te bramki w zadaniach headless i zaplanowanych.
- Własne subagenty mogą mieć własne hooki we frontmatterze, działające tylko w czasie pracy subagenta.
- Czytanie dowodów zamiast kodu wyjaśnia, dlaczego bramki testów, lintera i zakończenia zastępują przeglądanie kodu linijka po linijce.
- Audyty bezpieczeństwa z Claude Code rozszerzają listę destrukcyjnych poleceń do pełnego przeglądu.