Hook di Claude Code: eventi, configurazione e sicurezza
Scopri dove si configurano gli hook di Claude Code, quali eventi li attivano e perché il codice di uscita 2 può annullare una chiamata allo strumento.
Che cos'è un hook di Claude Code
Gli hook di Claude Code sono comandi shell che Claude Code esegue automaticamente in punti prestabiliti del proprio ciclo di vita. Questa è l'unica differenza tra un hook e un file di regole. Un'istruzione in CLAUDE.md è un suggerimento e il modello la valuta insieme a tutto il resto del contesto. Un hook è codice e viene eseguito indipendentemente dal fatto che il modello lo approvi o meno. Se l'agente continua a ignorare il formatter che gli hai indicato due volte, non ti serve un'istruzione più rigida. Ti serve un hook.
Il meccanismo è semplice. Si registra un comando in un file di configurazione, associandolo al nome di un evento. Quando l'evento viene generato, Claude Code esegue il comando e scrive i dati dell'evento nel suo standard input (stdin) in formato JSON (JavaScript object notation). Il comando legge i dati, esegue le operazioni necessarie e restituisce uno stato di uscita. Il codice di uscita 2 di un hook PreToolUse annulla la chiamata allo strumento prima che venga eseguita. Qualsiasi contenuto scritto dallo script sullo standard error (stderr) viene restituito al modello come motivazione.
I nomi degli eventi e dei campi riportati qui provengono dalla documentazione di riferimento sugli hook di Claude Code, verificata ad agosto 2026 con la release 2.1.232. Questa interfaccia cambia rapidamente. Prima di copiare JSON da qualsiasi post del blog, incluso questo, controlla la documentazione relativa alla tua versione. Visualizza la tua versione con claude --version.
Dove si trova la configurazione degli hook
Un hook è un blocco JSON in un file di impostazioni. Sei posizioni possono contenerne uno e l'ambito del file determina l'ambito dell'hook.
~/.claude/settings.json: tutti i progetti presenti sul tuo computer, ma nessun progetto su altri computer..claude/settings.json: un solo progetto, con il file salvato nel repository, così tutti quelli che lo clonano ricevono l'hook..claude/settings.local.json: un solo progetto, soltanto sul tuo computer.- Impostazioni dei criteri gestiti: valide per tutta l'organizzazione e definite da un amministratore.
hooks/hooks.jsonall'interno di un plugin: valide finché il plugin è abilitato.- Front matter di una skill o di un subagent: valido finché il componente è attivo.
Le voci degli hook provenienti da questi file vengono unite, invece di sovrascriversi. Un file di impostazioni del progetto aggiunge i propri hook a quelli presenti nelle impostazioni dell'utente, invece di sostituirli. Di conseguenza, uno stesso evento può contenere più hook provenienti da file diversi. Impostando "disableAllHooks": true gli hook vengono disabilitati, con un'eccezione: gli hook provenienti dalle impostazioni dei criteri gestiti continuano a essere eseguiti, a meno che tale impostazione non venga applicata anche nelle impostazioni gestite.
Esegui /hooks all'interno di una sessione per elencare tutti gli hook attualmente registrati, raggruppati per evento, indicando per ciascuno il file di origine e il matcher. Il menu è in sola lettura; per modificare un hook devi modificare il file di impostazioni. Il file watcher rileva normalmente la modifica senza richiedere un riavvio.
Quali eventi hook sono disponibili in Claude Code
La release 2.1.232 elenca trentuno eventi, da SessionStart a SessionEnd, che coprono compaction, subagent, worktree e file di configurazione. Per la gestione dei server ne servono soltanto alcuni.
PreToolUse: prima dell'esecuzione di una chiamata a uno strumento. È l'unico che può bloccare l'operazione.PostToolUse: dopo il completamento corretto di una chiamata a uno strumento.PostToolUseFailureviene attivato invece quando la chiamata non riesce; quindi, per intercettare entrambi gli esiti, un hook deve usare entrambi gli eventi.PermissionRequest: quando una chiamata a uno strumento richiede una decisione sui permessi, cioè nel momento in cui verrebbe visualizzata la richiesta di approvazione.UserPromptSubmit: quando si invia un prompt, prima che Claude lo elabori. Qualsiasi output che questo hook scrive su stdout viene aggiunto al contesto del modello.SessionStarteSessionEnd: alla fine di ogni sessione.SessionStartviene attivato anche dopo la compaction, con il valorecompactper il matcher.Stop: quando Claude termina la risposta. Viene attivato una volta per turno, non una volta per ogni attività completata.
Ogni gruppo contiene un matcher che determina per quali occorrenze viene eseguito l'hook. Negli eventi degli strumenti filtra in base al nome dello strumento; quindi "Edit|Write" viene attivato per le modifiche ai file e per nessun'altra operazione. I matcher distinguono tra maiuscole e minuscole. Un matcher vuoto viene attivato per ogni occorrenza. Gli strumenti di un server MCP (model context protocol) sono denominati mcp__<server>__<tool>; quindi un matcher "mcp__github__.*" intercetta gli strumenti di un server e lascia invariati quelli degli altri.
Gli hook Stop presentano un comportamento che è importante conoscere prima di scriverne uno. Un hook Stop che blocca l'operazione rimanda il modello al lavoro, e Claude Code sostituisce l'hook dopo otto blocchi consecutivi. Leggi il campo stop_hook_active dall'input dell'hook ed esci con codice 0 quando il valore è true; in caso contrario l'hook continuerà a essere eseguito fino al raggiungimento di quel limite.
Dati ricevuti dall’hook su stdin
Quando Claude sta per eseguire npm test, un hook PreToolUse su Bash legge questi dati da stdin:
{
"session_id": "abc123",
"cwd": "/home/deploy/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}Ogni evento contiene session_id, cwd, permission_mode, transcript_path e hook_event_name. Gli eventi relativi agli strumenti aggiungono tool_name, tool_input e tool_use_id. Gli altri eventi contengono campi specifici: UserPromptSubmit riceve il testo prompt, mentre SessionStart riceve un source di startup, resume, clear, compact o fork.
jq è il metodo usuale per leggere questi dati in uno script shell, ma un’immagine server minimale non lo include. Installarlo prima con sudo apt install -y jq su Ubuntu e Debian.
Cosa comporta lo stato di uscita per la chiamata allo strumento in corso
Sono possibili tre esiti.
- Exit 0 indica che l’hook non solleva obiezioni. Su
PreToolUsenon equivale a un’approvazione e il normale flusso delle autorizzazioni continua comunque. SuUserPromptSubmiteSessionStart, stdout viene aggiunto al contesto del modello. - Exit 2 blocca l’azione negli eventi che possono essere bloccati, incluso
PreToolUse, e stderr diventa il motivo mostrato al modello. Negli eventi che non possono essere bloccati, comePostToolUse, il blocco viene ignorato, ma stderr viene comunque inviato al modello come feedback. - Qualsiasi altro codice di uscita indica un errore non bloccante. L’azione continua. La trascrizione mostra una notifica di errore dell’hook contenente la prima riga di stderr dopo il testo
Failed with non-blocking status code:.
Per ottenere un comportamento diverso dal blocco o dall’assenza di output, usa exit 0 e stampa invece un oggetto JSON su stdout. Un hook PreToolUse prende la decisione con permissionDecision:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database drops go through a migration, not through the agent."
}
}"allow" ignora il prompt interattivo, "deny" annulla la chiamata e invia il motivo al modello, mentre "ask" mostra normalmente il prompt. Scegli uno stile per ogni hook. Combinare exit 2 con una decisione JSON su stdout produce un risultato che devi interpretare separatamente.
Quando più hook corrispondono allo stesso evento, vengono eseguiti in parallelo e ciascuno viene portato a termine. Un deny prodotto da un hook non interrompe gli altri: un hook di logging continua quindi a scrivere la propria riga mentre un hook di protezione nega la stessa chiamata. Claude Code unisce quindi le risposte e mantiene quella più restrittiva, nell’ordine deny, defer, ask, allow.
Esempio 1: bloccare un comando distruttivo prima dell'esecuzione
Salva questo contenuto come .claude/hooks/block-destructive.sh nel progetto:
#!/bin/bash
# Deny a Bash tool call whose command matches a banned pattern.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
for pattern in 'rm -rf /' 'mkfs' 'dd if=' 'DROP TABLE'; do
if printf '%s' "$COMMAND" | grep -qiF -- "$pattern"; then
echo "Blocked by policy: the command matches '$pattern'. A human runs this one." >&2
exit 2
fi
done
exit 0Rendilo eseguibile, quindi registralo in PreToolUse su .claude/settings.json:
chmod +x .claude/hooks/block-destructive.sh{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
"timeout": 10,
"statusMessage": "Checking the command against policy"
}
]
}
]
}
}Testa manualmente lo script prima di considerarlo affidabile, perché un hook che va in errore con il proprio input applica la configurazione fail-open:
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /var/lib/postgresql"}}' \
| .claude/hooks/block-destructive.sh
echo $?Dovresti vedere la riga Blocked by policy: su stderr e un codice di uscita pari a 2. Passagli un comando innocuo, ad esempio ls -la, e non dovresti vedere alcun output; il codice di uscita deve essere 0. In una sessione, la chiamata negata compare nella trascrizione con il tuo messaggio come motivazione; il modello legge il messaggio e adatta il comportamento.
Una proprietà rende utile questa configurazione: gli hook PreToolUse vengono eseguiti prima del controllo della modalità delle autorizzazioni, in ogni modalità. Di conseguenza, un'azione deny resta effettiva anche con bypassPermissions. Questo rende l'hook utile insieme a Claude Code in modalità automatica e alle relative impostazioni delle autorizzazioni, dove le richieste di conferma sono ridotte ma l'hook viene comunque eseguito.
È importante chiarire i limiti di questa soluzione. La ricerca di pattern nella stringa di un comando è una misura di sicurezza contro la disattenzione di un agent, non un confine contro un agent che cerca di aggirarla, perché lo stesso comando può essere scritto in una forma che grep non rileva. Le regole vincolanti devono essere definite nel sistema delle autorizzazioni e nell'account con cui viene eseguito il processo.
Esempio 2: formattazione e lint dopo ogni modifica
PostToolUse con un matcher Edit|Write viene eseguito dopo qualsiasi strumento di modifica dei file. Salvatelo come .claude/hooks/after-edit.sh:
#!/bin/bash
# Format the edited file, then report lint failures back to the model.
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
case "$FILE" in
*.py)
ruff format "$FILE" >/dev/null 2>&1
if ! ruff check "$FILE" >&2; then
exit 2
fi
;;
*.sh)
if ! shellcheck "$FILE" >&2; then
exit 2
fi
;;
esac
exit 0{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/after-edit.sh",
"timeout": 60
}
]
}
]
}
}Chiedete a Claude di aggiungere a un file Python una funzione con un'indentazione errata, quindi aprite il file. Il file risulta formattato. Questo conferma che l'hook è stato eseguito, perché un hook completato correttamente non mostra nulla nella conversazione.
In questo caso, l'exit 2 non annulla nulla. PostToolUse viene eseguito dopo che lo strumento ha già completato l'operazione, quindi la modifica viene comunque scritta su disco. Il vantaggio dell'exit 2 è che l'output di ruff check viene restituito al modello come feedback, che corregge l'errore appena introdotto invece di procedere. Questa è la differenza tra un errore di lint rilevato al momento del commit e uno che l'agente corregge nello stesso turno.
Qui sono importanti due limiti dei matcher. Edit|Write non rileva i file modificati da un comando shell, e Claude scrive abbastanza spesso i file tramite Bash perché questa lacuna sia concreta. Per coprire ogni chiamata, fate corrispondere anche Bash e fate in modo che lo script elenchi i file modificati con git status --porcelain. Per una copertura una volta per turno, inserite la scansione in un hook Stop.
Esempio 3: registrare ogni chiamata agli strumenti per l'audit
Un matcher vuoto su PostToolUse viene eseguito per ogni strumento. Inviando il record al journal di sistema invece che a un file nella directory home, il record non è accessibile dalla shell dell'agente:
{
"hooks": {
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{time: now|todate, session: .session_id, cwd: .cwd, tool: .tool_name, input: .tool_input}' | logger -t claude-code -p local0.info"
}
]
}
]
}
}Rileggi i record con journalctl -t claude-code -o cat | tail -n 5. Dovresti vedere una riga JSON per ogni chiamata allo strumento, con la più recente per ultima. Se non compare nulla, l'hook non è stato eseguito; la sezione sulla risoluzione dei problemi qui sotto descrive come verificarlo.
Aggiungi lo stesso blocco sotto PostToolUseFailure per acquisire le chiamate non riuscite, perché PostToolUse viene eseguito solo in caso di successo e, in genere, è proprio il comando non riuscito a essere più interessante. Il motivo per usare logger invece di aggiungere dati a un file nella directory home riguarda la proprietà: un hook viene eseguito con lo stesso utente della shell dell'agente, quindi tutto ciò a cui quell'utente può aggiungere dati può anche essere troncato dallo stesso utente. Il journal viene scritto da systemd-journald con il proprio account.
Durata massima di esecuzione di un hook
The data behind this chart
[
{
"label": "command, http or mcp_tool hook",
"default_timeout_seconds": 600
},
{
"label": "agent hook",
"default_timeout_seconds": 60
},
{
"label": "prompt hook",
"default_timeout_seconds": 30
},
{
"label": "command hook on UserPromptSubmit",
"default_timeout_seconds": 30
},
{
"label": "command hook on MessageDisplay",
"default_timeout_seconds": 10
},
{
"label": "any hook on SessionEnd",
"default_timeout_seconds": 1.5
}
]Per impostazione predefinita, un hook di comando ha a disposizione 600 secondi, cioè dieci minuti. Alcuni eventi riducono drasticamente questo limite. Gli hook SessionEnd condividono un budget di 1.5 secondi complessivi. La pulizia al termine della sessione deve quindi essere rapida. Impostando un valore timeout più lungo sull’hook, il budget condiviso aumenta allo stesso valore, fino a 60 secondi.
Un hook che raggiunge il timeout viene annullato e non produce alcuna decisione. Per una protezione PreToolUse, questo significa che non blocca l’operazione: la chiamata allo strumento prosegue nel normale flusso di autorizzazione. Per questo motivo, gli script di protezione devono essere brevi. Per le operazioni lente che non richiedono l’attesa dell’utente, come l’invio di un log a un’altra destinazione, imposta "async": true: l’hook viene eseguito in background senza ritardare la chiamata allo strumento.
Hook, file di regole, skill e server MCP
Quattro elementi vengono spesso confusi perché modificano tutti il comportamento di un agent. Soltanto uno smette di essere un suggerimento.
Un file di regole (CLAUDE.md oppure un file in .claude/rules/) è testo caricato nel contesto del modello. Influenza il comportamento, ma non impone nulla. In una conversazione lunga, con un diff esteso e una nuova richiesta dell'utente, una sua singola riga può passare inosservata. Questo è il normale meccanismo alla base degli agent che ignorano le istruzioni scritte.
Una skill è una directory di istruzioni e script che il modello carica quando ritiene che la skill sia pertinente. Questa valutazione è lo scopo della skill, ma anche il suo limite: la decisione resta al modello. Entrambi gli aspetti sono visibili in una skill come Ponytail, che orienta l'agent verso la modifica minima funzionante, perché influenza il modo in cui viene affrontata un'intera attività, cosa che nessun hook potrebbe fare, e lo fa soltanto quando il modello decide di caricarla.
Un server MCP (model context protocol) fornisce al modello nuovi strumenti da chiamare. Estende ciò a cui l'agent può accedere. Non fa sì che l'agent utilizzi tali strumenti e costituisce un processo separato che devi gestire, con attività operative dedicate: consulta eseguire server MCP su un VPS.
Un hook è l'unico dei quattro che viene eseguito senza una scelta del modello. Usa un file di regole per una preferenza e una skill per una procedura che il modello deve seguire quando è applicabile. Usa un hook per il passaggio che deve essere eseguito ogni volta o per l'azione che non deve mai essere eseguita. Il confronto completo, incluso quando una skill è preferibile a un file di regole, è disponibile in confronto tra skill, MCP e file di regole.
Un plugin riguarda il confezionamento, non costituisce un quinto meccanismo. Raggruppa hook e skill in una singola unità installabile. In questo modo un team distribuisce lo stesso controllo su ogni macchina: consulta come funzionano i plugin di Claude Code.
La decisione di sicurezza su un VPS condiviso
Un hook è codice attivato dall’agente ed eseguito con l’account che ha avviato Claude Code. Eredita l’ambiente e i permessi sui file di quell’account. Su un laptop è una questione di flusso di lavoro. Su un VPS in cui un agente viene eseguito senza supervisione, è una questione di sicurezza con quattro aspetti pratici.
Un hook in un repository è codice che non hai scritto tu. .claude/settings.json è incluso nel repository, quindi la clonazione di un repository e l’avvio di una sessione al suo interno possono registrare gli hook presenti nel repository. Claude Code sottopone gli hook del progetto alla finestra di dialogo per l'attendibilità dell'area di lavoro relativa a quella directory. Accettare l'attendibilità è quindi il momento in cui decidi di eseguirli. Leggi prima il blocco hooks.
Un hook vede l'intero input dello strumento. Un hook di audit che registra tool_input scrive in un file ogni argomento di ogni comando, incluso qualsiasi token eventualmente presente nella riga di comando. Anche questo log deve essere protetto come il secret, nell'ambito del problema più ampio descritto in impedire a un agente AI di accedere ai secret.
Un hook può scrivere nel contesto del modello. Qualsiasi output che un hook SessionStart o UserPromptSubmit invia a stdout viene aggiunto alla conversazione. Un hook che inoltra testo proveniente da una fonte esterna, da un sistema di issue tracking o da un file di log, consegna al modello testo non attendibile come se lo avessi digitato tu. Tratta quell'output stdout come input, non come output.
Il privilegio è il controllo effettivo. Esegui l'agente con un account dedicato senza privilegi, dotato soltanto delle regole sudo necessarie. È utile definire un deny PreToolUse, ma per sua natura offre solo una protezione best effort: la documentazione di riferimento afferma lo stesso del filtro if e indica di usare il sistema dei permessi quando serve un deny effettivo. Le regole dei permessi e l'account con cui viene eseguito il processo sono gli elementi che restano validi anche in condizioni critiche.
Una proprietà vale in ogni configurazione. Gli hook PreToolUse vengono eseguiti prima del controllo della modalità dei permessi in ogni modalità dei permessi. Pertanto, un hook che restituisce deny blocca lo strumento anche con bypassPermissions. Gli hook possono rendere più restrittivo ciò che consentono le regole dei permessi. Non possono renderlo meno restrittivo.
Perché il mio hook non viene eseguito?
Procedi nell’ordine indicato. Ogni passaggio descrive il sintomo che vedrai effettivamente.
- Esegui
/hookse verifica che l’hook compaia nell’evento previsto. Se un hook non compare nel menu, di solito il file delle impostazioni contiene un errore di sintassi JSON, perché non sono consentite virgole finali né commenti, oppure il file non si trova in una delle sei posizioni indicate sopra. - Confronta esattamente il matcher con il nome dello strumento. I matcher distinguono tra maiuscole e minuscole, quindi
"bash"non corrisponde mai allo strumentoBash. - Esegui manualmente lo script con un input di esempio, come nell’esempio 1 sopra. Un codice di uscita diverso da quello previsto indica un bug nello script. Claude Code lo segnala come errore dell’hook, non come decisione.
- Un messaggio con il testo
jq: command not foundindica che su quella macchina mancajq. Uncommand not foundrelativo allo script che hai scritto indica che il percorso non è stato risolto; usa quindi${CLAUDE_PROJECT_DIR}oppure un percorso assoluto. Se lo script non viene mai eseguito, probabilmente non dispone dei permessi di esecuzione. - L’hook stampa JSON valido, ma non accade nulla. Un hook in formato shell viene eseguito tramite
sh -c. Se il profilo della shell stampa un banner, questo viene anteposto al JSON. L’output standard non inizia più con{, quindi Claude Code interpreta l’intero contenuto come testo normale e ignora la decisione. Con codice di uscita 0 non viene riportato nulla, tranne che nel log di debug. Racchiudi qualsiasiechonel profilo in modo che venga eseguito soltanto nelle shell interattive. - Se il problema persiste, avvia la sessione con
claude --debug-file /tmp/claude.loged eseguitail -f /tmp/claude.login un secondo terminale. Il log di debug registra quali hook hanno trovato corrispondenza, il codice di uscita restituito da ciascuno e tutto ciò che hanno scritto su stdout e stderr.
FAQ
Qual è la differenza tra un hook di Claude Code e un'istruzione in CLAUDE.md?
Un'istruzione CLAUDE.md è testo nel contesto del modello. Compete quindi per l'attenzione con la conversazione e con la richiesta corrente, e il modello può valutarla rispetto a questi elementi. Un hook è un comando shell che Claude Code esegue in un punto specifico del proprio ciclo di vita. Viene quindi eseguito a ogni occorrenza dell'evento, indipendentemente dalla decisione del modello. Usate un'istruzione per esprimere una preferenza. Usate un hook per un passaggio che deve essere sempre eseguito o per un'azione che non deve mai essere eseguita.
Come posso impedire a Claude Code di eseguire uno specifico comando shell?
Registrate un hook PreToolUse con un matcher Bash che legge il comando da .tool_input.command, scrive una motivazione su stderr ed esce con il codice 2. Claude Code annulla la chiamata e mostra al modello la motivazione. Questo avviene prima del controllo della modalità dei permessi, quindi il blocco resta valido anche in modalità bypassPermissions. La corrispondenza su una stringa di comando è una misura di protezione, non un confine di sicurezza, perché lo stesso comando può essere scritto in una forma che il pattern non rileva. Affiancatela quindi a regole sui permessi e all'uso di un account senza privilegi.
Il mio hook stampa JSON valido, ma non succede nulla. Perché?
La causa più comune è il profilo della shell. Un hook senza il campo args viene eseguito tramite sh -c, e alcuni profili stampano un banner a ogni avvio della shell. Il banner finisce su stdout prima del JSON. Poiché l'output non inizia più con {, Claude Code tratta tutto come testo semplice e ignora la decisione. Con l'uscita 0, inoltre, nel transcript non viene riportato nulla. Racchiudete ogni echo nel profilo in un controllo che verifichi l'uso di una shell interattiva, quindi confermate la correzione leggendo il log di debug da claude --debug-file /tmp/claude.log.
È sicuro eseguire gli hook di Claude Code su un server condiviso?
Gli hook vengono eseguiti come l'utente che ha avviato Claude Code, con i permessi sui file di quell'utente. Un hook può quindi eseguire qualsiasi operazione consentita a quell'account. Due pratiche coprono la maggior parte dei rischi: eseguite l'agente con un account dedicato senza privilegi e con una policy sudo restrittiva, quindi leggete il blocco hooks di qualsiasi repository prima di accettare la relativa finestra di dialogo per l'attendibilità dell'area di lavoro, perché gli hook del progetto vengono distribuiti all'interno di .claude/settings.json. Impostate "disableAllHooks": true nel file delle impostazioni quando non volete eseguire nessuno di questi hook.