Guida

Hook di Claude Code: come trasformare le regole dell'agente in garanzie

Aggiornato: · Skillaria.Top

Gli hook di Claude Code sono comandi di shell che Claude Code stesso esegue in eventi precisi: prima e dopo una chiamata a uno strumento, all'invio di un prompt, all'avvio della sessione e prima che l'agente si fermi. Si configurano in settings.json, e un codice di uscita 2 blocca l'azione e restituisce all'agente un messaggio di errore: così l'hook trasforma una regola del CLAUDE.md in una garanzia che il modello non può dimenticare.

Che cosa sono gli hook di Claude Code

Un hook è un comando che non viene eseguito dal modello, ma dal programma Claude Code stesso, in un momento preciso del lavoro dell'agente. L'agente sta per eseguire rm -rf — l'hook scatta e decide se lasciar passare il comando. L'agente ha modificato un file — l'hook avvia il formattatore. L'agente vuole concludere il lavoro — l'hook esegue i test e non lo lascia fermarsi finché sono rossi.

La differenza principale rispetto alle istruzioni nel CLAUDE.md: un'istruzione è un testo che il modello tiene in considerazione con una certa probabilità, mentre un hook è codice deterministico. Scatta sempre, a prescindere da quanto sia lungo il contesto, da ciò che l'agente ha «deciso» o dal fatto che abbia dimenticato la regola al centesimo minuto della sessione. Per questo gli hook di Claude Code sono il modo principale per trasformare un desiderio in una garanzia.

Perché le regole nel CLAUDE.md non bastano

Un quadro tipico: l'agente ha completato una funzionalità, i test unitari sono verdi, in chat compare un sicuro «Fatto». Ma nessuno ha eseguito lo scenario end-to-end, il linter non è stato lanciato, la migrazione è caduta a metà. Nel CLAUDE.md però c'è scritto «prima di concludere, esegui i test». Il modello non sta barando di proposito: semplicemente valuta sé stesso, e questa autovalutazione non è affidabile.

Da qui il principio dell'harness: tutto ciò che si può verificare in modo meccanico viene verificato dalla meccanica, non dal modello. La regola «esegui il linter» diventa un hook che esegue il linter. La regola «non toccare la configurazione di produzione» diventa un hook che blocca la scrittura su quel file. L'istruzione testuale resta, ma serve solo a spiegare all'agente perché è stato fermato.

Eventi: quando scattano gli hook

Ogni hook è legato a un evento del ciclo di vita della sessione. Gli eventi principali:

EventoQuando scattaUso tipico
PreToolUseprima della chiamata a uno strumento (Bash, Edit, Write ecc.)blocco dei comandi pericolosi, protezione dei file
PostToolUsedopo una chiamata riuscita a uno strumentoformattatore, linter, test rapidi dopo una modifica
UserPromptSubmitquando l'utente invia un prompt, prima che venga elaboratoaggiungere contesto, bloccare un prompt con un segreto
Stopquando l'agente sta per concludere la rispostaimpedire la fermata finché i controlli non sono superati
SubagentStopquando un subagente termina il lavoroverifica del risultato del subagente
SessionStartall'avvio o alla ripresa della sessionecaricare lo stato: git status, piano, registro del progresso
SessionEndalla chiusura della sessionepulizia, scrittura dei log
Notificationquando Claude Code invia una notifica (per esempio, attende un permesso)suono o messaggio sul telefono
PreCompactprima della compressione del contestosalvare ciò che è importante prima della compattazione

I più utili per l'affidabilità sono PreToolUse, PostToolUse e Stop: coprono tre punti deboli dell'agente — le azioni pericolose, le modifiche non verificate e il «fatto» prematuro.

Dove si configurano gli hook: settings.json

Gli hook si descrivono nei file di configurazione di Claude Code:

  • ~/.claude/settings.json — personali, per tutti i progetti;
  • .claude/settings.json nel repository — condivisi con il team, si committano in git;
  • .claude/settings.local.json — locali al progetto, non finiscono in git.

La struttura è questa: evento → lista di gruppi con matcher → lista di comandi.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

matcher è il nome di uno strumento o un'espressione regolare su di esso: Bash, Edit|Write, .*. Serve per gli eventi legati agli strumenti; per Stop o UserPromptSubmit si può non indicarlo. La variabile CLAUDE_PROJECT_DIR punta alla radice del progetto, quindi è comodo tenere gli script in .claude/hooks/ e richiamarli tramite essa. Gli hook attivi si vedono con il comando /hooks dentro Claude Code.

