Hook di Claude Code: come si trasforma una regola in un vincolo (e perché il primo lo metterai nel posto sbagliato)
Una regola scritta nel CLAUDE.md è un consiglio: il modello può ignorarla, e il giorno che lo fa non te lo dice. L'hook è il posto dove quella regola diventa un vincolo. Ma il primo hook finisce quasi sempre nel punto sbagliato, e per scoprirlo devi romperci qualcosa: qui c'è il registro di cosa ho rotto io, con le date.
Un hook di Claude Code è uno script che il client esegue a un evento preciso, prima o dopo l'uso di uno strumento, e che può bloccare l'azione uscendo con codice 2. Serve quando una regola deve valere sempre: il CLAUDE.md la suggerisce al modello, l'hook la impone al processo. Il difficile non è scriverlo, è scegliere dove metterlo.
Il 30 luglio 2026 un'infografica è andata online con le emoji al posto delle icone SVG. Era destinata a un post LinkedIn, sul mio profilo e su quello aziendale. La regola che vieta le emoji nei contenuti c'era, stava nel file che entra in contesto a ogni sessione, ed è stata razionalizzata: il modello si è spiegato perché quelle spunte andassero bene lì.
Quel giorno ho capito una cosa che poi mi è costata cinque giorni di lavoro per applicarla bene: una regola dentro un prompt è un consiglio, e un consiglio viene disatteso in silenzio.
Perché una regola scritta nel CLAUDE.md non basta?
Perché il CLAUDE.md entra nel contesto e da lì in poi compete con tutto il resto: il compito che stai svolgendo, i file che hai aperto, le venti istruzioni precedenti. È materiale su cui il modello ragiona, non un vincolo che qualcuno fa rispettare. Nella maggior parte dei casi funziona. Nel caso che conta, cioè quando la regola è scomoda rispetto a quello che l'agente sta cercando di fare, cede.
L'hook sta da un'altra parte. Non è testo, è un processo che il client lancia a un evento, con il payload di quello che sta per succedere sullo standard input. Non ragiona e non negozia: guarda ed esce con un numero.
Gli eventi disponibili sono parecchi e la semantica dei codici di uscita la documenta Anthropic per esteso nella reference ufficiale sugli hook. Qui non la riscrivo: quello che manca non è l'elenco, è sapere dove va messa la regola.
Questo lo rende adatto a una sola categoria di regole, quelle che non ammettono eccezioni valutabili. Per tutto il resto il CLAUDE.md resta lo strumento giusto, ed è meglio: una regola motivata che il modello capisce copre anche i casi che tu non avevi previsto, mentre un hook copre esattamente e solo i casi che hai scritto.
Quando un hook è la scelta sbagliata?
Quando la regola parla di un percorso.
Il 1 marzo 2026 avevo messo nel repo del sito quattro hook. Due erano guardie su file: uno bloccava le modifiche a css/style.css, che è generato da Tailwind e non si tocca a mano, l'altro bloccava .env.deploy, che contiene le credenziali FTP. Funzionavano. Sono rimasti vivi cinque mesi.
Il 5 agosto li ho cancellati entrambi, e non perché fossero rotti. Perché erano nel posto sbagliato. Una regola su un percorso non è un hook, è una riga di permessi:
"permissions": {
"deny": [
"Edit(./css/style.css)",
"Write(./css/style.css)",
"Read(./.env.deploy)",
"Edit(./.env.deploy)",
"Write(./.env.deploy)"
]
}
Cinque righe in settings.local.json fanno il lavoro di due script. Zero processi lanciati a ogni scrittura, zero corse fra hook concorrenti, nessun timeout da gestire. E su .env.deploy c'è in più una cosa che l'hook non faceva: copre anche Read. Le credenziali non si modificano, ma soprattutto non si leggono, e quello me lo ero perso.
La domanda da farsi prima di scrivere un hook è quindi secca: la regola si può esprimere come "questo strumento non tocca questo percorso"? Se sì, è una riga di permessi. L'hook serve quando la decisione dipende dal contenuto, cioè da qualcosa che nella lista dei permessi non entra.
Dove va messo un controllo sul contenuto?
Nel punto dove il contesto è completo, che quasi mai è il punto dove avviene l'azione.
Il 1 agosto ho scritto brand-guard, il controllo che fa rispettare le regole di scrittura. Prima versione: si aggancia a Write e Edit, passa il testo al linter, se trova una violazione esce 2. Sembra ovvio, ed è sbagliato in cinque modi diversi. Li ho scoperti tutti nella stessa settimana:
- girava su
js,php,cssesvg, cioè sul codice, dove il tono di voce non c'entra niente. Un trattino lungo dentro un commento PHP faceva uscire 2. I miei stessi hook contenevano trattini lunghi nei commenti; - con
Writevedeva il file intero, quindi imputava all'agente righe scritte mesi prima da qualcun altro; - usciva 2 dopo che la scrittura era già avvenuta;
- dava i numeri di riga del frammento spacciandoli per numeri di riga del file, quindi le correzioni finivano sulla riga sbagliata;
- si appendeva quando lo standard input non si chiudeva.
Il primo tentativo di rimedio è stato un filtro: escludi questa cartella, escludi quell'estensione. Ho passato giorni ad aggiungere eccezioni, e il filtro non bastava mai. Ogni volta si rompeva qualcos'altro, il deploy o lo sviluppo di una pagina.
Il filtro non bastava perché il problema non era quale file guardare, era quando guardare. Il gate è finito al commit:
// PreToolUse su Bash: se il comando e' un git commit,
// controlla il brand sulle SOLE righe aggiunte dal diff.
const E_UN_COMMIT = /\bgit\b[^|;&]*\bcommit\b/;
Tre ragioni, e valgono per qualsiasi controllo sul contenuto, non solo per il mio:
- al commit vedi il file finito, non un frammento a metà;
- vedi il diff, quindi il pregresso è escluso per costruzione e non a forza di eccezioni;
- blocchi prima dell'azione, che è l'unico posto dove un blocco ha senso.
Il punto due è quello che ha chiuso la partita. Tutte le eccezioni che stavo scrivendo a mano servivano a distinguere le righe nuove da quelle vecchie. Guardando il diff, quella distinzione è gratis.
E una via d'uscita è parte del gate, non una concessione: brand-lint:ignore sulla riga per una citazione legittima, [skip brand] nel messaggio di commit quando serve saltare tutto. Il secondo resta scritto nella storia del repo, che è esattamente il punto. Un muro senza porta non viene rispettato, viene spento.
Perché un blocco che scatta dopo l'azione non è un blocco?
Perché diventa un ciclo.
Un PostToolUse che esce 2 comunica un errore all'agente quando la scrittura è già stata fatta. L'agente non può annullarla: può solo riscrivere. Riscrive, l'hook ricontrolla, se il testo ha ancora un problema esce 2 di nuovo. Correggi, riscrivi, ribloccato.
In una sessione interattiva te ne accorgi e intervieni. Dentro un workflow con agenti in parallelo no: il ciclo consuma il budget di turni del subagente e fa saltare l'intera fase, senza che nessuno lo dica. Il 4 agosto è successo, e per un giorno intero ho sospettato le due guardie sui percorsi, che avevo disattivato il giorno prima. Non erano loro. Era brand-guard, che era rimasto acceso.
La regola che ne ho ricavato: se blocca, deve essere PreToolUse. Se l'evento è PostToolUse, il massimo che può fare è informare, e allora esce sempre 0 e scrive un messaggio non bloccante. Oggi brand-guard fa esattamente questo, e il gate vero sta cinquanta righe più in là, sul commit.
Messe in fila, le tre decisioni stanno in uno schema solo:

