W poście o konfiguracji wspomniałem o 11 hookach. Od tamtej pory liczba wzrosła. Teraz mam 23 skrypty shell podpięte pod różne zdarzenia Claude Code: blokują wdrożenia produkcyjne, automatycznie formatują TypeScript, wysyłają wiadomości Telegram, gdy Claude potrzebuje mojej uwagi.

Hooki to prawdopodobnie najbardziej niedoceniana część systemu konfiguracji Claude Code. Komendy przyciągają uwagę, bo się je wpisuje. Hooki są niewidoczne, dopóki nie są potrzebne.

Czym jest hook

Hook to skrypt shell uruchamiany w odpowiedzi na zdarzenie Claude Code. Rejestrujesz go w settings.json pod kluczem hooks. Gdy zdarzenie wystąpi, Claude Code uruchamia skrypt i decyduje co dalej na podstawie kodu wyjścia.

Konfiguracja wygląda tak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
"hooks": {
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "bash /home/user/.claude/hooks/block-prod.sh"
        }
      ]
    }
  ]
}

matcher filtruje, które wywołania narzędzi wyzwalają hook. "Bash" łapie wszystkie wywołania Bash. "Write" łapie zapisy do plików. "mcp__" łapie wszystko związane z MCP. Pusty string łapie wszystko.

Kody wyjścia sterują zachowaniem:

  • 0: zezwól na wywołanie narzędzia
  • 2: zablokuj je, a stderr hooka pojawi się w Claude jako powód

Dla hooków PostToolUse blokowanie nie jest możliwe, ale można wstrzyknąć tekst do kontekstu Claude przez stdout.

Skrypty otrzymują pełny input narzędzia jako JSON na stdin. Możesz więc sprawdzić rzeczywiste polecenie, ścieżkę pliku lub zawartość pliku przed podjęciem decyzji.

PreToolUse: strażnicy

Tu dzieje się większość pracy związanej z bezpieczeństwem. Mam hooki na zdarzenia Bash, Write i Edit.

Blokowanie niebezpiecznych poleceń

block-prod.sh wypatruje wzorców wdrożeń produkcyjnych:

1
2
3
4
if echo "$CMD" | grep -qiE '(kubectl|helm|terraform|ansible).*(prod|production).*(--(apply|install|upgrade|delete|destroy))|deploy.*prod|push.*prod'; then
  echo "BLOCKED: Production deployment detected. Please confirm manually." >&2
  exit 2
fi

Nie zablokuje wszystkiego. Nie o to chodzi. To sprawdzenie bezpieczeństwa na wypadek, gdybym powiedział “wdróż usługę auth” i zapomniał podać środowisko. Claude powinien zapytać, nie zakładać.

docker-guard.sh chroni konkretne kontenery infrastrukturalne przed przypadkowym zatrzymaniem. Nazwy są wystarczająco specyficzne, żeby regex był prosty:

1
2
3
4
5
6
7
8
case "$CMD" in
  *"docker stop"*"trigger-"*|*"docker rm"*"trigger-"*|*"docker kill"*"trigger-"*)
    if echo "$CMD" | grep -qv "runner-"; then
      echo "BLOCKED: Cannot stop infrastructure containers." >&2
      exit 2
    fi
    ;;
esac

Efemeryczne kontenery runner są zatrzymywane cały czas. Kontenery infrastrukturalne nie. Wyjątek dla runner- przepuszcza normalną operację.

branch-guard.sh blokuje force push:

1
2
3
4
5
6
case "$CMD" in
  *"git push"*"--force"*|*"git push"*"-f "*)
    echo "BLOCKED: Force push is not allowed." >&2
    exit 2
    ;;
esac

Prosto i bez kompromisów.

Ochrona plików i sekretów

protect-configs.sh blokuje zapisy do /etc/, .ssh/ i plików .env:

1
2
3
4
5
case "$FILE" in
  /etc/*)    echo "BLOCKED: Writing to /etc/ requires manual confirmation." >&2; exit 2 ;;
  */.ssh/*)  echo "BLOCKED: Writing to .ssh/ is not allowed." >&2; exit 2 ;;
  */.env|*/.env.*)  echo "BLOCKED: Writing .env files directly." >&2; exit 2 ;;
esac

