10 · Hooks e automazioni locali
Un hook (“gancio”) è uno script tuo che Claude Code esegue automaticamente a
un momento preciso: prima di ogni comando, dopo ogni modifica a un file, a fine risposta. L’analogia
giusta è l’antifurto: non devi ricordarti di attivarlo — scatta da solo, ogni volta, quando succede la
cosa per cui l’hai montato. È il salto di qualità rispetto a tutto quello che hai visto finora: le
istruzioni nel CLAUDE.md sono consigli che Claude quasi sempre segue; un hook è una regola che
viene applicata sempre, perché non passa dal giudizio del modello. Formattare il codice dopo ogni
modifica, bloccare i comandi pericolosi, ricevere una notifica quando un task lungo finisce: tutte
cose da hook.
Se parti da zeroQuesto modulo è il più 'da programmatori' del corso
Gli hook sono script di shell, e il modulo li mostra riga per riga con i commenti. Se bash non è il tuo pane: leggi i commenti (dicono cosa fa ogni riga), copia i blocchi così come sono, e — soprattutto — sappi che al primo passo dell’esempio guidato c’è la scorciatoia legittima: farsi scrivere gli hook da Claude stesso. Capire cosa vuoi automatizzare conta più che saper scrivere lo script.
Obiettivi
Sezione intitolata “Obiettivi”- Capire quando un’istruzione va scritta come hook invece che nel
CLAUDE.md. - Scegliere l’evento giusto tra quelli disponibili.
- Scrivere un hook che blocca davvero un’azione, usando il contratto di exit code e JSON.
- Automatizzare lint e test dopo ogni modifica, e ricevere una notifica a fine turno.
- Diagnosticare un hook che non scatta, senza tirare a indovinare.
Concetti chiave
Sezione intitolata “Concetti chiave”Gli hook sono deterministici, il CLAUDE.md no. È la ragione per cui esistono. Le istruzioni nel
CLAUDE.md sono contesto: Claude le legge e cerca di seguirle, ma può ignorarle. Un hook è codice
che esegue a un punto fisso del ciclo di vita, indipendentemente da cosa decide Claude.
La regola di scelta è netta: se una cosa deve succedere ogni volta, senza eccezioni, è un hook.
Girano come processi, non come contesto. Un hook non occupa la finestra di contesto — con
un’eccezione importante: può iniettarci dentro del testo, tramite additionalContext o lo stdout su
certi eventi.
Cinque tipi di handler. Un hook non è per forza uno script di shell:
| Tipo | Cosa fa | Timeout di default |
|---|---|---|
command |
Esegue un comando locale | 600 s |
http |
Chiama un endpoint HTTP | 600 s |
mcp_tool |
Invoca un tool di un server MCP | 600 s |
prompt |
Manda un prompt a un modello | 30 s |
agent |
Lancia un agente di validazione | 60 s |
Il contratto è l’exit code più, opzionalmente, del JSON su stdout. Exit 0 = successo, exit 2 =
errore bloccante, qualunque altro = errore non bloccante. È il meccanismo con cui un hook dice “no”.
Gli eventi
Sezione intitolata “Gli eventi”Non ti serviranno tutti. Questi sono quelli che coprono il 95% dei casi reali:
| Evento | Quando scatta | Blocca con exit 2? |
|---|---|---|
PreToolUse |
Prima che una chiamata a tool venga eseguita | Sì — blocca la chiamata |
PostToolUse |
Dopo che una chiamata a tool è riuscita | No — mostra stderr a Claude |
PostToolUseFailure |
Dopo che una chiamata a tool è fallita | No |
UserPromptSubmit |
Quando invii un prompt, prima che Claude lo elabori | Sì — rifiuta il prompt |
Stop |
Quando Claude finisce di rispondere | Sì — fa continuare la conversazione |
SessionStart |
All’inizio o alla ripresa di una sessione | No |
SessionEnd |
Alla chiusura della sessione | No |
Notification |
Quando Claude Code manda una notifica | No |
SubagentStart / SubagentStop |
Avvio e fine di un subagent | SubagentStop: sì |
PreCompact / PostCompact |
Prima e dopo la compattazione del contesto | No |
FileChanged |
Quando un file sorvegliato cambia su disco | No |
InstructionsLoaded |
Quando viene caricato un CLAUDE.md o una regola |
No |
PermissionRequest |
Quando una chiamata a tool richiede una decisione sui permessi | No — usa il campo JSON decision |
PermissionDenied |
Quando la modalità auto nega una chiamata | No |
Gli altri 16 eventi, per completezza (apri solo se ti servono)
| Evento | Quando scatta |
|---|---|
Setup |
Nei run non interattivi avviati con i flag di inizializzazione |
UserPromptExpansion |
Quando un comando slash viene espanso nel prompt |
PostToolBatch |
Dopo un gruppo di chiamate a tool eseguite in parallelo |
StopFailure |
Quando un turno termina per un errore dell’API |
TaskCreated / TaskCompleted |
Quando un task della lista condivisa viene creato / completato |
TeammateIdle |
Quando un membro di un agent team sta per fermarsi |
MessageDisplay |
Mentre il testo di una risposta viene mostrato |
ConfigChange |
Quando un file di configurazione cambia durante la sessione |
CwdChanged |
Quando la directory di lavoro cambia |
DirectoryAdded |
Quando aggiungi una directory con /add-dir |
WorktreeCreate / WorktreeRemove |
Alla creazione / rimozione di un worktree |
Elicitation / ElicitationResult |
Quando un server MCP chiede un input all’utente / riceve la risposta |
flowchart TD
S["SessionStart"] --> U["UserPromptSubmit"]
U -->|"exit 2 → prompt rifiutato"| X1["bloccato"]
U --> PRE["PreToolUse"]
PRE -->|"exit 2 → tool bloccato"| X2["bloccato"]
PRE --> T["Esecuzione del tool"]
T -->|successo| POST["PostToolUse"]
T -->|errore| POSTF["PostToolUseFailure"]
POST --> LOOP{"altre azioni?"}
POSTF --> LOOP
LOOP -->|sì| PRE
LOOP -->|no| ST["Stop"]
ST -->|"exit 2 → Claude continua"| PRE
ST --> E["SessionEnd"]
classDef block fill:#7a2d2d,stroke:#c25b5b,color:#fff
classDef hook fill:#1d4e6f,stroke:#4a9ecb,color:#fff
class X1,X2 block
class S,U,PRE,POST,POSTF,ST,E hook
Dove si configurano
Sezione intitolata “Dove si configurano”| Posizione | Portata | Condivisibile |
|---|---|---|
~/.claude/settings.json |
Tutti i progetti | No |
.claude/settings.json |
Un progetto | Sì, si committa |
.claude/settings.local.json |
Un progetto | No, gitignored |
| Managed policy settings | Tutta l’organizzazione | Sì, la controlla l’admin |
hooks/hooks.json di un plugin |
Dove il plugin è abilitato | Sì |
| Frontmatter di skill e subagent | Durata della sessione / del subagent | Sì |
Il formato di configurazione va letto dall’esterno verso l’interno, e la struttura — che a prima
vista confonde, con hooks che compare due volte — ha una logica: per ogni evento (PreToolUse)
puoi avere più gruppi, ognuno con il suo filtro (matcher: a quali tool si applica), e ogni
gruppo può eseguire più script (la lista hooks interna). Quindi: evento → gruppi filtrati →
script da eseguire.
{ "hooks": { "PreToolUse": [ // ← l'evento: prima di ogni chiamata a tool... { "matcher": "Bash", // ← ...se il tool è Bash... "hooks": [ // ← ...esegui questi script: { "type": "command", "if": "Bash(rm *)", // filtro fine: solo comandi che iniziano con "rm" "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh", "timeout": 30 // secondi a disposizione dello script } ] } ] }}(I commenti // qui sono solo didattici: il JSON vero non li ammette — toglili se copi a mano.
I blocchi degli esempi più sotto sono già pronti da copiare.)
I matcher
Sezione intitolata “I matcher”| Matcher | Effetto |
|---|---|
"*", "" o omesso |
Corrisponde a tutto |
"Bash" |
Corrispondenza esatta |
"Edit|Write" |
Elenco di alternative |
"^Notebook.*" |
Regex JavaScript non ancorata |
"mcp__memory__.*" |
Tutti i tool di un server MCP |
La regola di interpretazione: solo lettere, cifre, _, -, spazi, , e | → stringa esatta o elenco.
Qualsiasi altro carattere → regex.
Cosa viene confrontato dipende dall’evento: sugli eventi dei tool, il tool_name; su SessionStart,
il motivo di avvio (startup, resume, clear, compact, fork); su Notification, il tipo; su
SubagentStart/SubagentStop, il tipo di agente; su FileChanged, i nomi di file letterali da
sorvegliare.
Il campo if
Sezione intitolata “Il campo if”Filtra ulteriormente con una regola di permesso, così lo script parte solo quando serve davvero:
{ "type": "command", "if": "Bash(git push *)", "command": "..." }{ "type": "command", "if": "Edit(*.ts)", "command": "..." }Segnaposto di percorso
Sezione intitolata “Segnaposto di percorso”| Segnaposto | Cosa punta |
|---|---|
${CLAUDE_PROJECT_DIR} |
Root del progetto |
${CLAUDE_PLUGIN_ROOT} |
Directory di installazione del plugin |
${CLAUDE_PLUGIN_DATA} |
Directory dati persistenti del plugin |
Usali sempre invece dei percorsi assoluti: un hook con /Users/tuonome/... dentro si rompe per chiunque
altro.
Il contratto di output
Sezione intitolata “Il contratto di output”Exit code
Sezione intitolata “Exit code”| Exit | Significato |
|---|---|
0 |
Successo. Se stdout inizia con {, viene letto come JSON. Su UserPromptSubmit, UserPromptExpansion e SessionStart, lo stdout viene aggiunto al contesto che Claude vede; sugli altri eventi finisce solo nel log di debug |
2 |
Errore bloccante. Il messaggio di blocco è la reason dal JSON o il testo su stderr |
| altri | Errore non bloccante. Con JSON valido la decisione viene onorata; con JSON invalido l’azione procede e viene mostrato <nome> hook error con la prima riga di stderr |
Output JSON
Sezione intitolata “Output JSON”{ "continue": true, "stopReason": "Hook stopped processing", "systemMessage": "Messaggio di avviso mostrato all'utente", "additionalContext": "Testo aggiunto al contesto di Claude", "decision": "allow", "reason": "Spiegazione", "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Violazione della policy di sicurezza" }}| Campo | Cosa fa |
|---|---|
continue: false |
Ferma completamente l’elaborazione dopo l’hook |
stopReason |
Messaggio mostrato quando continue: false |
systemMessage |
Avviso mostrato a te |
additionalContext |
Testo aggiunto al contesto di Claude |
decision |
"allow" o "deny" |
hookSpecificOutput |
Oggetto annidato con hookEventName più i campi specifici dell’evento |
Ogni hook riceve su stdin un JSON con i campi comuni:
{ "session_id": "abc123", "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl", "cwd": "/home/user/my-project", "permission_mode": "default", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "rm -rf build/" }}Dentro un subagent ci sono anche agent_id e agent_type.
Esempio guidato
Sezione intitolata “Esempio guidato”Tre automazioni che si usano davvero, dalla più semplice alla più utile. E prima di tutto, la scorciatoia legittima:
-
Prepara la cartella degli hook.
Terminal window mkdir -p .claude/hooksTerminal window New-Item -ItemType Directory -Force .claude\hooks -
Lint automatico dopo ogni modifica. L’automazione più richiesta, e la più semplice.
.claude/settings.json {"hooks": {"PostToolUse": [{"matcher": "Edit|Write","hooks": [{"type": "command","if": "Edit(*.{ts,tsx,js,jsx})","command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/lint.sh","timeout": 60}]}]}}.claude/hooks/lint.sh #!/usr/bin/env bash# set -euo pipefail = modalità "prudente" di bash: fermati al primo# errore (-e), tratta le variabili inesistenti come errori (-u), e non# nascondere i fallimenti dentro le pipe (pipefail)set -euo pipefail# Claude Code passa all'hook un JSON su stdin; jq estrae il percorso# del file modificato ("// empty" = se manca, stringa vuota)FILE=$(jq -r '.tool_input.file_path // empty')# nessun file nel payload? niente da fare: esci senza errore[ -z "$FILE" ] && exit 0# Formatta in silenzio, poi controlla. Se eslint trova problemi,# li rimandiamo a Claude su stderr con exit 2: su PostToolUse non# blocca nulla, ma Claude li legge e li corregge da solo.npx prettier --write "$FILE" >/dev/null 2>&1 || trueif ! OUTPUT=$(npx eslint "$FILE" 2>&1); thenecho "$OUTPUT" >&2exit 2fiTerminal window chmod +x .claude/hooks/lint.shLa sottigliezza sta nell’exit 2 su
PostToolUse: non blocca — il tool è già stato eseguito — ma mostra lo stderr a Claude, che vede gli errori di lint e li corregge senza che tu debba dire nulla. -
Bloccare i comandi distruttivi. Qui l’exit code conta davvero.
.claude/settings.json {"hooks": {"PreToolUse": [{"matcher": "Bash","hooks": [{"type": "command","if": "Bash(rm *)","command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"}]}]}}.claude/hooks/block-rm.sh #!/usr/bin/env bashCOMMAND=$(jq -r '.tool_input.command')if echo "$COMMAND" | grep -q 'rm -rf'; thenjq -n '{hookSpecificOutput: {hookEventName: "PreToolUse",permissionDecision: "deny",permissionDecisionReason: "Comando distruttivo bloccato da un hook di progetto"}}'elseexit 0 # nessuna decisione: si applica il normale flusso dei permessifiIl JSON qui è diverso da quello macOS, e non per errore: su Windows Claude può eseguire i comandi sia col tool Bash (se hai Git for Windows) sia col tool PowerShell, quindi il
matchercopre entrambi, e servono due vociif— una per la sintassi di cancellazione di ciascuna shell (rmeRemove-Item)..claude/settings.json {"hooks": {"PreToolUse": [{"matcher": "Bash|PowerShell","hooks": [{"type": "command","if": "Bash(rm *)","command": "powershell.exe","args": ["-NoProfile", "-ExecutionPolicy", "Bypass","-File", "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"]},{"type": "command","if": "PowerShell(Remove-Item *)","command": "powershell.exe","args": ["-NoProfile", "-ExecutionPolicy", "Bypass","-File", "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"]}]}]}}.claude/hooks/block-rm.ps1 $callInput = [Console]::In.ReadToEnd() | ConvertFrom-Json$command = $callInput.tool_input.commandif ($command -match 'rm -rf|Remove-Item.*-Recurse') {@{hookSpecificOutput = @{hookEventName = "PreToolUse"permissionDecision = "deny"permissionDecisionReason = "Comando distruttivo bloccato da un hook di progetto"}} | ConvertTo-Json} else {exit 0}Nota il ramo
else: uscire con0senza stampare JSON significa “nessuna decisione”, e si applica il normale flusso dei permessi. Non significa “permetti”.E come per
lint.shal passo 2, su macOS/Linux lo script va reso eseguibile:chmod +x .claude/hooks/block-rm.sh— senza, l’hook non scatta e non ti avvisa. -
Notifica a fine turno. Utile quando lanci un task lungo e vai a fare altro.
~/.claude/settings.json {"hooks": {"Stop": [{"hooks": [{"type": "command","command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/notify.sh","async": true}]}]}}.claude/hooks/notify.sh #!/usr/bin/env bash# prende il nome della cartella di lavoro dal JSON dell'evento,# per usarlo come titolo della notificaPROGETTO=$(basename "$(jq -r '.cwd')")# osascript esegue AppleScript: è il modo standard di mostrare# una notifica di sistema su macOS (con suono "Glass")osascript -e "display notification \"Claude ha finito\" with title \"$PROGETTO\" sound name \"Glass\"".claude/hooks/notify.ps1 # legge il JSON dell'evento da stdin e lo trasforma in oggetto$callInput = [Console]::In.ReadToEnd() | ConvertFrom-Json# il nome della cartella di lavoro fara da titolo$progetto = Split-Path -Leaf $callInput.cwd# Windows non ha un comando semplice per le notifiche: si passa# dalle librerie .NET (per questo le righe che seguono sono verbose)[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null$balloon = New-Object System.Windows.Forms.NotifyIcon$balloon.Icon = [System.Drawing.SystemIcons]::Information$balloon.BalloonTipTitle = $progetto$balloon.BalloonTipText = 'Claude ha finito'$balloon.Visible = $true# mostra il fumetto per 5000 millisecondi (5 secondi)$balloon.ShowBalloonTip(5000)"async": truefa sì che il turno non aspetti la notifica. Anche qui, su macOS:chmod +x .claude/hooks/notify.sh. -
Verifica cosa è configurato.
/hooksApre un browser in sola lettura di tutti gli hook, per evento, matcher, tipo e file di origine. È il primo posto in cui guardare quando qualcosa non scatta.
Hook nelle skill e nei subagent
Sezione intitolata “Hook nelle skill e nei subagent”Un hook può essere dichiarato anche nel frontmatter di una skill o di un subagent, invece che nei settings. Attenzione: la sintassi è identica nei due casi — quello che cambia è quanto a lungo l’hook resta attivo, e dipende solo da dove lo dichiari:
| Dichiarato in | L’hook vive… |
|---|---|
| Frontmatter di una skill | Da quando la skill viene invocata fino alla fine della sessione |
| Frontmatter di un subagent | Solo mentre il subagent gira: rimosso quando finisce |
La forma, uguale per entrambi:
---name: secure-operationshooks: PreToolUse: - matcher: "Bash" hooks: - type: command command: "./scripts/security-check.sh"---Nel frontmatter delle skill è disponibile anche l’opzione once: true, che esegue l’hook una volta per
sessione e poi lo rimuove.
Timeout
Sezione intitolata “Timeout”I default per tipo di handler sono nella tabella dei Concetti chiave (600 s per
command/http/mcp_tool, 30 s per prompt, 60 s per agent). Alcuni eventi li abbassano:
UserPromptSubmit a 30 s, MessageDisplay a 10 s.
SessionEnd è un caso a parte, e la regola va letta con attenzione: di norma tutti i suoi hook
insieme hanno 1,5 secondi (la sessione sta chiudendo, non può aspettare); ma se a un hook hai dato
esplicitamente un timeout più alto, il budget si allarga fino a quel valore, con un tetto di 60 s.
Allo scadere del timeout l’hook viene annullato e l’output scartato. Su PreToolUse non blocca: si
prosegue con il normale flusso dei permessi.
Disattivare gli hook
Sezione intitolata “Disattivare gli hook”{ "disableAllHooks": true}Per un singolo run:
claude --settings '{"disableAllHooks": true}'disableAllHooks nelle impostazioni utente, di progetto o locali non può disattivare gli hook di
managed policy: solo un disableAllHooks: true a livello managed lo fa.
Esercizio
Sezione intitolata “Esercizio”Costruisci una rete di sicurezza per un progetto reale.
- Un hook che impedisce a Claude di modificare i file di migrazione del database.
- Un hook che lancia i test dopo ogni modifica al codice sorgente e riporta i fallimenti a Claude.
- Un hook che inietta all’avvio della sessione il branch corrente e lo stato di CI.
- Verifica che tutti e tre scattino davvero.
Soluzione
{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-migrations.sh" } ] } ], "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "if": "Edit(src/**)", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/test.sh", "timeout": 300 } ] } ], "SessionStart": [ { "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/context.sh" } ] } ] }}1. Proteggere le migrazioni.
#!/usr/bin/env bashFILE=$(jq -r '.tool_input.file_path // empty')
case "$FILE" in */migrations/*|*/migrate/*) jq -n --arg f "$FILE" '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: ("Le migrazioni non si modificano dopo essere state applicate. " + "Per cambiare lo schema, crea una nuova migrazione. File rifiutato: " + $f) } }' ;; *) exit 0 ;;esacLa permissionDecisionReason non è decorativa: Claude la legge. Scriverci cosa fare invece — “crea
una nuova migrazione” — trasforma un blocco in un’indicazione, e Claude di solito corregge da solo al
tentativo successivo.
2. Test dopo ogni modifica.
#!/usr/bin/env bash# Nota: qui manca la "-e" (fermati al primo errore) che c'era in lint.sh,# ed e VOLUTO: i test che falliscono fanno "fallire" npm, ma vogliamo# gestire noi quel fallimento (leggerne l'output e rimandarlo a Claude),# non far morire lo script a meta.set -uo pipefail
FILE=$(jq -r '.tool_input.file_path // empty')[ -z "$FILE" ] && exit 0
if ! OUTPUT=$(npm test --silent 2>&1); then # Solo la coda: l'output completo di un test runner inonderebbe il contesto. echo "$OUTPUT" | tail -40 >&2 exit 2fiDue scelte deliberate: if: "Edit(src/**)" nella config, perché far girare la suite dopo ogni modifica a
un .md sarebbe assurdo; e il tail -40, perché tutto ciò che passa da stderr con exit 2 finisce nel
contesto di Claude.
3. Contesto all’avvio.
Nota il prerequisito in più di questo hook: la terza riga usa gh, la CLI di GitHub, che deve essere
installata e autenticata (gh auth login) — altrimenti quella voce risulterà “sconosciuto”, senza
rompere nulla (è il senso dei vari || echo ...: “se il comando fallisce, usa questo ripiego”).
#!/usr/bin/env bash# il nome del branch corrente (o "non-git" fuori da un repository)BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "non-git")# quanti file risultano modificati: git status in formato compatto,# contato con wc -l (tr toglie gli spazi dal numero)SPORCO=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')# esito dell'ultimo run di CI su GitHub, letto con ghCI=$(gh run list --limit 1 --json conclusion --jq '.[0].conclusion' 2>/dev/null || echo "sconosciuto")
# costruisce il JSON di risposta: additionalContext è il testo che# Claude Code inietta nel contesto della sessione appena avviatajq -n --arg b "$BRANCH" --arg s "$SPORCO" --arg c "$CI" '{ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: ("Branch: " + $b + " · file modificati: " + $s + " · ultimo run CI: " + $c) }}'Su SessionStart anche il semplice stdout finirebbe in contesto, ma additionalContext è esplicito e
funziona su più eventi: meglio abituarcisi.
4. Verificare che scattino.
chmod +x .claude/hooks/*.shPoi, in ordine:
/hooksDevono comparire tutti e tre, con il file di origine giusto. Se un hook non è in elenco, il problema è il JSON dei settings, non lo script.
Test funzionali:
| Hook | Come provarlo | Cosa deve succedere |
|---|---|---|
protect-migrations |
chiedi a Claude: "aggiungi una colonna in migrations/001_init.sql" |
Blocco, con la ragione che suggerisce una nuova migrazione |
test |
Rompi un test, poi fai modificare un file in src/ |
Claude vede il fallimento e lo corregge da solo |
context |
Avvia una sessione nuova e chiedi "su che branch siamo?" |
Risponde senza eseguire git |
Se uno script non parte, il sospetto numero uno è il bit di esecuzione — il “permesso di essere
eseguito” che ogni file deve avere e che si dà con chmod +x (per questo il passo inizia proprio da
lì); il numero due è jq non installato. Lancia con --debug per vedere l’errore vero.
Errori comuni e troubleshooting
Sezione intitolata “Errori comuni e troubleshooting”| Sintomo | Causa | Soluzione |
|---|---|---|
L’hook non compare in /hooks |
JSON dei settings malformato o chiave sbagliata | claude doctor valida i file di settings |
| L’hook compare ma non esegue | Manca il bit di esecuzione sullo script | chmod +x .claude/hooks/*.sh |
jq: command not found |
jq non installato |
Installalo, o parsa il JSON con il linguaggio che preferisci |
| L’hook funziona per te e non per i colleghi | Percorso assoluto della tua macchina | Usa ${CLAUDE_PROJECT_DIR} |
Exit 2 su PostToolUse non blocca nulla |
Su PostToolUse l’exit 2 non blocca: il tool è già stato eseguito |
Per bloccare serve PreToolUse |
Un permissionDecision: "allow" viene ignorato |
Lo script è uscito con 2 | Exit 2 non è sovrascrivibile: esci con 0 per permettere |
| Un hook che esce con 0 senza output “permette” tutto | Uscire con 0 senza JSON significa nessuna decisione | Si applica il normale flusso dei permessi |
| L’output dell’hook non arriva a Claude | Su eventi diversi da UserPromptSubmit/SessionStart, lo stdout va solo nel log di debug |
Usa additionalContext nel JSON, o stderr con exit 2 |
| L’hook rallenta ogni turno | Timeout alto e hook sincrono | "async": true dove il risultato non serve subito |
Un hook su SessionEnd non completa mai |
Budget condiviso di 1,5 secondi | Alza il timeout individuale, fino a 60 s |
| Un matcher regex non corrisponde | La regex è non ancorata e distingue i caratteri speciali | Solo lettere, cifre, _, -, spazi, ,, | valgono come stringa esatta |
| Non riesci a disattivare un hook | È di managed policy | Solo un disableAllHooks a livello managed lo disattiva |
| Il contesto si riempie di output degli hook | Uno script stampa troppo su stderr con exit 2 | Tronca l’output — tail -40 o simili |
Cheat-sheet
Sezione intitolata “Cheat-sheet”Struttura minima
{ "hooks": { "<Evento>": [ { "matcher": "<pattern>", "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/x.sh" } ] } ] }}Gli eventi da sapere
| Evento | Uso tipico |
|---|---|
PreToolUse |
Bloccare azioni pericolose |
PostToolUse |
Lint, format, test dopo le modifiche |
UserPromptSubmit |
Validare o arricchire il prompt |
SessionStart |
Iniettare contesto: branch, stato CI, ambiente |
Stop |
Notifiche, gate di verifica a fine turno |
Notification |
Reagire alle notifiche di Claude Code |
Exit code
0 → successo. stdout letto come JSON se inizia con {2 → blocco. La reason viene da JSON o stderr* → errore non bloccanteCampi JSON utili
| Campo | |
|---|---|
hookSpecificOutput.permissionDecision |
"deny" per bloccare su PreToolUse |
hookSpecificOutput.permissionDecisionReason |
Claude la legge: scrivici cosa fare invece |
additionalContext |
Testo iniettato nel contesto di Claude |
systemMessage |
Avviso mostrato a te |
continue: false |
Ferma tutta l’elaborazione |
Comandi
/hooks |
Browser in sola lettura degli hook configurati |
claude doctor |
Valida i file di settings |
claude --debug |
Mostra gli errori di esecuzione degli hook |
claude --settings '{"disableAllHooks": true}' |
Disattiva tutto per un run |
Pagine ufficiali consultate, verificate il 16 agosto 2026:
- Hooks reference — eventi, campi di input, formato di configurazione, contratto di exit code e JSON, timeout
- Get started with hooks — guida introduttiva ed esempi
- Settings — dove si configurano gli hook e
disableAllHooks - Extend Claude with skills — hook nel frontmatter delle skill e opzione
once - Subagents — hook con durata limitata al subagent
- Debug your configuration — diagnosi di hook che non scattano
Corso indipendente, non affiliato ad Anthropic o Google. Claude e Claude Code sono marchi di Anthropic; Antigravity e Gemini sono marchi di Google.