El hook Stop de Claude Code ejecuta un comando cada vez que el agente principal de Claude termina de responder, así que es el sitio ideal para enviar una notificación de "listo", registrar el resultado o lanzar comprobaciones. No se ejecuta si interrumpes a Claude, y un turno que termina con un error de la API dispara StopFailure en su lugar. El hook recibe el mensaje final de Claude en last_assistant_message y puede devolver decision: "block" con un reason para que Claude continúe en vez de parar.
01Cuándo se dispara Stop y cuándo no
Stop se dispara al final de cada turno, no solo cuando se completa una tarea entera. Claude Code no tiene noción de "la tarea"; solo sabe que Claude ha dejado de responder y te está esperando. Una pregunta corta genera un Stop, y un refactor de dos horas también.
No se dispara:
- cuando pulsas Esc o interrumpes el turno de cualquier otra forma;
- cuando el turno termina por un error de la API, como un rate limit, sobrecarga o fallo de autenticación. Para esos casos Claude Code dispara
StopFailure, con un campoerrorque puedes usar para filtrar (rate_limit,overloaded,server_errory otros); - cuando termina un subagente. Para eso está
SubagentStop.
Stop no admite matchers, así que cualquier matcher que añadas se ignora y el hook se ejecuta en cada turno.
02El Stop hook más sencillo
Añade esto a ~/.claude/settings.json para ejecutar un comando cada vez que Claude termine. Este reproduce un sonido en macOS:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
}
}
Para comandos de notificaciones de escritorio en Mac, Windows y Linux, consulta cómo recibir una notificación cuando Claude Code termina. El resto de esta guía trata de lo que puedes hacer con el input y el output del hook.
03Qué recibe el hook
Además de los campos comunes (session_id, transcript_path, cwd, permission_mode, hook_event_name), un hook Stop recibe:
last_assistant_messageEl texto de la respuesta final de Claude en este turnostop_hook_activetrue si Claude ya está continuando porque un Stop hook lo bloqueóbackground_tasksTareas que siguen en ejecución, como shells en segundo plano o subagentessession_cronsReactivaciones programadas en esta sesión, por ejemplo con /loopUsa last_assistant_message en lugar de leer el archivo de transcript. El transcript se escribe de forma asíncrona y puede que aún no contenga el mensaje final cuando se ejecuta el hook.
background_tasks y session_crons te indican si la sesión ha terminado de verdad o solo está en pausa hasta que el trabajo en segundo plano la reactive. Un script de notificación puede comprobar que ambos estén vacíos antes de decir "terminado".
04Muestra el último mensaje de Claude en la notificación
Este script (macOS y Linux, necesita jq) muestra el comienzo de la respuesta de Claude y no dice nada si aún hay trabajo en segundo plano:
#!/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
Guárdalo, ejecuta chmod +x ~/.claude/hooks/notify-done.sh y apunta el hook a él:
{
"hooks": {
"Stop": [
{
"hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-done.sh" }]
}
]
}
}
El script siempre sale con 0. Esto es importante: en Stop, el código de salida 2 significa "no pares", así que un script de notificación que saliera con 2 por error mantendría a Claude trabajando.
05Hacer que Claude siga
Stop es uno de los eventos que pueden bloquear. Bloquear una parada significa que Claude continúa con el turno, usando tu motivo como su siguiente instrucción. Puedes hacerlo de dos formas:
- sal con
2y Claude recibirá tu stderr como motivo; o - sal con
0e imprime JSON:
{
"decision": "block",
"reason": "The test suite is failing. Run npm test, fix the failures, then finish."
}
Si el hook da indicaciones en lugar de informar de un error, hookSpecificOutput.additionalContext también mantiene la conversación, pero el transcript lo marca como feedback del Stop hook en vez de como error del hook:
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"additionalContext": "Please run the test suite before finishing"
}
}
Cómo evitar un bucle infinito
Un Stop hook que siempre bloquee nunca dejaría parar a Claude. Hay dos cosas que te protegen:
stop_hook_activevaletruecuando Claude ya está continuando por un Stop hook. Compruébalo y sal antes.- Claude Code termina el turno de todos modos cuando los Stop hooks han bloqueado ocho veces seguidas sin ninguna llamada a herramientas entre medias. La variable de entorno
CLAUDE_CODE_STOP_HOOK_BLOCK_CAPpermite subir ese límite.
Aquí tienes un hook que hace que Claude arregle los tests que fallan una vez y luego lo deja parar:
#!/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
Los hooks se ejecutan en el directorio de trabajo de la sesión, así que npm test se ejecuta en tu proyecto. Haz que las comprobaciones sean rápidas: por defecto, un hook de comando puede ejecutarse hasta 600 segundos y Claude lo espera.
Si solo quieres que Claude siga trabajando hacia un objetivo sin escribir un script, el comando integrado /goal de Claude Code configura por ti un Stop hook limitado a la sesión.
06Notificaciones también para los fallos
Como los errores de la API se saltan Stop, una alerta de "terminado" no te avisará de que Claude se rindió por un límite de uso. Añade el mismo script en StopFailure si quieres enterarte. Su salida y su código de salida se ignoran, así que solo sirve para notificar o registrar. En StopFailure, last_assistant_message contiene el texto del error, como "API Error: Rate limit reached".
07Dónde encaja Claude Code Notifier
Un Stop hook que llama a un servicio de push es una forma perfectamente válida de saber que Claude ha terminado mientras estás fuera. Si quieres montarlo tú mismo, la guía de ntfy es un buen punto de partida.
Claude Code Notifier usa el mismo hook Stop, con una diferencia: puede devolver algo. Su herramienta de línea de comandos gratuita, ccn, funciona en Mac, Windows y Linux y muestra por sí sola una notificación de escritorio. Con la app de iPhone o Android emparejada, recibes un push de "Claude ha terminado" en cuanto acaba un turno, y puedes escribir la siguiente instrucción en el móvil. ccn la devuelve desde el hook Stop como la siguiente instrucción de Claude, así que la sesión continúa sin que estés al teclado. Si has escrito en los últimos 30 segundos, no se espera al móvil. Es independiente y no lo ha creado Anthropic. Consulta alejarte de las tareas largas para ese flujo de trabajo y la página de ccn para instalarlo.
08Preguntas frecuentes
¿El Stop hook se ejecuta cuando Claude termina toda la tarea o después de cada respuesta?
Después de cada respuesta. Stop se dispara cada vez que el agente principal termina de responder, sea cual sea el tamaño de la petición. Revisa background_tasks y session_crons en la entrada si necesitas saber si aún queda trabajo programado.
¿Por qué no se disparó mi Stop hook?
Puede que interrumpieras el turno, lo que se salta Stop, o que terminara con un error de la API, que dispara StopFailure en su lugar. Si no, comprueba que /hooks muestra tu hook y que el script es ejecutable. La lista de comprobación para solucionar problemas cubre el resto.
¿Por qué Claude sigue trabajando después de que se ejecute mi Stop hook?
Tu hook está bloqueando la parada, ya sea saliendo con código 2 o imprimiendo decision: "block". Asegúrate de que un hook que solo notifica siempre salga con 0, y de que cualquier hook que bloquee compruebe primero stop_hook_active.
¿Cómo obtengo la respuesta final de Claude en el hook?
Lee last_assistant_message del JSON que llega por stdin, por ejemplo con jq -r '.last_assistant_message'. No dependas de transcript_path: puede que el archivo aún no contenga el último mensaje cuando se dispara Stop.
¿Qué diferencia hay entre Stop y SubagentStop?
Stop es para el agente principal. SubagentStop se dispara cuando termina un subagente, filtra por tipo de agente y admite los mismos campos de bloqueo. Una alerta de "Claude ha terminado" va en Stop.