Dla niektórych plików tylko ostrzega i kontynuuje. Modyfikacje .mcp.json wypisują ostrzeżenie, ale go nie blokują. Celowo. Ostrzeżenie trafia do kontekstu Claude, więc kontynuuje ze świadomością.

secret-scanner.sh sprawdza zawartość pliku przed zapisem. Skanuje wzorce wyglądające jak prawdziwe klucze API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
PATTERNS=(
  'AKIA[0-9A-Z]{16}'          # AWS Access Key
  'sk-[a-zA-Z0-9]{20,}'       # OpenAI/Stripe secret key
  'ghp_[a-zA-Z0-9]{36}'       # GitHub PAT
  'AIza[0-9A-Za-z\-_]{35}'    # Google API key
)

for pattern in "${PATTERNS[@]}"; do
  if echo "$CONTENT" | grep -qE "$pattern"; then
    echo "BLOCKED: Potential secret detected in $FILE. Use environment variables instead." >&2
    exit 2
  fi
done

Pomija znane bezpieczne pliki jak .mcp.json i settings.json, które legalnie zawierają klucze lub odniesienia do nich.

strategy-validator.sh działa przed wywołaniami narzędzia Skill. Jeśli ktoś poda --strategy z nieprawidłową nazwą, blokuje i wypisuje listę poprawnych opcji. To ważne, bo mam dużo skilli z domyślnymi strategiami wbudowanymi w kod, a literówka mogłaby się cicho ześlizgnąć do złego zachowania.

PostToolUse: po fakcie

Działają po zakończeniu wywołania narzędzia. Brak możliwości blokowania, ale można logować, formatować i wstrzykiwać kontekst.

audit-log.sh dopisuje każde polecenie Bash z timestampem do pliku logu. Prosto:

1
echo "$(date -Iseconds)	$CMD" >> "$AUDIT_LOG"

Kilka tygodni takiego logu jest zaskakująco przydatne, gdy próbujesz sobie przypomnieć, co naprawdę uruchomiłeś we wtorek.

auto-format.sh uruchamia Prettier na każdym pliku TypeScript po edycji. Uratował mnie przed zacommitowaniem nieformatowanego kodu więcej niż raz. Działa cicho i dotyczy tylko plików .ts i .tsx.

mcp-audit.sh loguje wywołania narzędzi MCP osobno. Chcę wiedzieć, jakich zewnętrznych usług używa Claude, szczególnie w sesjach, gdzie pracuję nad automatyzacją.

Hooki sesji: wstrzykiwanie kontekstu

Hooki cyklu życia sesji są najbardziej interesujące. Pozwalają wstrzykiwać informacje do kontekstu Claude w konkretnych momentach.

SessionStart

session-context.sh uruchamia się na początku każdej sesji i wstrzykuje krótki status systemu:

1
2
3
4
5
6
7
8
9
cat <<EOF
SESSION CONTEXT:
- Branch: $BRANCH | Modified files: $MODIFIED
- Disk: $DISK | Memory: $MEM | Load: $LOAD
- Docker: $TRIGGER_STATUS | Healthy: $DOCKER_HEALTHY | Unhealthy: $DOCKER_UNHEALTHY
- Uptime: $UPTIME
- Date: $(date '+%Y-%m-%d %H:%M %Z')
- Audit log: $AUDIT_SIZE entries
EOF

Gałąź git, miejsce na dysku, pamięć, uruchomione kontenery, aktywne workery. Wszystko, czego Claude może potrzebować, żeby dawać mi dokładne odpowiedzi bez podawania tego samego kontekstu na nowo. Nie muszę mówić “jestem na gałęzi feature z 3 zmodyfikowanymi plikami.” Już wie.

UserPromptSubmit

Uruchamia się przed każdą moją wiadomością. Mam tu dwa hooki.

prompt-enhance.sh wypatruje promptów związanych z git i wstrzykuje aktualny stan gałęzi. Jeśli wpiszę “zacommituj to,” już wie, na której gałęzi jestem i ile plików jest staged.

telos-context.sh wypatruje słów kluczowych związanych z karierą lub celami i wstrzykuje przypomnienie, żeby zajrzeć do pliku z celami. To bardziej osobista rzecz. Jeśli wspomnę coś jak “czy powinienem skupić się na tej certyfikacji,” hook odpala i mówi Claude, żeby przeczytał odpowiedni plik pamięci przed odpowiedzią. To sposób na spójność rady z długoterminowym kierunkiem, nie tylko bieżącym pytaniem.