Come un hook riceve i dati e risponde

Ingresso: JSON su stdin

Claude Code passa all'hook un JSON con i dati dell'evento: identificativo della sessione, cartella di lavoro, nome dell'evento e, per gli strumenti, tool_name e tool_input. Per Bash, in tool_input.command c'è il comando; per Edit e Write, in tool_input.file_path, il percorso del file. Conviene leggerlo con jq.

{
  "session_id": "abc123",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf build/" },
  "cwd": "/home/dev/project"
}

Uscita: codice di ritorno

CodiceCosa succede
0successo, il lavoro continua; per UserPromptSubmit e SessionStart lo stdout viene aggiunto al contesto dell'agente
2errore bloccante: lo stderr va all'agente e l'azione si ferma (per PreToolUse la chiamata allo strumento viene annullata; per Stop l'agente continua a lavorare)
altroerrore non bloccante: lo stderr viene mostrato all'utente, il lavoro prosegue

Il codice 2 è il meccanismo chiave. L'agente riceve il testo dello stderr come feedback e può correggersi: riscrivere il comando, sistemare il test, non toccare il file. Quindi nello stderr non scrivete «errore», ma la causa comprensibile e ciò che va fatto al suo posto.

Oltre al codice di ritorno, un hook può scrivere nello stdout un JSON con la decisione, per esempio "decision": "block" e "reason". Serve per una configurazione fine, ma per la maggior parte dei casi bastano il codice 2 e un messaggio nello stderr.

Esempio 1: linter e test dopo ogni modifica

Un hook su PostToolUse con matcher Edit|Write scatta dopo la modifica di un file. Lo strumento è già stato eseguito e la modifica non si può annullare, ma il codice 2 restituisce all'agente l'output del linter, ed è lui a correggere l'errore subito, non dopo mezz'ora.

#!/usr/bin/env bash
# .claude/hooks/lint.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
  *.py)
    if ! out=$(ruff check "$file" 2>&1); then
      echo "Il linter ha trovato errori in $file:" >&2
      echo "$out" >&2
      exit 2
    fi ;;
  *.php)
    if ! out=$(php -l "$file" 2>&1); then
      echo "$out" >&2
      exit 2
    fi ;;
esac
exit 0

Non mettete qui un'intera esecuzione dei test di dieci minuti: l'hook scatta a ogni modifica. Dopo una modifica bastano controlli rapidi (sintassi, linter, test di un singolo modulo), quelli pesanti vanno nell'hook Stop.

Esempio 2: blocco dei comandi pericolosi

Un hook su PreToolUse con matcher Bash vede il comando prima dell'esecuzione. Il codice 2 annulla la chiamata e l'agente riceve la spiegazione.

#!/usr/bin/env bash
# .claude/hooks/guard.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push (-f|--force)|DROP (TABLE|DATABASE)'; then
  echo "Comando bloccato dalla politica del progetto: $cmd. Le operazioni pericolose le esegue una persona." >&2
  exit 2
fi
exit 0

Allo stesso modo si proteggono i file: un hook su Edit|Write controlla file_path e blocca la scrittura su .env, sulle configurazioni di produzione, sul codice generato o sui file di migrazione già applicati. Tenete presente che il filtro con espressione regolare si può aggirare — l'agente può scrivere il comando in un altro modo. L'hook è una protezione forte, ma non sostituisce i permessi di accesso né la sandbox.

Esempio 3: l'agente non può dire «fatto» finché i test sono rossi

L'hook Stop scatta quando l'agente sta per concludere. Se i test non passano, il codice 2 impedisce la fermata e l'output dei test diventa per l'agente il compito successivo. È la versione meccanica della Definition of Done.

#!/usr/bin/env bash
# .claude/hooks/done.sh
input=$(cat)
# protezione da un ciclo infinito: l'hook ha già prolungato il lavoro una volta
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
if ! out=$(npm test --silent 2>&1); then
  echo "I test non passano, il lavoro non è concluso:" >&2
  echo "$out" | tail -n 40 >&2
  exit 2
fi
exit 0

Il campo stop_hook_active vale true se l'agente sta già proseguendo il lavoro a causa di un hook di stop. Senza questo controllo l'hook può far girare l'agente in tondo all'infinito — proprio la situazione in cui l'agente «lavora tutta la notte nella direzione sbagliata». Nei cicli autonomi aggiungete anche un limite di tentativi e una condizione in cui si chiama una persona.