Come si scrive un hook che non diventi lui il problema?
Tre regole, imparate lo stesso giorno e scritte in testa al modulo che ora tutti i miei hook usano per leggere l'input.
Non aspettare la chiusura dello standard input. Su Windows l'evento di fine a volte non arriva mai: l'hook resta appeso fino al timeout del client, e con dieci subagent in parallelo questo uccide il turno. Si decide appena il JSON è completo, senza aspettare altro.
Fai fail-open. Se dopo qualche secondo non hai capito niente, lascia passare. Un hook incerto che blocca è peggio di un hook assente, perché il lavoro si ferma e la causa non è visibile da nessuna parte.
Scrivi in modo sincrono. process.exit() tronca le scritture pendenti su una pipe: un exit 2 con il messaggio tagliato a metà è un blocco senza motivo dichiarato, e l'agente ci gira attorno a vuoto.
A queste ne aggiungo una quarta che vale come principio: se lo script che decide solleva un'eccezione, si esce 0. Un hook rotto non deve fermare nessuno.
Come si prova un hook prima di fidarsi?
Eseguendolo. È l'unica famiglia di file di configurazione che si testa lanciandola invece che rileggendola, ed è il motivo per cui è anche l'unica su cui vale la pena scrivere dei test veri.
Un caso di prova è una riga: un payload JSON finto sullo standard input e il codice di uscita che ti aspetti.
echo '{"tool_input":{"file_path":"clients/rossi-spa/relazione.md",
"content":"Il progetto e finito."}}' | node .claude/hooks/brand-guard.js
echo "atteso 0, ottenuto: $?"
Nel mio banco i casi sono decine, uno per guardia e per situazione, e ci passo ogni volta che tocco una regola. Non è disciplina: è che sugli hook non esiste modo di sapere se funzionano guardandoli. Ho lavorato su decine di hook e li ho provati decine e decine di volte prima che si comportassero bene. Alla fine è questione di ripetizione, e il banco è ciò che rende la ripetizione sostenibile.
Una nota su quel percorso finto: nel mio banco quel caso usa il nome di un cliente vero, perché serviva un percorso realistico. Qui l'ho sostituito, e la ragione per cui l'ho fatto è la stessa per cui esiste la guardia sulla riservatezza. Se scrivi hook che presidiano regole, poi tocca rispettarle anche quando racconti gli hook.
Domande frequenti
Qual è la differenza fra un hook di Claude Code e un hook di git?
Sono cose diverse che risolvono problemi diversi. Un hook di git scatta su un'operazione di git e vale per chiunque lavori nel repo, esseri umani compresi. Un hook di Claude Code scatta su un'azione dell'agente, quindi vede cose che git non vede mai: un file che sta per essere scritto, un comando che sta per partire, una chiamata a uno strumento esterno. I due si sommano bene: il pre-commit di git è la rete finale, l'hook è il controllo che arriva prima e che può spiegare all'agente cosa correggere.
Un hook rallenta le sessioni di lavoro?
Sì, e va misurato. Ogni hook registrato è un processo lanciato a ogni evento corrispondente, quindi tre guardie separate su Write significano tre processi a ogni scrittura. La contromisura è registrare un solo ingresso per evento e far interrogare le singole regole da lì, che è quello che ho fatto quando le guardie sono diventate più di due. Metti sempre un timeout esplicito nella registrazione: senza, un hook che si impianta blocca il lavoro fino al timeout del client.
Meglio bloccare o solo avvisare con un hook?
Blocca solo dove il danno è irreversibile, avvisa in tutti gli altri casi. Una credenziale finita nella storia di git ci resta, un nome di cliente pubblicato è già uscito, un errore di scrittura si corregge dopo se il testo è ancora lì. La tentazione è bloccare tutto, e il risultato di quella tentazione è prevedibile: un hook che blocca troppo viene disattivato, e da quel momento non protegge più niente.
Da dove partire
Non partire dagli hook. Parti dal CLAUDE.md, che nella mia esperienza copre quasi tutte le regole che vorresti far rispettare, e costa niente: se ancora non ce l'hai, la guida per scriverne uno che cambi davvero il comportamento del modello viene prima di questa. Poi guarda le due o tre regole che continuano a essere disattese e chiediti, per ognuna: è una regola su un percorso? Allora è una riga di permissions.deny. È una regola sul contenuto? Allora l'hook va nel punto dove il contenuto è completo, non dove viene scritto.
Se ti interessa come sta insieme il resto della configurazione, la struttura di un progetto Claude Code è la mappa in cui questo pezzo è un livello sotto.
Gli strumenti che pubblichiamo, incluse le skill che usiamo davvero, stanno nell'Open Lab.
Tags
Founder & CEO · Castaldo Solutions
Sono un consulente di trasformazione digitale con esperienza enterprise. Aiuto le PMI italiane ad adottare AI, CRM e architetture IT con risultati misurabili in 90 giorni.