Hook in Claude Code: configurazione e 7 esempi
Markdown7 hook pronti all’uso per .claude/settings.json, da copiare e incollare: protezione di .env, prettier, force-push su main, trattini lunghi e un server di sviluppo. 4 tipi di handler e 31 eventi.
Leggi l’articolo completo
Gli hook in Claude Code sono comandi shell, chiamate HTTP o brevi verifiche LLM eseguiti automaticamente prima o dopo un’azione di Claude. Aggiungono controlli su eventi specifici, formattano dopo le modifiche e inseriscono contesto aggiornato all’avvio. Un PreToolUse corrispondente può negare quella chiamata; le regex sono controlli di comodità, non un confine per ogni accesso a file o Git. Di seguito trovi una panoramica dei 4 tipi di handler, dei 31 eventi del ciclo di vita, un test per capire «quando una regola dovrebbe essere un hook», 7 hook pronti da copiare in .claude/settings.json e 3 insidie in cui sono incappato personalmente.
Documentazione di Ailoot: API, chiavi e configurazione.
Cosa sono gli hook in Claude Code?
Punto chiave
Un hook è un comando shell, una chiamata HTTP, un breve prompt LLM o un subagente che Claude Code esegue in risposta a uno specifico evento del ciclo di vita. A differenza delle istruzioni in CLAUDE.md, che il modello può ignorare o dimenticare, un hook viene eseguito ogni volta allo stesso modo: è codice, non un suggerimento.
Per sei mesi ho mantenuto in CLAUDE.md la regola «non modificare mai direttamente .env». Claude la violava una volta al mese. Non intenzionalmente; semplicemente la dimenticava verso la fine di una lunga sessione. Una volta è finito un token in più in .env; un’altra, una variabile con un nome duplicato ha sovrascritto quella in uso.
Ho spostato quella regola in un hook, rendendola impossibile da dimenticare. Quando uno strumento di modifica diretta corrispondente fornisce un percorso .env, l’hook rifiuta la chiamata. Redirezioni Bash, script, MCP e altri percorsi richiedono controlli separati: il file non diventa immutabile.
Gli hook sono comandi shell, endpoint HTTP o prompt LLM che definisci e che vengono eseguiti automaticamente in punti specifici del ciclo di vita di Claude Code.
In altre parole, un hook è un tuo comando che Claude Code esegue in occasione di un evento specifico. Ogni modifica a un file, avvio di Bash, inizio di sessione o completamento di un subagente è un evento a cui puoi collegare un hook.
Perché è importante per chi programma con il vibe coding:
- Proteggere da modifiche accidentali i file che contengono segreti.
- Eseguire automaticamente prettier/eslint dopo ogni generazione di codice.
- Bloccare
git push --forcesu main senza un’autorizzazione esplicita. - Inserire contesto aggiornato all’inizio di ogni sessione.
- Registrare le azioni di Claude a fini di audit.
CLAUDE.md gestisce il contesto. Gli hook gestiscono le azioni. Sono due cose diverse e confonderle è il principale errore dei principianti.
In cosa differiscono gli hook da CLAUDE.md e dalle skill?
Punto chiave
CLAUDE.md è un testo che il modello legge all’inizio di una sessione e cerca di seguire. Un hook è un comando shell che il processo di Claude Code esegue in risposta a un evento, indipendentemente dal modello. Una skill è un pacchetto di istruzioni con un nome che Claude può invocare con lo strumento Skill, come una funzione. Ognuno dei tre serve a tipi diversi di attività.
La documentazione di Anthropic spiega esplicitamente la differenza tra CLAUDE.md e gli hook:
Il contenuto di CLAUDE.md viene fornito come messaggio dell’utente dopo il prompt di sistema, non come parte del prompt di sistema. Claude lo legge e cerca di seguirlo, ma non è garantita un’adesione rigorosa.
In altre parole, il contenuto di CLAUDE.md arriva a Claude come primo messaggio dell’utente, non come istruzione di sistema. Il modello lo legge e cerca di seguirlo, ma non ci sono garanzie, soprattutto verso la fine di una lunga sessione, quando il contesto cresce e le istruzioni precedenti vengono espulse.
Un hook funziona in modo diverso. Si trova in settings.json, non dipende dal modello e viene eseguito dal processo di Claude Code in occasione di un evento specifico: PreToolUse, PostToolUse, SessionStart e altri. Il comando viene eseguito prima che Claude riceva il risultato e può bloccare fisicamente un’azione (exit 2) o modificare un file dopo averlo editato (exit 0).
Confronto:
| Funzionalità | CLAUDE.md | Hook | Skill |
|---|---|---|---|
| Dove si trova | CLAUDE.md nella radice del progetto | Cartella .claude/settings.json | Cartella .claude/skills/ |
| Chi lo esegue | Il modello lo legge come parte di un messaggio dell’utente | Il processo di Claude Code in occasione di un evento | Il modello tramite lo strumento Skill |
| Garanzia di esecuzione | No (best effort) | Un comando configurato e corrispondente esegue codice; guasti e uscita dipendono dall’evento | Tramite una chiamata al modello (con un grant, ma non a livello shell) |
| A cosa serve | Contesto del progetto, tono, stile, instradamento | Restrizioni, formattazione, controlli su file specifici | Procedure complesse composte da più passaggi e con parametri |
| Quando viene eseguito | A ogni messaggio del modello in una sessione | In occasione di un evento del ciclo di vita | Quando viene invocato dal modello |
| Può bloccare un’azione? | No | Sì (exit 2) | No |
Il modello completo di CLAUDE.md e 6 regole per un file vivo si trovano nella guida Come configurare CLAUDE.md nel 2026: un modello pronto all’uso e 6 regole. Scopri le skill come capacità con un nome in Claude Code Skills: come creare la tua libreria.
Un hook è il livello più basso: niente modello, niente interpretazione, solo shell.
Gli hook sono uno degli strumenti di context engineering, insieme a Plan Mode e /compact.
4 tipi di hook: Command, HTTP, Prompt, Agent: quale scegliere?
Punto chiave
Claude Code ha 4 tipi di handler. Command è uno script shell, il tipo più comune. HTTP invia una richiesta POST al tuo URL, per team e integrazioni. Prompt esegue una breve chiamata Haiku per un controllo semantico. Agent crea un subagente con accesso agli strumenti per una convalida approfondita. Ai principianti bastano Command e Prompt; coprono il 90% delle attività.
| Tipo | Cosa fa | Quando sceglierlo | Complessità |
|---|---|---|---|
| Command | Esegue un comando shell, riceve JSON su stdin e risponde con un codice di uscita e stdout | Nel 90% dei casi: controllare percorsi, formattare, fare grep sui contenuti, lsof, operazioni git | bassa |
| HTTP | Invia una richiesta POST al tuo URL con un corpo JSON e riceve una risposta JSON | Quando una regola deve essere gestita dal team/CI: convalida centralizzata, dashboard di stato condivisa, audit remoto | media (richiede un server) |
| Prompt | Invia un breve prompt a Haiku (per impostazione predefinita) e ottiene sì/no per una valutazione semantica | Quando grep non basta: «valuta se questo testo ha un tono aziendale» | bassa |
| Agent | Crea un subagente con accesso a Read/Grep/Glob per controlli composti da più passaggi | Convalida impegnativa: «trova tutti i file modificati e controlla che esistano i test corrispondenti» | alta |
L’errore più comune è cercare di usare Command quando serve Prompt. Per esempio, «non usare trattini lunghi nei file .md» è un’attività da grep, quindi Command è l’ideale. Ma «non scrivere con un tono aziendale» è una questione semantica e richiede Prompt.
Se hai appena iniziato, usa Command. I 7 hook pronti all’uso qui sotto sono tutti di tipo Command. HTTP/Prompt/Agent verranno dopo, quando sarai pronto.
Dove si trova settings.json e quali sono i 4 livelli di configurazione?
Punto chiave
Claude Code cerca la configurazione in 4 posizioni: enterprise (distribuita tramite MDM), utente (~/.claude/settings.json), progetto (.claude/settings.json nella radice del repository) e locale (.claude/settings.local.json, escluso da git). All’inizio della sessione i 4 file vengono uniti; il livello più basso ha la precedenza.
| Livello | Percorso | Quando si applica | In git? |
|---|---|---|---|
| Enterprise | /Library/Application Support/ClaudeCode/settings.json (Mac), /etc/claude-code/settings.json (Linux) | Per tutto il team tramite MDM | no |
| Utente | ~/.claude/settings.json | Tutti i progetti di un utente | no |
| Progetto | .claude/settings.json nella radice del repository | Questo progetto, per tutto il team | sì |
| Locale | .claude/settings.local.json | Solo questo progetto e questo utente | no (in .gitignore) |
Tengo i 7 hook descritti qui sotto nel file .claude/settings.json del progetto, così vengono inclusi automaticamente quando si clona il repository e Claude applica le stesse regole in una nuova finestra. Gli hook sperimentali personali vanno in .claude/settings.local.json, così non interferiscono con il team.
Tutti e 4 i file vengono uniti all’inizio della sessione. Se le regole sono in conflitto, prevale il livello più basso (locale sul progetto, progetto sull’utente).
Il modo più rapido per creare il tuo primo hook è usare il menu interattivo /hooks direttamente in Claude Code. Scegli un evento dall’elenco, seleziona un matcher (per esempio, Bash o Edit) e incolla un comando. Claude Code scrive JSON valido in settings.json per te.
31 eventi del ciclo di vita: quali servono ai principianti?
Punto chiave
Attualmente Claude Code ha 31 eventi del ciclo di vita, suddivisi in 3 frequenze: una volta per sessione (SessionStart/SessionEnd), una volta per turno (UserPromptSubmit/Stop) e a ogni chiamata a uno strumento (PreToolUse/PostToolUse). Ai principianti servono 4 dei 31 eventi: PreToolUse per le restrizioni, PostToolUse per le modifiche automatiche, SessionStart per il contesto e Stop per i controlli finali.
Gli eventi si verificano con tre frequenze: una volta per sessione (SessionStart, SessionEnd), una volta per turno (UserPromptSubmit, Stop, StopFailure) e a ogni chiamata a uno strumento all’interno del ciclo agentico (PreToolUse, PostToolUse).
È importante comprendere le tre frequenze per non confondersi:
- Una volta per sessione -
SessionStart,SessionEnd,Setup. Vengono eseguiti una volta quando Claude Code si avvia e si chiude. Usali per inizializzare il contesto e ripulire i file temporanei. - Una volta per turno -
UserPromptSubmit,UserPromptExpansion,Stop,StopFailure. Si attivano per ciascuno dei tuoi messaggi e ogni volta che il modello si ferma. Usali per registrare la conversazione e controllare il risultato finale. - A ogni chiamata a uno strumento -
PreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest,PermissionDenied,PostToolBatch,SubagentStart,SubagentStop. Si attivano a ogni modifica di file, avvio di Bash, lettura o ricerca. È il livello più intenso, dove si trovano restrizioni, formattazione e convalide.
L’elenco completo dei 31 eventi include eventi asincroni/reattivi: TeammateIdle, PreCompact, PostCompact, Elicitation, ElicitationResult, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Notification. Sono destinati a scenari avanzati; per ora puoi ignorarli.
Test in cinque domande: quando una regola dovrebbe essere un hook e quando dovrebbe restare in CLAUDE.md?
Punto chiave
Se una regola riguarda un’azione (non il contesto), è legata alla sicurezza/al denaro/ai dati, si può verificare con grep/regex, è difficile rimediare se viene violata e Claude l’ha già violata almeno una volta, dovrebbe diventare un hook. Se meno di tre risposte su cinque sono «sì», resta in CLAUDE.md. Ho applicato il test alle mie 80 righe e 5 regole sono diventate hook.
Per me funziona come una legge, non come un suggerimento. Ho ridotto la scelta tra CLAUDE.md e un hook a un semplice test in cinque domande. Se almeno tre risposte sono «sì», la regola diventa un hook. Se sono meno di tre, resta nel testo.
Il test «quando una regola dovrebbe essere un hook?»:
- Questa regola riguarda un’azione anziché il contesto? («non eseguire
rm -rf» è un’azione; «ho un progetto TypeScript» è contesto.) - Questa regola riguarda la sicurezza, il denaro o i dati? («non toccare
.env», «non pubblicare chiavi».) - Puoi verificarla con grep, regex o il codice di uscita di un comando shell? («dopo aver modificato
.md, controlla che non ci siano trattini lunghi».) - Se la regola viene violata, le conseguenze sarebbero difficili da annullare? («force-push su main» è difficile da annullare; «aver dimenticato un punto alla fine» è facile.)
- Hai già visto Claude dimenticare questa regola almeno una volta?
Esempio: «Non usare trattini lunghi; usa solo trattini brevi». È un’azione? Sì. Sicurezza? No. Verificabile con grep? Sì. Difficile da annullare? No. L’hai già vista violata? Sì. 3 su 5: inseriscila in un hook.
Controesempio: «Faccio vibe coding, non sviluppo software; scrivi senza slang». È un’azione? Più simile a un contesto. Sicurezza? No. Verificabile con regex? Troppo difficile. Difficile da annullare? No. L’hai già vista violata? Sì. 1 su 5: lasciala in CLAUDE.md.
Cinque delle mie regole hanno superato il test. Le ho spostate negli hook. Il restante 75% è rimasto nel file di testo.
7 hook pronti da copiare
Punto chiave
Qui sotto trovi 7 hook pronti all’uso per .claude/settings.json: bloccare le modifiche a .env, eseguire prettier automaticamente, impedire il force-push su main, controllare i trattini lunghi, liberare una porta prima di npm run dev, eseguire il commit automatico dopo test riusciti e inserire contesto aggiornato all’inizio di una sessione. Copia ciascuno nel file, riavvia Claude Code e funzionerà.
Tutti e 7 gli hook sono archiviati in un unico file .claude/settings.json nella radice del progetto. Ogni voce contiene un tipo di evento, un filtro per lo strumento (matcher) e un comando shell. Exit 2 rifiuta una chiamata PreToolUse corrispondente prima dell’esecuzione. In PostToolUse l’azione è già avvenuta: l’hook fornisce feedback senza annullarla.
Per la maggior parte degli eventi hook, solo il codice di uscita 2 blocca un’azione. Claude Code considera il codice di uscita 1 un errore non bloccante e prosegue con l’azione, anche se in Unix 1 indica convenzionalmente un errore.
exit 0 significa successo. exit 1 significa errore, ma Claude prosegue (è il primo antipattern qui sotto). exit 2 dipende dall’evento: può bloccare PreToolUse, mentre PostToolUse riferisce soltanto su un’azione già avvenuta.
Hook 1. Controllare le modifiche dirette a .env (PreToolUse, exit 2)
Blocca i tentativi di modificare direttamente qualsiasi file che corrisponda a .env, .env.local, .env.production e così via.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Non modificare direttamente `.env`. Dimmi quale variabile aggiungere e aprirò io il file.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Cosa fa: prima di ogni Edit/Write/MultiEdit, recupera il percorso del file, controlla con grep se corrisponde al pattern .env e, in caso affermativo, restituisce exit 2 con un messaggio. Claude vede stderr e mi dice: «Ho provato a modificare .env, ma l’hook me l’ha impedito; dimmi quale variabile aggiungere».
Hook 2. Eseguire prettier automaticamente dopo ogni modifica al codice (PostToolUse, exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
}
]
}
]
}
}
Cosa fa: dopo ogni Edit/Write, recupera il percorso e, se l’estensione è .ts/.tsx/.js/.jsx/.json, esegue prettier in background con &. Esce sempre con 0 perché si tratta di formattazione, non di una restrizione. Se prettier non riesce, non è grave: il file resta com’era.
La differenza rispetto all’hook 1 è fondamentale: PreToolUse + exit 2 è un blocco rigido prima dell’azione. PostToolUse + exit 0 è una modifica leggera dopo l’azione. Sono due modalità diverse per attività diverse.
Hook 3. Bloccare git push --force sul branch main (PreToolUse, exit 2)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Il force-push su main è vietato. Crea un nuovo branch o chiedimi di farlo manualmente.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Cosa fa: a ogni avvio di Bash controlla che il comando contenga push, --force/--force-with-lease/-f e menzioni main. Se tutte e tre le condizioni corrispondono, blocca l’azione con un messaggio chiaro.
Prima avevo questa regola in maiuscolo in CLAUDE.md. Claude la violava comunque circa una volta ogni due settimane quando gli chiedevo di «fare force-push per annullare il mio commit sbagliato». L’hook riconosce le forme letterali mostrate, ma alias, script, branch impliciti o altri client Git possono sfuggire. Proteggi main sul remoto e limita le credenziali disponibili all’agente.
Hook 4. Bloccare i trattini lunghi nei file di testo (PostToolUse, exit 2)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"Il file $FILE contiene un trattino lungo (U+2014). Sostituiscilo con un trattino normale (U+002D).\" >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Cosa fa: PostToolUse con exit 2 segnala a Claude un controllo fallito dopo la modifica. Non annulla né impedisce la scrittura. Se Claude modifica un file e lascia un trattino lungo, l’hook fallisce, Claude riceve un feedback e ci riprova. Dopo 2–3 sessioni, Claude smette del tutto di inserire questo carattere.
Nota che grep cerca specificamente il carattere del trattino lungo (U+2014) usando $'\xe2\x80\x94', la notazione Bash per scriverlo come byte UTF-8. Se scrivi semplicemente grep -q '-', intercetterà i trattini normali e verrà eseguito su ogni file. Un carattere invisibile manda in errore tutta la logica.
Hook 5. Liberare la porta 3000 prima di npm run dev (PreToolUse, exit 0)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Porta 3000 liberata prima dell’avvio' >&2; }; exit 0"
}
]
}
]
}
}
Cosa fa: prima di ogni npm run dev (o pnpm/yarn), termina il processo sulla porta 3000. Esce sempre con 0 perché è una preparazione, non una restrizione. Questo evita una serie di situazioni in cui Claude vede «porta in uso», pensa che ci sia un problema nel codice e inizia a correggere cose, quando invece era semplicemente rimasto in esecuzione il vecchio server.
Hook 6. Eseguire il commit automatico dopo test riusciti (PostToolUse, exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "INPUT=$(cat); CMD=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty') || exit 1; EXIT=$(printf '%s' \"$INPUT\" | jq -r '.tool_response.exit_code // empty') || exit 1; case \"$CMD\" in 'npm test'|'pnpm test'|'yarn test') ;; *) exit 0 ;; esac; [ \"$EXIT\" = \"0\" ] || exit 0; git diff --cached --quiet || { echo 'Existing staged changes: review manually' >&2; exit 0; }; git add -- src/orders.ts tests/orders.test.ts || exit 1; git diff --cached --quiet && exit 0; git commit -m 'tests: checkpoint after successful run'"
}
]
}
]
}
}
Cosa fa: legge stdin JSON una volta e accetta solo i comandi di test semplici esatti mostrati. Prosegue solo se tool_response.exit_code è esplicitamente zero; se la versione non fornisce quel campo, salta il commit senza indovinare. Sostituisci src/orders.ts e tests/orders.test.ts con i file esatti di competenza del compito. Modifiche già in staging fermano la procedura per non includere lavoro estraneo. Un add fallito la interrompe; un diff vuoto non crea commit; i normali hook restano attivi. Prova offline input riusciti, falliti e senza risultato prima di attivarla.
Antipattern: aggiungere git commit --no-verify a ogni Edit. Mantieni i controlli del commit. Se un controllo attiva ricorsivamente l’automazione, restringi il trigger o riprogettalo senza saltare la verifica per impostazione predefinita. Esamina diff in staging e competenza sui file prima dei commit automatici.
Hook 7. Inserire contesto aggiornato all’inizio della sessione (SessionStart con additionalContext)
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Current branch: $BRANCH. Uncommitted changes:\\n$STATUS\\\"}\""
}
]
}
]
}
}
Cosa fa: all’inizio della sessione raccoglie il branch Git corrente e l’elenco delle modifiche non sottoposte a commit, poi restituisce JSON con un campo additionalContext. Claude Code inserisce questo testo nel primo messaggio di sistema, così Claude sa fin dall’inizio dove si trova e cosa è rimasto dalla sessione precedente.
Usa additionalContext per le informazioni che Claude dovrebbe conoscere sullo stato corrente dell’ambiente o su un’operazione appena completata: stato dell’ambiente (branch corrente, destinazione del deployment, feature flag attivi), regole condizionali del progetto (quale comando di test si applica al file appena modificato), dati esterni (issue aperte assegnate a te, risultati CI recenti).
Estensioni di questo hook: recuperare il commit più recente e la sua descrizione, lo stato di GitHub Actions, lo sprint attivo in Linear/Jira e le issue aperte del repository corrente.
3 insidie degli hook che colgono una persona su due
Punto chiave
La prima insidia: un hook con exit 1 passa senza segnalazioni e Claude ignora l’errore. La seconda: il lavoro pesante in un hook viene interrotto dopo un timeout di 60 secondi. La terza: un hook vede solo le variabili d’ambiente di ~/.zshrc, non quelle del .env del progetto. Ognuna si risolve con una riga, ma senza conoscere queste insidie può sembrare che «gli hook non funzionino».
Insidia 1. Un hook con exit 1 passa senza segnalazioni
È la più pericolosa. Per una settimana ho pensato che il mio protettore di .env funzionasse, ma poi ho scoperto che nell’hook avevo jq -r '.tool_input.file_path' senza un controllo per null. Quando tool_input era vuoto, jq usciva con codice 1. Claude interpretava exit 1 come «errore dell’hook, continua», quindi l’azione veniva eseguita. Non capivo perché .env venisse comunque toccato. Ho individuato la causa solo quando ho eseguito manualmente l’hook in un terminale con input vuoto.
La soluzione è una sola regola: inizia ogni hook controllando null e termina con || exit 0. Qualsiasi uscita imprevista produrrà così un’uscita «sicura» anziché passare senza segnalazioni.
# Modello per un inizio e una fine sicuri:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... la tua logica ...
exit 0 # oppure || exit 0 su una sola riga
Insidia 2. Gli hook lenti vengono interrotti per timeout
Avevo un hook che eseguiva eslint --fix sull’intero file dopo Edit. I file grandi (1000+ righe) richiedevano 8–10 secondi per essere elaborati. Il timeout predefinito degli hook di Claude Code è 60 secondi, quindi pensavo di rientrare nei limiti. Ma quando avvengono diverse modifiche in un minuto, gli hook si accumulano e si mettono in coda, e alcuni vengono semplicemente saltati. Me ne sono accorto solo quando nel log ho visto «hook timed out».
Soluzione: se un hook richiede un lavoro pesante, eseguilo in background. eslint --fix "$FILE" & senza aspettare il risultato. Così l’hook restituisce exit 0 in pochi millisecondi mentre il lavoro effettivo prosegue in background.
# Operazione pesante in background:
npx eslint --fix "$FILE" &
exit 0
Insidia 3. Un hook non può vedere le variabili d’ambiente del progetto
Pensavo che process.env fosse disponibile in un hook come in un’app Node.js. Non è così. L’hook viene eseguito in una shell avviata dal processo genitore di Claude Code e vede solo ciò che si trova in ~/.zshrc o ~/.bashrc, oltre a ciò che Claude Code passa esplicitamente. Se l’hook dipende da OPENAI_API_KEY contenuta in .env, dovrai leggere .env manualmente.
Soluzione: leggi esplicitamente .env nel comando dell’hook.
# Passare l’ambiente del progetto all’hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Oppure con dotenv:
dotenv -e .env -- node my-validator.js
È particolarmente problematico in un monorepo, dove pacchetti diversi hanno i propri file .env. Risolvi passando PWD e avviando esplicitamente il comando con cd "$PWD".
Come si testano gli hook prima del commit?
Punto chiave
Un hook è un comando shell. Puoi eseguirlo manualmente in un terminale con uno stdin fittizio, controllarne il codice di uscita e stderr e fare tutto questo prima che venga eseguito da Claude Code. In questo modo risparmi ore di debug ed eviti hook che bloccano la propria operatività.
Scenario di test di base. Prendi il comando dell’hook dal JSON, apri un terminale e passagli il JSON che Claude Code invierebbe:
# Verifica hook 1 (proteggi .env):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
echo "BLOCKED" >&2
exit 2
} || exit 0
'
echo "Exit code: $?"
Dovresti vedere BLOCKED in stderr e Exit code: 2. Se ottieni exit 0, la tua regex non funziona; correggi grep.
Scenari da controllare:
- Percorso positivo - il file dovrebbe essere bloccato.
.envper l’hook 1,git push --force mainper l’hook 3. - Percorso negativo - il file non dovrebbe essere bloccato.
README.mdper l’hook 1,git push origin feature-branchper l’hook 3. - Casi limite - input vuoto, valori null, caratteri imprevisti. L’hook dovrebbe restituire exit 0 o fallire con un errore chiaro, non passare senza segnalazioni.
Dove mettere gli hook sperimentali. Non in .claude/settings.json, ma in .claude/settings.local.json (non in git). Inserisci lì i nuovi hook sotto PostToolUse usando echo al posto dell’azione reale, controlla che si attivino e poi spostali in settings.json di produzione.
// Esperimento in settings.local.json: scrive in /tmp/hook-debug.log
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
}
]
}
]
}
}
Dopo 5–10 modifiche in Claude Code, apri /tmp/hook-debug.log per vedere quali file hanno attivato l’hook. Se è vuoto, il matcher non corrisponde; correggi la regex.
E adesso?
Punto chiave
Inizia con hook PostToolUse che restituiscono exit 0: sono i più sicuri e non bloccano nulla. Dopo una settimana, aggiungi il tuo primo hook PreToolUse con exit 2 per proteggere .env o impedire il force-push. Dopo due settimane, CLAUDE.md passerà da un contratto di 200 righe a una breve descrizione del contesto di 50–80 righe, con controlli di eventi selezionati negli hook e limiti rigidi separati tramite permessi, sandbox e protezioni remote.
5 passaggi per la prossima settimana:
- Cerca con grep nel tuo
CLAUDE.mdparole come «mai», «vietato», «sempre» e «deve». Ognuna di queste regole può diventare un hook. Se una regola è imperativa, il file di testo non la farà rispettare. - Inizia con PostToolUse + exit 0. Sono i più sicuri: apportano una modifica senza bloccare nulla. Prettier (hook 2), terminare il server di sviluppo (hook 5), commit automatico dopo i test (hook 6).
- Solo quando ti senti sicuro, aggiungi il tuo primo PreToolUse + exit 2. Per esempio, proteggi
.env(hook 1) o impedisci il force-push (hook 3). È un blocco rigido e un errore nel comando grep può bloccare anche te. - Testa gli hook manualmente prima del commit (vedi la sezione qui sopra). Ti risparmierà ore di debug.
- Tieni
settings.local.jsonper gli esperimenti. Inserisci lì i nuovi hook usando echo al posto dell’azione reale.
Una volta configurati gli hook, il prossimo passaggio in questo cluster è come annullare le modifiche in Claude Code, nel caso in cui un hook lasci passare qualcosa di pericoloso. Se vuoi che Claude ricordi la struttura del tuo progetto tra una sessione e l’altra, crea un Second Brain in Claude Code (cartella business/ + knowledge base collegata). E per dare un nome alle skill del tuo progetto così da poterle riutilizzare, consulta Claude Code Skills.
Fonti
Se ti serve accedere alle reti neurali tramite Ailoot, dai un’occhiata ad Ailoot.
Maggiori dettagli
Domande frequenti
Perché un hook deve leggere il JSON di input una sola volta?
Stdin è un flusso: il primo jq lo consuma e le letture successive trovano EOF. Salva l’input una volta, poi estrai comando e risultato da quel valore. Un esito assente o sconosciuto resta non verificato: salta il commit automatico.
Le regex degli esempi creano un confine di sicurezza?
No. Controllano eventi specifici e stringhe riconoscibili. Redirezioni shell, alias, script, MCP o altri percorsi possono eluderle: per limiti forzati usa credenziali ristrette, permessi sui file, sandbox e branch protetti.