L'hook Stop di Claude Code esegue un comando ogni volta che l'agente principale di Claude finisce di rispondere: è quindi il posto giusto per inviare una notifica di "fatto", registrare il risultato o lanciare dei controlli. Non viene eseguito quando interrompi Claude, e un turno che termina con un errore API attiva invece StopFailure. L'hook riceve il messaggio finale di Claude in last_assistant_message e può restituire decision: "block" con un reason per far continuare Claude invece di fermarsi.
01Quando Stop si attiva e quando no
Stop si attiva alla fine di ogni turno, non solo quando un intero task è completato. Claude Code non ha il concetto di "task": sa solo che Claude ha smesso di rispondere e ti sta aspettando. Una domanda veloce genera un solo Stop, esattamente come un refactoring di due ore.
Non si attiva:
- quando premi Esc o interrompi il turno in altro modo;
- quando il turno termina per un errore API, come un rate limit, un sovraccarico o un errore di autenticazione. In questi casi Claude Code attiva
StopFailure, con un campoerrorsu cui puoi fare match (rate_limit,overloaded,server_errore altri); - quando finisce un subagent. In quel caso si usa
SubagentStop.
Stop non supporta i matcher: qualsiasi matcher aggiungi viene ignorato e l'hook viene eseguito a ogni turno.
02Lo Stop hook più semplice
Aggiungi questo a ~/.claude/settings.json per eseguire un comando ogni volta che Claude finisce. Questo riproduce un suono su macOS:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
}
}
Per i comandi di notifica desktop su Mac, Windows e Linux, leggi come ricevere una notifica quando Claude Code finisce. Il resto di questa guida parla di cosa puoi fare con l'input e l'output dell'hook.
03Cosa riceve il hook
Oltre ai campi comuni (session_id, transcript_path, cwd, permission_mode, hook_event_name), un hook Stop riceve:
last_assistant_messageIl testo della risposta finale di Claude per questo turnostop_hook_activetrue se Claude sta già continuando perché uno Stop hook ha bloccatobackground_tasksTask ancora in esecuzione, come shell in background o subagentsession_cronsRisvegli programmati in questa sessione, ad esempio da /loopUsa last_assistant_message invece di leggere il file del transcript. Il transcript viene scritto in modo asincrono e potrebbe non contenere ancora il messaggio finale quando l'hook viene eseguito.
background_tasks e session_crons ti dicono se la sessione è davvero finita o solo in pausa finché un lavoro in background non la risveglia. Uno script di notifica può verificare che siano entrambi vuoti prima di dire "finito".
04Metti l'ultimo messaggio di Claude nella notifica
Questo script (macOS e Linux, richiede jq) mostra l'inizio della risposta di Claude e resta in silenzio se c'è ancora lavoro in background:
#!/bin/bash
# ~/.claude/hooks/notify-done.sh
input=$(cat)
# Skip if background tasks are still in flight
busy=$(jq '(.background_tasks // []) | length' <<<"$input")
[ "$busy" -gt 0 ] && exit 0
msg=$(jq -r '(.last_assistant_message // "Claude has finished")
| gsub("\\s+"; " ") | .[0:140]' <<<"$input")
if command -v osascript >/dev/null 2>&1; then
osascript -e 'on run argv' \
-e 'display notification (item 1 of argv) with title "Claude has finished"' \
-e 'end run' "$msg"
elif command -v notify-send >/dev/null 2>&1; then
notify-send "Claude has finished" "$msg"
fi
exit 0
Salvalo, esegui chmod +x ~/.claude/hooks/notify-done.sh e punta l'hook verso lo script:
{
"hooks": {
"Stop": [
{
"hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-done.sh" }]
}
]
}
}
Lo script esce sempre con 0. È importante: su Stop, l'exit code 2 significa "non fermarti", quindi uno script di notifica che esce per sbaglio con 2 terrebbe Claude al lavoro.
05Far continuare Claude
Stop è uno degli eventi che possono bloccare. Bloccare uno stop significa che Claude prosegue il turno usando il tuo motivo come istruzione successiva. Puoi farlo in due modi:
- esci con
2e Claude riceve il tuo stderr come motivo; oppure - esci con
0e stampa un JSON:
{
"decision": "block",
"reason": "The test suite is failing. Run npm test, fix the failures, then finish."
}
Se l'hook fornisce indicazioni anziché segnalare un errore, anche hookSpecificOutput.additionalContext fa proseguire la conversazione, ma il transcript lo etichetta come feedback dello Stop hook invece che come errore dell'hook:
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"additionalContext": "Please run the test suite before finishing"
}
}
Evitare un loop infinito
Uno Stop hook che blocca sempre non lascerebbe mai fermare Claude. Ti proteggono due cose:
stop_hook_activevaletruequando Claude sta già continuando a causa di uno Stop hook. Controllalo ed esci subito.- Claude Code termina comunque il turno dopo che gli Stop hook hanno bloccato otto volte di fila senza nessuna chiamata a tool nel mezzo. La variabile d'ambiente
CLAUDE_CODE_STOP_HOOK_BLOCK_CAPalza questo limite.
Ecco un hook che fa correggere a Claude i test falliti una volta, poi lo lascia fermare:
#!/bin/bash
# ~/.claude/hooks/tests-must-pass.sh
input=$(cat)
[ "$(jq -r '.stop_hook_active' <<<"$input")" = "true" ] && exit 0
if ! npm test --silent >/dev/null 2>&1; then
jq -nc '{decision: "block", reason: "npm test is failing. Fix the failures, then finish."}'
fi
exit 0
Gli hook vengono eseguiti nella directory di lavoro della sessione, quindi npm test gira nel tuo progetto. Tieni i controlli veloci: di default un command hook può durare fino a 600 secondi e Claude lo aspetta.
Se vuoi solo che Claude continui a lavorare verso un obiettivo senza scrivere uno script, il comando integrato /goal di Claude Code configura per te uno Stop hook limitato alla sessione.
06Notifiche anche per i fallimenti
Dato che gli errori API saltano Stop, un avviso di "fatto" non ti dirà che Claude si è arreso per un rate limit. Se vuoi saperlo, aggiungi lo stesso script sotto StopFailure. Il suo output e l'exit code vengono ignorati, quindi può solo notificare o scrivere log. Per StopFailure, last_assistant_message contiene il testo dell'errore, ad esempio "API Error: Rate limit reached".
07Dove si inserisce Claude Code Notifier
Uno Stop hook che chiama un servizio di push è un ottimo modo per sapere che Claude ha finito mentre sei via. Se vuoi farlo da solo, la guida a ntfy è un buon punto di partenza.
Claude Code Notifier usa lo stesso hook Stop, con una differenza: può rispondere. Il suo tool da riga di comando gratuito, ccn, gira su Mac, Windows e Linux e mostra da solo una notifica desktop. Con l'app per iPhone o Android associata, ricevi una push "Claude ha finito" appena termina un turno e puoi scrivere la prossima istruzione dal telefono. ccn la restituisce dallo hook Stop come prossima istruzione per Claude, così la sessione va avanti anche senza di te alla tastiera. Se hai digitato negli ultimi 30 secondi, non si aspetta il telefono. È un progetto indipendente, non realizzato da Anthropic. Vedi allontanarsi durante i task lunghi per questo workflow e la pagina di ccn per installarlo.
08FAQ
Lo Stop hook parte quando Claude finisce l'intero task o dopo ogni risposta?
Dopo ogni risposta. Stop scatta ogni volta che l'agente principale finisce di rispondere, qualunque sia la dimensione della richiesta. Controlla background_tasks e session_crons nell'input se devi sapere se c'è ancora altro lavoro in programma.
Perché il mio Stop hook non è partito?
Forse hai interrotto il turno, cosa che salta Stop, oppure è terminato con un errore API, che invece fa scattare StopFailure. Altrimenti verifica che /hooks elenchi il tuo hook e che lo script sia eseguibile. La checklist di troubleshooting copre il resto.
Perché Claude continua a lavorare dopo che il mio Stop hook è stato eseguito?
Il tuo hook sta bloccando lo stop, uscendo con codice 2 o stampando decision: "block". Assicurati che un hook di sola notifica esca sempre con 0 e che ogni hook bloccante controlli prima stop_hook_active.
Come ottengo la risposta finale di Claude nello hook?
Leggi last_assistant_message dal JSON su stdin, ad esempio con jq -r '.last_assistant_message'. Non fare affidamento su transcript_path: quando scatta Stop il file potrebbe non contenere ancora l'ultimo messaggio.
Qual è la differenza tra Stop e SubagentStop?
Stop riguarda l'agente principale. SubagentStop scatta quando un subagent finisce, fa match sul tipo di agente e supporta gli stessi campi di blocco. Un avviso "Claude ha finito" va su Stop.