Notification: Telegram kiedy trzeba

Zdarzenie Notification odpala się, gdy Claude potrzebuje ludzkiej decyzji. Domyślnie to dzwonek w terminalu. Mam to ustawione nieco szerzej:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Powiadomienie desktop (zawsze)
notify-send -i dialog-information 'Claude Code' 'Needs your attention' 2>/dev/null || true

# Telegram (pomijaj rutynowe ukończenia)
case "$MESSAGE" in
  *"task"*"completed"*|*"subtask"*|*"agent"*"completed"*)
    exit 0
    ;;
esac

# Wyślij do bota Telegram z credentials z Vault
curl -s -o /dev/null \
  -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
  -d "{\"chat_id\":\"${CHAT_ID}\",\"text\":\"Claude Code: Needs your attention\"}"

Lookup tokena bota z Vault jest trochę ciężki, ale dzięki temu credentials nie leżą w samym pliku hooka. Instrukcja case pomija Telegram dla rutynowych ukończeń podzadań, bo inaczej odpala się co kilka minut podczas długich sesji agentów.

Dostanę ping na Telegram, gdy Claude trafi na prompt uprawnień, potrzebuje decyzji albo skończył zadanie wymagające mojego inputu. Rutynowe ukończenia zostają ciche.

Co w praktyce zostało zablokowane

Większość blokad w pierwszym miesiącu dotyczyła przypadków brzegowych, o których nie pomyślałem. Secret scanner odpalił dwa razy w pierwszym tygodniu, gdy pracowałem na przykładach integracyjnych z ciągami wyglądającymi jak placeholdery. block-prod.sh złapał jedno prawdziwe prawie-nieporozumienie, gdy prosiłem Claude o pomoc z upgradeem Helm bez podania środowiska.

Hooki ochrony plików są głównie prewencyjne. Nigdy nie zablokowały czegoś, czego chciałem. Taki jest cel: niski wskaźnik fałszywych pozytywów, okazjonalne prawdziwe ratowanie.

Hooki powiadomień są naprawdę przydatne. Pracując w terminalu, łatwo przegapić moment, gdy długa sesja agenta potrzebuje uwagi. Ping na Telegram pozwala odejść od komputera i wrócić, gdy jest to istotne.

Struktura hooka

Jeśli piszesz własny, podstawowy szkielet wygląda tak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash
INPUT=$(cat 2>/dev/null)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)

[ -z "$CMD" ] && exit 0  # nic do sprawdzenia

if echo "$CMD" | grep -q "something dangerous"; then
  echo "BLOCKED: reason here" >&2
  exit 2
fi

exit 0

Struktura JSON inputu różni się zależnie od typu hooka. Dla hooków Bash .tool_input.command to string z poleceniem. Dla hooków Write/Edit masz .tool_input.file_path i .tool_input.content lub .tool_input.new_string. Pełna struktura jest w dokumentacji Claude Code.

Warto wiedzieć: hooki działają w kolejności z settings.json. Jeśli masz trzy hooki PreToolUse na Bash, wszystkie trzy się uruchamiają. Pierwsze exit 2 wygrywa.

Czy warto?

Tak, ale zwrot jest nierówny. Hooki bezpieczeństwa opłacają się sporadycznie i działają jak ubezpieczenie. Hooki wstrzykiwania kontekstu opłacają się w każdej sesji, bo startowanie z dokładnym stanem systemu jest po prostu lepsze niż proszenie o niego.

Hooki powiadomień opłacają się proporcjonalnie do tego, jak często uruchamiasz długie sesje. Jeśli używasz Claude Code tylko do szybkich edycji, nie potrzebujesz Telegrama. Jeśli uruchamiasz zadania wielu agentów trwające 10 minut, ping jest przydatny.

Zacznij od protect-configs.sh i secret-scanner.sh. Są tanie w konfiguracji, a koszt braku ich jest credential w historii git. Potem dodaj wstrzykiwanie kontekstu, jeśli odkryjesz, że podajesz ten sam kontekst w każdej sesji. Reszta to optymalizacja.


Powiązane: Moja konfiguracja Claude Code | Własne komendy | Komendy DevOps