Hook di Claude Code: eventi, configurazione e sicurezza
Scopri dove configurare gli hook di Claude Code, quali eventi li attivano e perché il codice di uscita 2 annulla una chiamata allo strumento prima dell'esecuzione.
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 sia d'accordo. Se l'agente continua a ignorare il formatter che gli hai indicato due volte, non ti serve un'istruzione più esplicita. Ti serve un hook.
Il meccanismo è semplice. Registri un comando in un file di impostazioni associandolo al nome di un evento. Quando l'evento viene generato, Claude Code esegue il comando e scrive i dati dell'evento nello standard input (stdin) in formato JSON (JavaScript object notation). Il comando legge questi dati, esegue le proprie operazioni e restituisce uno stato di uscita. Il codice di uscita 2 di un hook PreToolUse annulla la chiamata allo strumento prima della sua esecuzione. Qualsiasi contenuto scritto dallo script nello 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 sulla release 2.1.232. Questa interfaccia cambia rapidamente. Prima di copiare il JSON da un articolo di blog, incluso questo, consulta 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 contenuto in un file di impostazioni. Sei posizioni possono contenerne uno; l'ambito del file determina l'ambito dell'hook.
~/.claude/settings.json: tutti i progetti sul proprio computer, ma nessun progetto sugli altri computer..claude/settings.json: un solo progetto, con il file salvato nel repository; chiunque lo cloni riceverà l'hook..claude/settings.local.json: un solo progetto, soltanto sul proprio computer.- Impostazioni dei criteri gestiti: a livello dell'organizzazione, definite da un amministratore.
hooks/hooks.jsonall'interno di un plugin: attivo finché il plugin è abilitato.- Frontmatter di una skill o di un subagent: attivo finché il componente è in esecuzione.
Le voci degli hook presenti in questi file vengono unite, invece di sovrascriversi. Un file di impostazioni del progetto aggiunge i propri hook a quelli definiti nelle impostazioni dell'utente, invece di sostituirli. Di conseguenza, un evento può contenere diversi hook provenienti da file diversi. Impostando "disableAllHooks": true, gli hook vengono disattivati, con un'eccezione: gli hook definiti nelle impostazioni dei criteri gestiti continuano a essere eseguiti, a meno che l'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 sorgente e il matcher. Il menu è di sola lettura; per modificare un hook devi modificare il file di impostazioni. Il file watcher rileva generalmente la modifica senza richiedere un riavvio.
Quali eventi degli hook di Claude Code esistono
La release 2.1.232 elenca trentuno eventi, da SessionStart a SessionEnd, che coprono compattazione, subagent, worktree e file di configurazione. Per il lavoro di amministrazione dei server ne bastano alcuni.
PreToolUse: prima dell'esecuzione di una chiamata a uno strumento. È l'evento che può bloccarla.PostToolUse: dopo il completamento corretto di una chiamata a uno strumento.PostToolUseFailureviene attivato invece quando la chiamata non riesce; quindi un hook che deve rilevare ogni esito deve gestire entrambi.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 contenuto scritto da questo hook su stdout viene aggiunto al contesto del modello.SessionStarteSessionEnd: a ogni chiusura di una sessione.SessionStartviene attivato anche dopo la compattazione, con il valore del matchercompact.Stop: quando Claude termina la risposta. Viene attivato una volta per turno, non una volta per attività completata.
Ogni gruppo include 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 sulle modifiche ai file e in nessun altro caso. I matcher distinguono tra maiuscole e minuscole. Un matcher vuoto viene attivato a 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 non quelli degli altri.
Gli hook Stop presentano un comportamento importante da conoscere prima di scriverne uno. Un hook Stop che blocca l'operazione rimanda il modello all'elaborazione e Claude Code sostituisce l'hook dopo otto blocchi consecutivi. Leggi il campo stop_hook_active dall'input dell'hook e termina con codice 0 quando il valore è true; in caso contrario l'hook continuerà a essere eseguito fino al raggiungimento di questo limite.
Cosa riceve un 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 degli 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 consueto per leggere questi dati in uno script shell, ma non è disponibile in un'immagine server minimale. Installalo prima con sudo apt install -y jq su Ubuntu e Debian.
Cosa comporta lo stato di uscita per la chiamata allo strumento in corso
Gli esiti possibili sono tre.
- Exit 0 indica che l’hook non rileva problemi. Su
PreToolUsenon equivale all’approvazione e viene comunque eseguito il normale flusso delle autorizzazioni. 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 trasmesso al modello come feedback. - Qualsiasi altro codice di uscita indica un errore non bloccante. L’azione prosegue. 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 dal silenzio, usa exit 0 e stampa invece un oggetto JSON su stdout. Un hook PreToolUse decide tramite 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. Usa un solo stile per ogni hook. Combinare exit 2 con una decisione JSON su stdout produce un risultato che devi cercare nella documentazione.
Quando più hook corrispondono allo stesso evento, vengono eseguiti in parallelo e ciascuno completa la propria esecuzione. Un deny di un hook non interrompe gli hook associati, quindi un hook di logging scrive comunque 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 nega, rimanda, chiede, consente.
Esempio 1: bloccare un comando distruttivo prima dell'esecuzione
Salva il file con il nome .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"
}
]
}
]
}
}Prova lo script manualmente prima di considerarlo affidabile, perché un hook che va in errore durante l'elaborazione del proprio input non applica il blocco:
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 dovrebbe 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.
Questa proprietà rende utile la configurazione: gli hook PreToolUse vengono eseguiti prima del controllo della modalità di autorizzazione, in ogni modalità di autorizzazione. Di conseguenza, un rifiuto resta valido anche con bypassPermissions. Per questo un hook è utile insieme a modalità automatica e impostazioni di autorizzazione di Claude Code, dove le richieste di conferma sono ridotte, ma l'hook continua a essere eseguito.
È importante chiarire i limiti di questa soluzione. Il riconoscimento di modelli in una stringa di comando è una protezione contro la disattenzione di un agente, non un confine contro un agente 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 di autorizzazione 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. Salvalo 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
}
]
}
]
}
}Chiedi a Claude di aggiungere a un file Python una funzione con indentazione errata, quindi apri il file. Il file risulterà formattato. Questo conferma che l’hook è stato eseguito, perché un hook completato correttamente non mostra nulla nella conversazione.
In questo caso, l’uscita 2 non annulla nulla. PostToolUse viene eseguito dopo che lo strumento ha già completato l’operazione, quindi la modifica è comunque presente su disco. L’uscita 2 fa sì che l’output di ruff check venga restituito al modello come feedback, permettendogli di correggere l’errore appena introdotto invece di proseguire. Questa è la differenza tra un errore di linting rilevato al momento del commit e uno corretto dall’agente nello stesso turno.
Qui contano due limiti dei matcher. Edit|Write non rileva i file modificati da un comando shell, mentre Claude scrive abbastanza spesso i file tramite Bash perché questa lacuna sia concreta. Per una copertura a ogni chiamata, fai il match anche su Bash e fai elencare allo script i file modificati con git status --porcelain. Per una copertura una volta per turno, inserisci 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. L'invio del record al journal di sistema, invece che a un file nella directory home, lo mantiene fuori dalla portata della 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, dalla più recente alla più vecchia. Se non compare nulla, l'hook non è stato eseguito; la sezione sulla risoluzione dei problemi riportata sotto tratta questo caso.
Aggiungi lo stesso blocco sotto PostToolUseFailure per acquisire le chiamate non riuscite, perché PostToolUse viene eseguito solo in caso di successo e un comando non riuscito è generalmente quello più interessante. Il motivo per usare logger invece di aggiungere dati a un file nella directory home è 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 dispone di 600 secondi, cioè dieci minuti. Alcuni eventi riducono drasticamente questo limite. Gli hook SessionEnd condividono un budget di 1.5 secondi complessivi, quindi la pulizia al termine della sessione deve essere rapida. Tuttavia, impostando un valore timeout più lungo sull'hook, il budget condiviso aumenta fino a quel valore, con un massimo di 60 secondi.
Un hook che raggiunge il timeout viene annullato e non produce alcuna decisione. Per un meccanismo di 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 da cui nessuno deve attendere, come l'invio di un log a un'altra destinazione, impostare "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. Solo uno di questi smette di essere un suggerimento.
Un file di regole (CLAUDE.md oppure un file sotto .claude/rules/) è testo caricato nel contesto del modello. Influisce sul comportamento, ma non impone nulla. In una conversazione lunga, con un diff esteso e una nuova richiesta dell'utente, una sua riga può passare inosservata. Questo è il meccanismo ordinario alla base degli agent che ignorano le istruzioni scritte.
Una skill è una directory di istruzioni e script che il modello carica quando ritiene pertinente. Questa valutazione è lo scopo di una 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é definisce il modo di affrontare un'intera attività in un modo che nessun hook potrebbe offrire, e solo finché il modello sceglie di caricarla.
Un server MCP (model context protocol) fornisce al modello nuovi strumenti da chiamare. Amplia ciò che l'agent può raggiungere. Non fa sì che l'agent utilizzi necessariamente questi strumenti. Inoltre è un processo separato che devi gestire, ed è un'attività autonoma: vedi come 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 è pertinente. 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 è un sistema di pacchettizzazione, non un quinto meccanismo. Raggruppa hook e skill in un'unica unità installabile. In questo modo un team può distribuire la stessa protezione su ogni macchina: vedi come funzionano i plugin di Claude Code.
La decisione di sicurezza su un VPS condiviso
Un hook è codice attivato dall'agent ed eseguito con l'utente che ha avviato Claude Code. Eredita l'ambiente e i permessi sui file di quell'utente. Su un laptop è una questione di flusso di lavoro. Su un VPS in cui un agent 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 clonare un repository e avviare una sessione al suo interno può registrare gli hook presenti nel repository. Claude Code sottopone gli hook del progetto alla finestra di dialogo di attendibilità dell'area di lavoro per quella directory. Accettare l'attendibilità è quindi il momento in cui decidi di eseguirli. Leggi prima il blocco hooks.
Un hook vede tutti gli input degli strumenti. 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 più ampio del problema descritto in tenere i secret fuori dalla portata di un agent AI.
Un hook può scrivere nel contesto del modello. Qualunque contenuto stampi su stdout un hook SessionStart o UserPromptSubmit viene aggiunto alla conversazione. Un hook che inserisce testo proveniente da fonti esterne, da un sistema di issue tracking o da un file di log sta fornendo testo non attendibile al modello come se lo avessi digitato tu. Un hook che inoltra una nota da un'altra sessione di Claude Code sullo stesso VPS fa la stessa cosa, e l'output di un agent non è più attendibile di quello del sistema di issue tracking. Considera stdout un input, non un output.
Il privilegio è il vero controllo. Esegui l'agent con un utente dedicato senza privilegi, concedendogli soltanto le regole sudo necessarie. Un deny PreToolUse è utile e, per sua progettazione, offre solo una protezione di tipo best effort: la documentazione di riferimento afferma lo stesso per il 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 sotto pressione.
Una proprietà vale in ogni configurazione. Gli hook PreToolUse vengono eseguiti prima del controllo della modalità dei permessi in ogni modalità di autorizzazione, quindi un hook che restituisce deny blocca lo strumento anche con bypassPermissions. Gli hook possono restringere ciò che le regole dei permessi consentono. Non possono ampliarlo.
Perché il mio hook non viene eseguito?
Segui questi passaggi nell’ordine indicato. Ogni passaggio descrive il sintomo che vedrai effettivamente.
- Esegui
/hookse verifica che l’hook compaia sotto l’evento previsto. Se un hook non compare nel menu, di solito il file delle impostazioni contiene un errore di sintassi JSON, perché le virgole finali e i commenti non sono consentiti, oppure il file non si trova in una delle sei posizioni precedenti. - 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 precedente. Un codice di uscita diverso da quello previsto indica un problema nello script. Claude Code lo segnala come errore dell’hook, non come decisione.
- Un messaggio
jq: command not foundindica chejqmanca su quella macchina. Uncommand not foundrelativo a uno script personale 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 è eseguibile. - 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. Avvolgi ogniechonel 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 CLAUDE.md?
Un'istruzione CLAUDE.md è testo nel contesto del modello, quindi compete per l'attenzione con la conversazione e con la richiesta corrente, e il modello può valutarla rispetto a queste. Un hook è un comando shell che Claude Code esegue in un punto prestabilito del proprio ciclo di vita, quindi viene eseguito ogni volta che si verifica l'evento corrispondente, indipendentemente dalla decisione presa dal modello. Usare un'istruzione per esprimere una preferenza. Usare un hook per un passaggio che deve avvenire sempre o per un'azione che non deve mai avvenire.
Come posso impedire a Claude Code di eseguire uno specifico comando shell?
Registrare un hook PreToolUse con un matcher Bash che legge il comando da .tool_input.command, scrive un motivo su stderr ed esce con codice 2. Claude Code annulla la chiamata e mostra il motivo al modello. Questo avviene prima del controllo della modalità delle autorizzazioni, 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. Rafforzarla quindi con regole di autorizzazione e con 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 interpreta tutto come testo normale e ignora la decisione. Con codice di uscita 0, inoltre, nel transcript non viene riportato nulla. Proteggere qualsiasi echo nel profilo con un controllo della shell interattiva, quindi confermare 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 con l'utente che ha avviato Claude Code e 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: eseguire l'agente con un account dedicato senza privilegi e con una policy sudo restrittiva, quindi leggere il blocco hooks di qualsiasi repository prima di accettare la finestra di dialogo relativa all'attendibilità dell'area di lavoro, perché gli hook del progetto si trovano all'interno di .claude/settings.json. Impostare "disableAllHooks": true nel file delle impostazioni quando non si vuole eseguire nessuno di questi hook.