Claude Code Hooks: Come Automatizzare il Controllo Qualità dei Tuoi Workflow AI
Due Agenti, Lo Stesso Codice, Risultati Incomparabili
Gennaio 2026. Ho messo in produzione due versioni dello stesso agente Claude sullo stesso server, per lo stesso cliente. Stesso system prompt. Stesso modello — Sonnet 4.6. Stessa pipeline di dati in ingresso, stessa struttura di output attesa.
Dopo 30 giorni di produzione, i risultati erano ai poli opposti. L'agente A aveva un tasso di errori silenti del 23%: operazioni completate senza eccezioni ma con output errato o incompleto. L'agente B aveva uno 0.4%.
Non era la differenza nel codice applicativo. Non era il modello. Non erano le istruzioni. Era una sola cosa: l'agente B aveva gli hooks configurati. L'agente A girava senza nessun meccanismo di intercettazione.
Gli hooks di Claude Code sono punti di aggancio nel ciclo di esecuzione dell'agente. Ti permettono di intercettare eventi precisi — prima che uno strumento venga chiamato, dopo che ha restituito un output, quando l'agente notifica l'utente, quando la sessione termina — e di reagire con logica custom: validare, bloccare, loggare, notificare.
Non sono un'opzione avanzata per casi estremi. Sono l'infrastruttura minima per operare su dati reali senza aspettare che qualcosa vada storto per accorgertene.
In questa guida: architettura dei 4 tipi di hook, configurazione del file settings.json, tre scenari reali con codice completo, benchmark prima/dopo su 8 mesi in produzione.
I 4 Tipi di Hook: Architettura e Timing
Prima di scrivere una sola riga di configurazione, è utile capire esattamente quando si attiva ciascun hook nel ciclo di esecuzione dell'agente.
| Hook | Timing | Input ricevuto | Uso tipico |
|------|--------|----------------|------------|
| PreToolUse | Prima dell'esecuzione di uno strumento | Tool name, parametri input | Validazione, blocco operazioni rischiose |
| PostToolUse | Dopo che lo strumento ha restituito output | Tool name, input, output, exit code | Logging, controllo qualità, trasformazione output |
| Notification | Quando l'agente invia un messaggio all'utente | Testo del messaggio, session ID | Alert su Slack, Telegram, email |
| Stop | Quando la sessione termina (normale o per errore) | Session ID, motivo di stop | Report finale, cleanup, archiviazione log |
Ogni hook riceve un payload JSON completo via stdin. Può restituire una risposta JSON via stdout. In base al tipo di hook, la risposta può approvare l'operazione, bloccarla, o modificarne l'output prima che raggiunga il modello.
Il matcher nel file di configurazione usa espressioni regolari per filtrare su quale strumento (tool) applicare l'hook. "Bash" intercetta solo le chiamate allo strumento Bash. ".*" intercetta tutte le chiamate a qualsiasi strumento. "(Bash|Write)" intercetta solo Bash e Write.
Questo significa che puoi configurare hook diversi per strumenti diversi, con logiche completamente indipendenti.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 /hooks/validate_bash.py"
}
]
}
],
"PostToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "python3 /hooks/log_output.py"
}
]
}
],
"Notification": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "python3 /hooks/notify_slack.py"
}
]
}
],
"Stop": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "python3 /hooks/session_report.py"
}
]
}
]
}
}
Configurazione Base: .claude/settings.json
Tutta la configurazione degli hooks risiede in un unico file: .claude/settings.json nella root del tuo progetto. Se il file non esiste, Claude Code opera senza hooks — nessun errore, nessun warning, semplicemente nessuna intercettazione.
Il command è uno script esterno che viene invocato come sottoprocesso. Puoi usare Python, Node.js, bash — qualsiasi runtime disponibile nel tuo ambiente. Lo script riceve il payload JSON via stdin e restituisce la risposta via stdout.
Una cosa importante: gli script degli hooks ereditano le variabili d'ambiente del processo Claude Code. Questo significa che puoi passare credentials sensibili (Slack webhook, database URL, API keys) tramite variabili d'ambiente senza scriverle nel codice.
Suggerimento pratico
Mantieni tutti gli script degli hook in una directory dedicata (ad esempio /hooks o .claude/hooks) e versionali insieme al progetto. Le credenziali devono vivere solo nelle variabili d'ambiente, mai negli script.
Scenario 1 — PreToolUse: Bloccare Operazioni Rischiose Prima che Avvengano
Il caso d'uso più critico per chi usa Claude Code su workflow B2B con accesso a filesystem e database: impedire che il modello esegua operazioni distruttive.
Non si tratta di sfiducia nel modello. Si tratta di avere un livello di difesa aggiuntivo per i casi in cui un'istruzione ambigua, un contesto mal interpretato o un'allucinazione possono portare a conseguenze irreversibili.
In produzione, il mio agente ha accesso allo strumento Bash per operazioni su file, query a database PostgreSQL, e sync con API esterne. Senza hook, una singola sessione con contesto corrotto poteva eliminare file, sovrascrivere dati o inviare email a clienti sbagliati.
import json
import sys
payload = json.loads(sys.stdin.read())
tool_input = payload.get("tool_input", {})
command = tool_input.get("command", "")
# Pattern che richiedono blocco immediato
dangerous_patterns = [
"rm -rf",
"DROP TABLE",
"DELETE FROM",
"TRUNCATE",
"> /dev/null 2>&1", # mascheramento di errori
"chmod 777",
"curl | bash", # esecuzione di script remoti
]
for pattern in dangerous_patterns:
if pattern.lower() in command.lower():
result = {
"decision": "block",
"reason": (
f"Operazione bloccata: pattern '{pattern}' rilevato nel comando. "
"Richiedere conferma esplicita o riformulare il comando."
)
}
print(json.dumps(result))
sys.exit(0)
# Nessun pattern rischioso: approva
print(json.dumps({"decision": "approve"}))
Quando l'hook restituisce "decision": "block", Claude Code non esegue il comando e riceve il messaggio di reason come feedback. Il modello può quindi riformulare la richiesta, chiedere conferma, o fermarsi.
Risultato pratico: 14 operazioni bloccate in 8 mesi su 4 agenti in produzione. Di queste, 3 erano rm -rf su percorsi sbagliati, 7 erano query DELETE prive di clausola WHERE, 4 erano comandi bash con mascheramento di errori. Senza questo hook, almeno 3 avrebbero causato perdita di dati irreversibile.
Non affidarti solo al prompt
I guardrail nel system prompt sono utili, ma non sostituiscono un controllo sintattico sul comando finale. Il PreToolUse è l'ultimo punto in cui puoi fermare un'operazione distruttiva prima che tocchi il filesystem o il database.
Scenario 2 — PostToolUse: Logging Strutturato per Debug e Audit
Il secondo problema che gli hooks risolvono è la visibilità. Un agente che opera in background è per definizione opaco: sai che gira, sai che ha prodotto un output finale, ma non hai idea di cosa è successo nel mezzo.
Quando qualcosa va storto — e va sempre storto prima o poi — senza log strutturati il debug diventa un'operazione cieca. Ricreare la sessione, ricostruire il contesto, capire quale tool call ha introdotto l'errore: con 40-50 tool call per sessione, senza log, puoi impiegarci ore.
L'hook PostToolUse con matcher ".*" registra ogni operazione in formato JSONL (una entry JSON per riga, ottimale per analisi con jq e tool simili):
import json
import sys
from datetime import datetime
import os
payload = json.loads(sys.stdin.read())
tool_response = payload.get("tool_response", {})
exit_code = (
tool_response.get("exit_code")
if isinstance(tool_response, dict)
else None
)
log_entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"session_id": payload.get("session_id"),
"tool": payload.get("tool_name"),
"input_preview": str(payload.get("tool_input", ""))[:300],
"output_length": len(str(tool_response)),
"exit_code": exit_code,
"is_error": exit_code not in (None, 0),
}
log_dir = os.environ.get("CLAUDE_LOG_DIR", "/var/log/claude-agent")
os.makedirs(log_dir, exist_ok=True)
log_path = os.path.join(log_dir, "operations.jsonl")
with open(log_path, "a") as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
# Non blocca mai, solo logga
print(json.dumps({"decision": "approve"}))
# Tool call più usate
jq -r '.tool' operations.jsonl | sort | uniq -c | sort -rn
# Sessioni con almeno un errore
jq -r 'select(.is_error == true) | .session_id' operations.jsonl | sort -u
# Output medio per tool
jq -r '[.tool, .output_length] | @tsv' operations.jsonl | sort | \
awk '{sum[$1]+=$2; count[$1]++} END {for(t in sum) print t, sum[t]/count[t]}'
Con questo log in produzione, rispondi a domande come:
- Quante tool call ha fatto l'agente in questa sessione?
- Qual è la distribuzione degli strumenti usati?
- Quanti exit code non-zero ho avuto questa settimana?
Nel mio setup, processo questi log ogni domenica con un secondo agente Claude che genera un report settimanale di performance e segnala eventuali anomalie.
Pattern consigliato per i log
Usa JSONL (una riga JSON per evento) e mantieni i file ruotati per data (operations-YYYY-MM-DD.jsonl). È il formato più semplice da consumare sia con tool da riga di comando sia con altri agenti AI dedicati all'osservabilità.
Scenario 3 — Notification: Alert su Slack per Task Lunghi
Il terzo scenario riguarda i workflow ad alta latenza. Quando un agente Claude elabora batch di dati, genera report su decine di record o sincronizza sistemi esterni, una singola sessione può durare 10-30 minuti. Il polling manuale sullo stato è inefficiente e inaffidabile.
L'hook Notification intercetta tutti i messaggi che l'agente invia all'utente e seleziona quelli critici per inviarli su Slack:
import json
import sys
import os
try:
import requests
except ImportError:
# Fallback silenzioso se requests non è disponibile
print(json.dumps({"decision": "approve"}))
sys.exit(0)
payload = json.loads(sys.stdin.read())
message = payload.get("message", "")
session_id = payload.get("session_id", "unknown")
# Filtra solo messaggi con parole chiave rilevanti
keywords = [
"completato", "errore", "fallito", "terminato",
"warning", "critico", "bloccato", "completati"
]
should_notify = any(kw in message.lower() for kw in keywords)
if should_notify:
webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
if webhook_url:
slack_payload = {
"text": f"*Claude Agent* [`{session_id[:8]}`]\n{message}",
"username": "Claude Agent",
"icon_emoji": ":robot_face:"
}
try:
requests.post(webhook_url, json=slack_payload, timeout=5)
except Exception:
pass # Non bloccare l'agente per un errore Slack
print(json.dumps({"decision": "approve"}))
Il filtro sulle keyword riduce il rumore. In produzione ho circa 3% di falsi positivi sul totale delle notifiche — messaggi non critici che contengono una keyword per caso. Tasso accettabile. L'alternativa — nessun alert — costava 15-20 minuti di latenza media per identificare sessioni fallite.
Riduci il rumore su Slack
Oltre alle keyword, puoi filtrare per tipo di messaggio (inizio, avanzamento, fine) usando campi aggiuntivi nel payload di Notification, e inviare su Slack solo completamenti, errori e warning critici.
Prima e Dopo: 8 Mesi con Hooks in Produzione su 4 Agenti B2B
Ecco i dati misurati sul mio setup (4 agenti Claude in produzione su workflow B2B: generazione report, sync CRM, elaborazione lead, aggiornamento documentazione):
| Metrica | Senza Hooks | Con Hooks | Delta |
|---------|-------------|-----------|-------|
| Tasso errori silenti | 23% | 0.4% | -98.3% |
| Tempo medio di debug per sessione | 47 min | 8 min | -83% |
| Operazioni rischiose bloccate (8 mesi) | N/D | 14 totali | — |
| Visibilità sulle operazioni | 0% | 100% | — |
| Latenza media rilevazione errori | 4.2 ore | 12 min | -95% |
Il dato che colpisce di più non è il tasso di errori silenti. È la latenza di rilevazione: da oltre 4 ore a 12 minuti. Senza hooks, un errore silente veniva scoperto quando il cliente segnalava un problema o quando facevo una review manuale. Con hooks e notifiche Slack, la rilevazione è quasi in tempo reale.
I 14 blocchi del PreToolUse non sono un segnale che l'agente "non funziona". Sono esattamente il contrario: sono 14 operazioni che in precedenza sarebbero passate in silenzio e avrebbero richiesto ore di rollback manuale.
Overhead di performance
Ogni hook aggiunge latenza (tipicamente 20–50 ms per invocazione con script leggeri). Con ~40 tool call per sessione e 3 hook per chiamata, parliamo di 2–6 secondi di overhead su una sessione da 10 minuti: 0.3–1%. Il trade-off è quasi sempre favorevole rispetto al rischio di errori silenti.
La documentazione ufficiale di Anthropic sugli hooks di Claude Code è disponibile su docs.anthropic.com con tutti i dettagli sui payload e le opzioni di risposta.
Come Iniziare Questa Settimana: 5 Passi
Se stai usando Claude Code senza hooks, il punto di partenza non è configurare tutto. È costruire la visibilità prima, la protezione dopo.
Passo 1 — Crea la struttura delle directory
```bash
mkdir -p .claude/hooks
touch .claude/settings.json
```
Passo 2 — Configura solo il PostToolUse per il logging
Inizia con l'hook più semplice e meno invasivo. Copia la configurazione del settings.json mostrata sopra, attiva solo la sezione PostToolUse, e scrivi il tuo script di logging in poche righe Python.
Passo 3 — Esegui Claude Code su un task reale e verifica il log
Lancia il tuo agente su un workflow consueto e controlla che il file operations.jsonl si popoli correttamente. Questo ti dà già un beneficio immediato: visibilità completa su cosa fa l'agente.
Passo 4 — Analizza i log per identificare i pattern rischiosi
Dopo 2-3 sessioni, esamina le tool call registrate: quali comandi bash vengono eseguiti? Ci sono pattern che non ti aspettavi? Identifica i 3-5 comandi che non vuoi mai vedere in produzione senza conferma.
Passo 5 — Aggiungi il PreToolUse con i pattern identificati
Solo a questo punto costruisci il blocco preventivo. Con i log a disposizione, sai esattamente cosa proteggere. Non stai lavorando su ipotesi: stai costruendo difese su pattern reali osservati nel tuo workflow specifico.
Questo processo richiede 2-3 ore la prima volta. Una volta in produzione, la manutenzione è minima: aggiungi un pattern al blocklist quando scopri un nuovo edge case, raramente altro.
Integra gli hooks con i task schedulati
Se usi già Claude Code per task schedulati, gli hooks trasformano un semplice cron job in un sistema osservabile: puoi avere report automatici, alert sugli errori e blocchi preventivi senza toccare il codice applicativo principale.
Gli Hooks Non Sono Opzionali. Sono Infrastruttura.
Un agente senza hooks è un processo senza logging, senza validazione, senza visibilità. Funziona in sviluppo. In produzione, su dati reali di clienti reali, è un sistema cieco che aspetta di fallire silenziosamente.
La differenza tra il 23% e lo 0.4% di errori silenti non è nel modello, non è nel prompt, non è nell'architettura applicativa. È nell'infrastruttura di controllo attorno al modello.
Gli hooks sono quella infrastruttura. Sono l'equivalente dei middleware in un framework web, dei pre-commit hook in git, dei circuit breaker in un'architettura a microservizi: non si vedono nell'output finale, ma senza di loro ogni operazione è un potenziale problema non rilevato.
Se vuoi costruire sistemi Claude che reggono la pressione di produzione — non prototipi, sistemi — ha senso investire tempo in orchestrazione multi-agente, gestione degli errori e pattern di resilienza per workflow complessi.
Domande Frequenti
Gli hooks funzionano anche con Claude Code in modalità SDK/headless?
Sì. Gli hooks vengono eseguiti anche quando usi Claude Code tramite SDK in modalità non interattiva o come sottoprocesso. Il file .claude/settings.json viene letto dalla directory del progetto indipendentemente dalla modalità di esecuzione — interattiva, headless, o schedulata.
Posso usare gli hooks per modificare l'output di uno strumento prima che l'agente lo legga?
Sì. Il PostToolUse hook può restituire un campo output_override nella risposta JSON. Il valore di questo campo sostituisce l'output originale del tool prima che raggiunga il modello. Utile per sanitizzare dati sensibili, troncare output eccessivamente lunghi, o normalizzare formati prima dell'elaborazione.
Come passo le credenziali agli hook scripts senza hardcodarle?
Gli script ricevono l'environment del processo Claude Code, incluse tutte le variabili d'ambiente impostate prima del lancio. Il pattern standard: lancia Claude Code con le variabili già esportate (SLACK_WEBHOOK_URL=xxx claude) o usa un file .env caricato dall'entry point del tuo sistema. Non scrivere mai credenziali direttamente nel codice degli hook.
Come debuggo uno script hook che non funziona come atteso?
Redireziona stderr su un file di log separato: "command": "python3 /hooks/my_hook.py 2>> /tmp/hooks_debug.log". Gli errori dello script non interrompono l'agente (Claude Code continua l'esecuzione), ma vengono scritti nel file di debug. In questo modo puoi iterare sullo script senza bloccare la produzione.
È possibile avere hook diversi per ambienti diversi (sviluppo vs produzione)?
Sì. Il modo più semplice: usa variabili d'ambiente per condizionare il comportamento degli script. In alternativa, mantieni due file .claude/settings.json separati e selezioni quale usare tramite symlink o variabile d'ambiente personalizzata nel tuo script di lancio. Alcuni team usano directory di progetto distinte per i due ambienti.
Ogni settimana condivido workflow, errori e numeri reali
21 automazioni in produzione, zero dipendenti. Su LinkedIn documento il dietro le quinte: cosa funziona, cosa no, e i dati che nessuno mostra.
