Blog
PLEN

Hooks w Claude Code — jak wymuszam dyscyplinę na agencie

Hooks to mały kawałek konfiguracji który zamienia Claude Code w narzędzie z prawdziwymi gardami. Pokazuję moje setupy: blokowanie destruktywnych komend, wymuszanie commit policy, telemetria do Telegrama.

·4 min read
Hooks w Claude Code — jak wymuszam dyscyplinę na agencie

Claude Code z hookami to nie ten sam Claude Code. Hooks to skrypty które uruchamiają się na konkretnych zdarzeniach, przed użyciem narzędzia, po użyciu, na zakończenie sesji, na każdy prompt. Nie są frameworkiem. Są punktem integracji który odróżnia "agent w terminalu" od "agent z dyscypliną".

Po trzech miesiącach mam ~10 aktywnych hooków. Pokażę cztery które dają największy zwrot.

Hook #1: blokowanie destruktywnych komend

PreToolUse na tool Bash. Jeśli komenda zawiera określone wzorce, odmawiamy.

#!/usr/bin/env bash
# .claude/hooks/block-destructive.sh
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')
 
forbidden=(
  "rm -rf /"
  "git push --force"
  "DROP DATABASE"
  ":(){ :|:& };:"
)
 
for pattern in "${forbidden[@]}"; do
  if [[ "$cmd" == *"$pattern"* ]]; then
    echo '{"decision":"block","reason":"Destructive command blocked"}'
    exit 0
  fi
done
 
echo '{"decision":"approve"}'

Drobiazg w setttings:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "command": ".claude/hooks/block-destructive.sh" }]
    }]
  }
}

Co to daje: agent może próbować, hook blokuje. Nie polegam na tym że "model nigdy nie zrobi rm -rf". Polegam na tym że gdyby spróbował, hook go zatrzyma.

Hook #2: commit policy — zawsze, bez pytania

Mam preferencję: po każdej zmianie kodu Claude robi commit. Nie pyta, nie czeka. To jest zapisane w mojej memory, ale memory to soft constraint. Hook to hard constraint.

Stop hook który sprawdza czy są niezacommitowane zmiany i przypomina:

#!/usr/bin/env bash
# .claude/hooks/commit-reminder.sh
cd "$(jq -r '.cwd' <<< "$(cat)")" || exit 0
 
if git diff --quiet && git diff --cached --quiet; then
  exit 0  # czysto
fi
 
cat <<EOF
{
  "decision": "block",
  "reason": "Niezacommitowane zmiany. Zacommituj przed zakończeniem sesji."
}
EOF

Efekt: jeśli agent kończy z brudnym tree, dostaje feedback "wróć i zacommituj". To eliminuje 90% sytuacji w których po sesji znajduję uncommited zmiany.

Hook #3: telemetria do Telegrama

Każde użycie narzędzia loguje się jako event. Najciekawsze dla mnie: ile tool calli per sesja, jaki rozkład, ile czasu zajęło.

# .claude/hooks/telemetry.sh
event=$(cat)
tool=$(jq -r '.tool_name' <<< "$event")
when=$(date -Iseconds)
 
echo "$when $tool" >> ~/logs/claude-tools.log
 
# Co 100 calli — pingnij Telegram
count=$(wc -l < ~/logs/claude-tools.log)
if (( count % 100 == 0 )); then
  curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
    -d "chat_id=${ADMIN_CHAT}" \
    -d "text=Claude przekroczył ${count} tool calli dzisiaj 🔧"
fi

Po co to? Wcześniej nie miałem intuicji ile agent realnie wykonuje. Teraz widzę: typowa sesja produktywna to ~80 calli, sesja "model się zapętla" to 200+. Mogę reagować.

Hook #4: walidacja przed pushem

PreToolUse na bash z matcherem git push. Sprawdza:

  • czy branch nie jest main przy force pushu
  • czy testy przechodzą lokalnie
  • czy nie ma plików .env* w stage'u
#!/usr/bin/env bash
input=$(cat)
cmd=$(jq -r '.tool_input.command' <<< "$input")
 
# Force push do main = ZAWSZE blok
if [[ "$cmd" == *"push --force"* && "$cmd" == *"main"* ]]; then
  echo '{"decision":"block","reason":"Force push to main is forbidden"}'
  exit 0
fi
 
# Sekrety w stage
if git diff --cached --name-only 2>/dev/null | grep -qE '\.env'; then
  echo '{"decision":"block","reason":"Staged .env file detected"}'
  exit 0
fi
 
echo '{"decision":"approve"}'

Hook nie testuje wszystkiego. Testuje tylko klasyczne wpadki które mnie kiedyś kosztowały. Jeden hook, trzy reguły, każda warta swojego linijkowego kosztu.

Czego NIE wkładam w hooki

Trzy rzeczy uczyłem się nie wpychać:

1. Logiki biznesowej. Hook ma być filtrem albo telemetrią. Jeśli zaczynam pisać if/else z 5 warunkami biznesowymi, to znak że to powinien być osobny skrypt który agent wywołuje świadomie.

2. Czegoś co wymaga dużej latencji. Hook blokuje narzędzie. Jeśli hook trwa 5 sekund, agent czeka. Mam regułę: hook < 200ms. Większe rzeczy (np. test suite przed pushem) odpalam jako rekomendację a nie blok.

3. Reguł które zmieniam co tydzień. Każdy hook to mała umowa z agentem. Zmieniana często, mylimy się oboje. Memory + CLAUDE.md są lepsze do soft preferences.

Setup w pigułce

.claude/
├── settings.json          # rejestr hooków
└── hooks/
    ├── block-destructive.sh
    ├── commit-reminder.sh
    ├── telemetry.sh
    └── validate-push.sh

Każdy hook to ~20 linii bash + jq. Bez frameworka. Bez build stepu. To co lubię w Claude Code: hooki są bash scriptami, nie kodem aplikacyjnym.


Jeśli korzystasz z Claude Code i hookami się jeszcze nie bawiłeś, zacznij od block-destructive.sh. Najmniejszy wysiłek, największy zwrot. Jak sam mówię agentowi: "ostre narzędzia, miękkie ręce".