Esempio 4: caricare lo stato all'inizio della sessione

Su SessionStart lo stdout dell'hook finisce nel contesto. Conviene stampare il ramo corrente, le modifiche non committate e il prossimo compito dal piano, così l'agente non parte da zero.

#!/usr/bin/env bash
echo "Ramo: $(git branch --show-current)"
git status --short | head -n 20
[ -f PROGRESS.md ] && tail -n 15 PROGRESS.md
exit 0

Come gli hook trasformano le regole in garanzie

Conviene dividere le regole dell'harness in tre livelli. Il primo è il testo nel CLAUDE.md: spiega come lavorare, ma viene rispettato in modo probabilistico. Il secondo sono i controlli eseguibili: test, E2E, regole architetturali sotto forma di script che cadono in caso di violazione. Il terzo sono gli hook, che lanciano questi controlli automaticamente nel momento giusto, senza la partecipazione del modello.

Regola pratica: se l'agente ha violato due volte la stessa regola, non aggiungete nel CLAUDE.md un ulteriore avvertimento in maiuscolo. Chiedetevi se la violazione si può verificare con uno script. Se sì, scrivete lo script e agganciatelo a un hook. Così l'istruzione testuale diventa più breve e il comportamento più stabile.

Regola nel testoGaranzia tramite hook
«Formatta il codice secondo lo standard»PostToolUse avvia il formattatore sul file modificato
«Non fare push con force»PreToolUse su Bash blocca il comando, exit 2
«Non dire fatto finché i test sono rossi»Stop avvia i test e non lascia concludere
«Non modificare .env»PreToolUse su Edit|Write verifica il percorso
«Ricorda dove ti sei fermato»SessionStart mostra il piano e git status

Errori tipici

  • Codice 1 invece di 2. Lo script termina con codice 1, l'agente non se ne accorge e l'azione passa. Bloccare può solo il codice 2.
  • Messaggio nello stdout. Con il codice 2 all'agente arriva lo stderr. Scrivete con >&2.
  • Hook lento su un evento frequente. L'esecuzione completa dei test in PostToolUse rende ogni modifica un'attesa di minuti. Le operazioni pesanti vanno in Stop.
  • Stop infinito. Manca il controllo stop_hook_active o un limite — l'agente gira senza fine.
  • Percorsi relativi. L'hook viene eseguito nella cartella di lavoro della sessione; usate $CLAUDE_PROJECT_DIR.
  • Hook altrui non verificati. L'hook viene eseguito con i vostri permessi senza conferma. Leggete gli script dai repository altrui prima di fidarvene.

Gli hook di Claude Code spostano le regole più importanti dal testo al codice: PreToolUse ferma le azioni pericolose, PostToolUse controlla ogni modifica, Stop impedisce di dichiarare il lavoro finito prima del tempo, SessionStart restituisce all'agente la memoria del progetto. Iniziate con due o tre hook sulle regole che l'agente ha già violato, e mantenete i messaggi nello stderr chiari: così l'agente corregge da solo gli errori e voi controllate il risultato, non il processo. La validazione, il controllo di prontezza e i cicli autonomi li affrontiamo in pratica nel corso.

Domande frequenti

Serve riavviare Claude Code dopo aver modificato gli hook?

Le modifiche in settings.json si applicano più affidabilmente in una nuova sessione; la configurazione attuale degli hook si può vedere con il comando /hooks.

Si possono scrivere gli hook in un linguaggio diverso dal bash?

Sì, un hook è un qualsiasi comando: uno script Python, Node.js o un eseguibile. L'importante è leggere il JSON dallo stdin e restituire il codice di uscita corretto.

L'agente vede l'output di un hook riuscito?

Di solito no: con il codice 0 l'output non entra nel contesto. Le eccezioni sono UserPromptSubmit e SessionStart, dove lo stdout viene aggiunto al contesto dell'agente.

Gli hook funzionano anche per i subagenti?

Sì, le chiamate agli strumenti dei subagenti passano attraverso gli stessi hook PreToolUse e PostToolUse, e per la loro conclusione esiste l'evento separato SubagentStop.

È sicuro committare gli hook nel repository?

Sì, .claude/settings.json e gli script in .claude/hooks/ sono una pratica comune per un team; gli hook personali conviene tenerli in settings.local.json o in ~/.claude/settings.json.