W poście #2 i #3 wspomnieliśmy hooks jako jedną z warstw rozszerzeń Claude Code. W poście #5 omówiliśmy skills — teraz czas na hooks.
Czym jest hook
Hook to polecenie shell, które Claude Code wykonuje w odpowiedzi na zdarzenie: przed lub po wywołaniu narzędzia, przy wysyłaniu notyfikacji, przy zatrzymaniu agenta. W odróżnieniu od skills (wywoływanych jawnie), hooks uruchamiają się bez udziału użytkownika.
Prosty przykład: agent skończył implementację i chce zacommitować — ale hook przechwytuje git commit, uruchamia testy i blokuje operację dopóki nie będą zielone. Zamiast liczyć na to, że agent "pamięta" o testach (instrukcja w CLAUDE.md), hook blokuje commit na poziomie runtime. To jest sedno hooków: twarde, automatyczne reguły zamiast miękkich instrukcji behawioralnych (pełny przykład — wzorzec 4).
Typy hooków
graph TD
E[Zdarzenie agentyczne] --> PT{PreToolCall}
E --> N{Notification}
E --> ST{Stop}
PT -->|OK| TC[Wykonanie narzędzia]
PT -->|BLOCKED| X[Zablokowano]
TC --> PO{PostToolCall}
| Typ | Kiedy się uruchamia | Przykład zastosowania |
|---|---|---|
| PreToolCall | Przed wywołaniem narzędzia. Może zablokować wywołanie (exit code ≠ 0); zmodyfikować parametry lub zalogować operację | Blokada git push --force lub rm -rf — agent nie wykona destrukcyjnej operacji; nawet jeśli sam zdecyduje; że chce ją uruchomić |
| PostToolCall | Po wywołaniu narzędzia. Może uruchomić dodatkowe operacje (linter; formatter); zalogować wynik lub wysłać notyfikację | Po każdej edycji pliku .py automatycznie uruchom ruff format — agent pisze kod; hook formatuje; wynik jest zawsze spójny ze stylem projektu |
| Notification | Gdy agent chce zakomunikować coś użytkownikowi | Przekierowanie notyfikacji do Slacka lub powiadomień desktopowych — przydatne gdy agent pracuje w tle; a Ty robisz coś innego |
| Stop | Gdy agent kończy pracę (sukces lub porażka) | Wysłanie wiadomości na Slacka »agent skończył pracę nad repo X« lub zapis podsumowania sesji do loga — wiesz kiedy wrócić do rezultatów |
Konfiguracja hooks w settings.json
Hooks konfiguruje się w settings.json:
- Projektowy:
.claude/settings.json - Globalny:
~/.claude/settings.json
{
"hooks": {
"PreToolCall": [
{
"matcher": "Bash(git commit*)",
"command": "pytest tests/ -x -q || (echo 'BLOCKED: tests failed' && exit 1)"
}
],
"PostToolCall": [
{
"matcher": "Edit(*.go)",
"command": "gofmt -w $CLAUDE_FILE_PATH"
}
],
"Notification": [
{
"command": "notify-send 'Claude Code' \"$CLAUDE_NOTIFICATION\""
}
],
"Stop": [
{
"command": "echo \"$(date '+%H:%M') Agent finished\" >> ~/.claude/agent.log"
}
]
}
}
matcher to wzorzec dopasowania zdarzenia — format NarzędzieName(wzorzec). Brak matchera oznacza, że hook reaguje na każde zdarzenie danego typu. Więcej przykładów dla każdego typu — w sekcji Praktyczne wzorce.
Zmienne środowiskowe w hooks
Hooks mają dostęp do zmiennych opisujących kontekst zdarzenia:
| Zmienna | Wartość |
|---|---|
$CLAUDE_TOOL_NAME | Nazwa narzędzia (np. »Edit«; »Bash«) |
$CLAUDE_FILE_PATH | Ścieżka pliku (dla Edit/Write) |
$CLAUDE_COMMAND | Polecenie (dla Bash) |
$CLAUDE_EXIT_CODE | Kod wyjścia narzędzia (dla PostToolCall) |
$CLAUDE_NOTIFICATION | Treść notyfikacji (dla Notification) |
Praktyczne wzorce hooks
Wzorzec 1: Auto-formatting
Po każdej edycji pliku — uruchom formatter:
{
"PostToolCall": [
{
"matcher": "Edit(*.py)",
"command": "ruff format $CLAUDE_FILE_PATH && ruff check --fix $CLAUDE_FILE_PATH"
},
{
"matcher": "Edit(*.{ts,tsx,js,jsx})",
"command": "npx prettier --write $CLAUDE_FILE_PATH"
},
{
"matcher": "Edit(*.go)",
"command": "gofmt -w $CLAUDE_FILE_PATH"
}
]
}
Agent pisze kod, hook automatycznie formatuje. Agent nie musi znać konfiguracji formattera — hook wymusza spójność niezależnie od tego, co agent wygeneruje.
Wzorzec 2: Guardrails bezpieczeństwa
Blokuj niebezpieczne operacje:
{
"PreToolCall": [
{
"matcher": "Bash(git push --force*)",
"command": "echo 'BLOCKED: force push requires manual approval' && exit 1"
},
{
"matcher": "Bash(git reset --hard*)",
"command": "echo 'BLOCKED: hard reset requires manual approval' && exit 1"
},
{
"matcher": "Bash(docker rm*)",
"command": "echo 'BLOCKED: container removal requires manual approval' && exit 1"
},
{
"matcher": "Edit(.env*)",
"command": "echo 'BLOCKED: editing .env files is forbidden for AI agent' && exit 1"
}
]
}
Nawet w trybie "allow all" agent nie może wykonać destrukcyjnych operacji. Defense in depth — nie polegasz na instrukcji w CLAUDE.md, tylko na twardym bloku runtime.
Wzorzec 3: Audit log
Loguj każde wywołanie narzędzia:
{
"PreToolCall": [
{
"command": "echo \"$(date '+%Y-%m-%d %H:%M:%S') PRE $CLAUDE_TOOL_NAME\" >> ~/.claude/audit.log"
}
],
"PostToolCall": [
{
"command": "echo \"$(date '+%Y-%m-%d %H:%M:%S') POST $CLAUDE_TOOL_NAME exit=$CLAUDE_EXIT_CODE\" >> ~/.claude/audit.log"
}
]
}
Pełna historia operacji agenta. Przydatne do debugowania ("dlaczego agent zrobił X?") i compliance ("co agent modyfikował?").
Wzorzec 4: Testy przed commitem
Zablokuj commit, jeśli testy nie przechodzą:
{
"PreToolCall": [
{
"matcher": "Bash(git commit*)",
"command": "cd $(git rev-parse --show-toplevel) && python -m pytest tests/ -x -q --tb=short 2>&1 | tail -5; test ${PIPESTATUS[0]} -eq 0 || (echo 'BLOCKED: unit tests failed — fix before committing' && exit 1)"
}
]
}
Agent kończy implementację, wywołuje git commit — hook przechwytuje to zdarzenie i uruchamia testy. Jeśli którykolwiek test nie przejdzie, commit jest blokowany. Agent widzi output z pytest, naprawia kod i próbuje ponownie. Commit przejdzie dopiero gdy wszystkie testy będą zielone.
Jeśli w projekcie są zarówno szybkie testy jednostkowe, jak i wolne testy integracyjne, ogranicz hook do szybkiego podzbioru: pytest tests/unit/ -x -q. Pełny suite odpal ręcznie lub w CI.
Wzorzec 5: Integracja z CI/CD
Wyślij notyfikację po zakończeniu pracy agenta:
{
"Stop": [
{
"command": "curl -s -X POST https://hooks.slack.com/services/T.../B.../... -d '{\"text\": \"Claude Code finished working on $(basename $(pwd))\"}'"
}
]
}
Lub triggeruj pipeline CI po pushu:
{
"PostToolCall": [
{
"matcher": "Bash(git push*)",
"command": "gh workflow run ci.yml --ref $(git branch --show-current)"
}
]
}
Skills + Hooks = Custom workflow
Najsilniejsze rozszerzenia łączą skills i hooks. Przykład: workflow "safe deploy":
Skill: /deploy
---
name: deploy
description: Użyj gdy użytkownik prosi o deploy na produkcję
---
# deploy
1. Sprawdź czy branch jest aktualny z main (git fetch && git status)
2. Uruchom pełny test suite: pytest && pnpm run typecheck
3. Zbuduj Docker image: docker/build.sh
4. Wypchnij obraz: docker push ghcr.io/...
5. Zaktualizuj deployment: kubectl set image ...
6. Monitoruj rollout: kubectl rollout status ...
7. Pokaż użytkownikowi status
Hooks: guardrails dla deploy
{
"PreToolCall": [
{
"matcher": "Bash(kubectl set image*)",
"command": "git diff --quiet HEAD main || (echo 'BLOCKED: branch not synced with main' && exit 1)"
}
],
"PostToolCall": [
{
"matcher": "Bash(kubectl rollout status*)",
"command": "curl -s -X POST $SLACK_WEBHOOK -d '{\"text\": \"Deploy complete: $(kubectl get deployment -o jsonpath={.metadata.name})\"}'"
}
]
}
Skill definiuje co robić. Hooks definiują guardrails i automatyzacje. Razem tworzą powtarzalny, bezpieczny workflow.
Permissions — najsilniejsza warstwa kontroli
Oprócz hooks, Claude Code ma system uprawnień w settings.json:
{
"permissions": {
"allow": [
"Bash(npm test)",
"Bash(npm run build)",
"Bash(pytest*)",
"Bash(git status)",
"Bash(git diff*)",
"Bash(git log*)",
"Edit(src/**)",
"Edit(tests/**)",
"Read(**)"
],
"deny": [
"Edit(.env*)",
"Edit(*.pem)",
"Bash(curl*)",
"Bash(wget*)"
]
}
}
allow — operacje dozwolone bez pytania o zgodę. Zmniejsza liczbę approval prompts.
deny — operacje zawsze zablokowane. Silniejsze niż hooks (nie da się obejść).
Trzy warstwy kontroli w Claude Code:
| Warstwa | Mechanizm | Charakterystyka |
|---|---|---|
| 1 | Permissions (deny) | Twarda blokada — nie do obejścia |
| 2 | Hooks (PreToolCall) | Programowalna blokada z logiką |
| 3 | CLAUDE.md | Instrukcja behawioralna (agent może zignorować) |