Hooks de Claude Code: eventos, configuración y errores
Aprende dónde se configuran los hooks de Claude Code, qué eventos los activan y cómo el código de salida 2 cancela una herramienta y devuelve stderr al modelo.
Qué es un hook de Claude Code
Los hooks de Claude Code son comandos de shell que Claude Code ejecuta automáticamente en puntos concretos de su propio ciclo de vida. Esta es la diferencia fundamental entre un hook y un archivo de reglas. Una instrucción en CLAUDE.md es una recomendación, y el modelo la pondera junto con todo lo demás que tiene en su contexto. Un hook es código y se ejecuta independientemente de que el modelo esté de acuerdo. Si el agente sigue omitiendo el formateador aunque ya se lo haya indicado dos veces, no necesita una instrucción más estricta. Necesita un hook.
El mecanismo es sencillo. Se registra un comando en un archivo de configuración bajo el nombre de un evento. Cuando se activa ese evento, Claude Code ejecuta el comando y escribe los datos del evento en su entrada estándar (stdin) como JSON (notación de objetos de JavaScript). El comando lee esos datos, realiza su trabajo y responde con un estado de salida. El estado de salida 2 de un hook de PreToolUse cancela la llamada a la herramienta antes de que se ejecute, y lo que el script haya escrito en el error estándar (stderr) se devuelve al modelo como motivo.
Los nombres de los eventos y de los campos de esta sección proceden de la referencia de hooks de Claude Code, comprobada en agosto de 2026 con la versión 2.1.232. Esta interfaz cambia rápidamente, por lo que debe consultar la referencia correspondiente a su propia versión antes de copiar JSON de cualquier entrada de blog, incluida esta. Muestre la suya con claude --version.
Dónde se encuentra la configuración de los hooks
Un hook es un bloque JSON en un archivo de configuración. Seis ubicaciones pueden contener uno, y el alcance del archivo determina el alcance del hook.
~/.claude/settings.json: todos los proyectos de su equipo, pero sólo en su equipo..claude/settings.json: un proyecto, confirmado en el repositorio, para que todas las personas que lo clonen obtengan el hook..claude/settings.local.json: un proyecto, sólo en su equipo.- Configuración de políticas administradas: para toda la organización, establecida por un administrador.
hooks/hooks.jsondentro de un plugin: está activo mientras el plugin está habilitado.- Frontmatter de una skill o un subagente: está activo mientras ese componente está activo.
Las entradas de hooks de estos archivos se combinan en lugar de sobrescribirse. Un archivo de configuración del proyecto añade sus hooks a los de la configuración del usuario, en lugar de reemplazarlos. Por tanto, un evento puede contener varios hooks procedentes de varios archivos. Establecer "disableAllHooks": true los desactiva, con una excepción: los hooks de la configuración de políticas administradas siguen ejecutándose, a menos que ese ajuste también se aplique en la configuración administrada.
Ejecute /hooks dentro de una sesión para mostrar todos los hooks registrados actualmente, agrupados por evento, junto con el archivo de origen y el matcher de cada uno. El menú es de sólo lectura. Para cambiar un hook, edite el archivo de configuración. Normalmente, el observador de archivos detecta la edición sin necesidad de reiniciar.
Eventos de hooks disponibles en Claude Code
La versión 2.1.232 incluye treinta y un eventos, desde SessionStart hasta SessionEnd, que cubren la compactación, los subagentes, los worktrees y los archivos de configuración. Para tareas de administración de servidores se usa un subconjunto de ellos.
PreToolUse: antes de ejecutar una llamada a una herramienta. Es el único que puede bloquearla.PostToolUse: después de que una llamada a una herramienta se ejecuta correctamente.PostToolUseFailurese activa cuando falla; por tanto, un hook que deba detectar todos los resultados necesita ambos.PermissionRequest: cuando una llamada a una herramienta necesita una decisión de permisos. Ese es el momento en que aparecería el aviso de aprobación.UserPromptSubmit: cuando se envía un prompt, antes de que Claude lo procese. Todo lo que este hook escriba en stdout se añade al contexto del modelo.SessionStartySessionEnd: al principio y al final de cada sesión.SessionStarttambién se activa después de la compactación, con el valor de matchercompact.Stop: cuando Claude termina de responder. Se activa una vez por turno, no una vez por tarea terminada.
Cada grupo incluye un matcher que determina qué ocurrencias ejecutan el hook. En los eventos de herramientas, filtra por nombre de herramienta; por tanto, "Edit|Write" se activa al editar archivos y no en ningún otro caso. Los matchers distinguen entre mayúsculas y minúsculas. Un matcher vacío se activa en todas las ocurrencias. Las herramientas de un servidor MCP (protocolo de contexto del modelo) se denominan mcp__<server>__<tool>; por tanto, un matcher con el valor "mcp__github__.*" captura las herramientas de un servidor y deja las demás sin cambios.
Los hooks Stop tienen una particularidad que debe conocer antes de escribir uno. Un hook Stop que bloquea devuelve el modelo al trabajo, y Claude Code omite el hook después de ocho bloqueos consecutivos. Lea el campo stop_hook_active de la entrada del hook y salga con el código 0 cuando su valor sea true. De lo contrario, el hook se repetirá hasta alcanzar ese límite.
Qué recibe un hook por stdin
Cuando Claude está a punto de ejecutar npm test, un hook PreToolUse en Bash lee estos datos desde stdin:
{
"session_id": "abc123",
"cwd": "/home/deploy/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}Todos los eventos incluyen session_id, cwd, permission_mode, transcript_path y hook_event_name. Los eventos de herramientas también incluyen tool_name, tool_input y tool_use_id. Los demás eventos tienen sus propios campos: UserPromptSubmit recibe el texto prompt y SessionStart recibe un source de startup, resume, clear, compact o fork.
jq es la forma habitual de leer estos datos dentro de un script de shell, pero una imagen de servidor mínima no lo incluye. Instálelo primero con sudo apt install -y jq en Ubuntu y Debian.
Qué hace el estado de salida con la llamada a la herramienta en curso
Hay tres resultados.
- Exit 0 significa que el hook no presenta objeciones. En
PreToolUse, esto no equivale a una aprobación y el flujo normal de permisos continúa. EnUserPromptSubmitySessionStart, stdout se añade al contexto del modelo. - Exit 2 bloquea la acción en los eventos que se pueden bloquear, incluido
PreToolUse, y stderr se muestra al modelo como motivo. En los eventos que no se pueden bloquear, comoPostToolUse, el bloqueo se ignora, aunque stderr sigue llegando al modelo como información. - Cualquier otro código de salida es un error no bloqueante. La acción continúa. La transcripción muestra un aviso de error del hook con la primera línea de stderr después del texto
Failed with non-blocking status code:.
Para cualquier comportamiento distinto de bloquear o no hacer nada, use exit 0 y escriba un objeto JSON en stdout. Un hook de PreToolUse toma la decisión mediante permissionDecision:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database drops go through a migration, not through the agent."
}
}"allow" omite el aviso interactivo, "deny" cancela la llamada y envía el motivo al modelo, y "ask" muestra el aviso con normalidad. Elija un solo estilo por hook. Mezclar exit 2 con una decisión JSON en stdout produce un resultado que tendrá que consultar.
Cuando varios hooks coinciden con un evento, se ejecutan en paralelo y todos se ejecutan hasta finalizar. Un deny de un hook no detiene a sus homólogos, por lo que un hook de registro sigue escribiendo su línea mientras un hook de protección rechaza la misma llamada. Claude Code combina las respuestas y conserva la más restrictiva, en este orden: denegar, aplazar, preguntar, permitir.
Ejemplo 1: bloquear un comando destructivo antes de ejecutarlo
Guarde esto como .claude/hooks/block-destructive.sh en su proyecto:
#!/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 0Dé permisos de ejecución al archivo y regístrelo después en PreToolUse dentro de .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"
}
]
}
]
}
}Pruebe el script manualmente antes de confiar en él, porque un hook que falla al procesar sus propios datos de entrada deja pasar la operación:
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /var/lib/postgresql"}}' \
| .claude/hooks/block-destructive.sh
echo $?Debería ver la línea Blocked by policy: en stderr y un código de salida 2. Páselo un comando inofensivo, como ls -la, y no debería ver ninguna salida; el código de salida debe ser 0. En una sesión, la llamada denegada aparece en la transcripción con su mensaje como motivo, y el modelo lee ese mensaje y adapta su comportamiento.
Una propiedad hace que esto sea útil: los hooks de PreToolUse se ejecutan antes de comprobar el modo de permisos, en cualquier modo de permisos, por lo que una denegación se mantiene incluso con bypassPermissions. Esto hace que un hook sea útil junto con el modo automático de Claude Code y sus opciones de permisos, donde se reducen las solicitudes de confirmación, pero el hook sigue ejecutándose.
Sea preciso sobre lo que ofrece. La coincidencia de patrones en una cadena de comando es una medida de protección contra los descuidos de un agente. No constituye un límite contra un agente que intente eludirla, porque el mismo comando puede escribirse de una forma que su grep nunca detecte. Las reglas estrictas deben establecerse en el sistema de permisos y en la cuenta con la que se ejecuta el proceso.
Ejemplo 2: aplicar formato y lint después de cada edición
PostToolUse con un comparador Edit|Write se ejecuta después de cualquier herramienta que edite archivos. Guárdelo como .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
}
]
}
]
}
}Pida a Claude que añada una función con una indentación incorrecta a un archivo Python y, después, abra el archivo. Aparecerá con el formato corregido. Esa es la comprobación de que el hook se ejecutó, porque un hook correcto no muestra nada en la conversación.
El código de salida 2 no deshace nada. PostToolUse se ejecuta después de que la herramienta ya haya terminado, por lo que la edición queda guardada en el disco en cualquier caso. Lo que proporciona el código de salida 2 es que la salida de ruff check llegue al modelo como información de retorno, para que corrija el error que acaba de introducir en lugar de continuar. Esta es la diferencia entre un fallo de lint que se detecta al hacer commit y uno que el agente corrige en el mismo turno.
Aquí importan dos limitaciones de los comparadores. Edit|Write no detecta los archivos modificados por un comando de shell, y Claude escribe archivos mediante Bash con suficiente frecuencia como para que esa limitación sea relevante. Para cubrir cada llamada, haga coincidir también Bash y configure el script para que enumere los archivos modificados con git status --porcelain. Para cubrir una sola vez por turno, coloque el análisis en un hook Stop.
Ejemplo 3: registrar cada llamada a una herramienta para auditoría
Un comparador vacío en PostToolUse se ejecuta con todas las herramientas. Enviar el registro al journal del sistema, en lugar de guardarlo en un archivo del directorio personal, lo mantiene fuera del alcance del shell del propio 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"
}
]
}
]
}
}Vuelva a leerlo con journalctl -t claude-code -o cat | tail -n 5. Debería ver una línea JSON por cada llamada a una herramienta, con la más reciente al final. Si no aparece nada, el hook no se ejecutó. La sección de resolución de problemas siguiente explica cómo investigarlo.
Añada el mismo bloque bajo PostToolUseFailure para registrar las llamadas que fallaron, porque PostToolUse sólo se ejecuta cuando hay éxito y un comando fallido suele ser el más relevante. La razón para usar logger en lugar de añadir registros a un archivo del directorio personal es la propiedad: un hook se ejecuta con el mismo usuario que el shell del agente, por lo que todo lo que ese usuario pueda modificar mediante adición también puede truncarlo. systemd-journald escribe el journal con su propia cuenta.
Cuánto tiempo puede ejecutarse 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
}
]De forma predeterminada, un hook de comandos dispone de 600 segundos, es decir, diez minutos. Algunos eventos reducen ese tiempo considerablemente. Los hooks SessionEnd comparten un presupuesto de 1.5 segundos entre todos ellos, por lo que la limpieza al finalizar la sesión debe ser rápida. Sin embargo, configurar un timeout más largo en el hook aumenta ese presupuesto compartido hasta el mismo valor, con un máximo de 60 segundos.
Si un hook alcanza su tiempo de espera, se cancela y no emite ninguna decisión. En un control de seguridad PreToolUse, esto significa que no bloquea la operación: la llamada a la herramienta continúa por el flujo normal de permisos. Por ese motivo, los scripts de control deben ser pequeños. Para tareas lentas que no requieren una respuesta inmediata, como enviar un registro a otra ubicación, establezca "async": true. El hook se ejecutará en segundo plano sin retrasar la llamada a la herramienta.
Hooks, archivos de reglas, skills y servidores MCP
Hay cuatro conceptos que se confunden porque todos cambian el comportamiento de un agente. Sólo uno deja de ser una sugerencia.
Un archivo de reglas (CLAUDE.md o un archivo dentro de .claude/rules/) es texto que se carga en el contexto del modelo. Da forma al comportamiento, pero no lo impone. Frente a una conversación larga, un diff grande y una solicitud nueva del usuario, una de sus líneas puede perderse. Ese es el mecanismo habitual por el que los agentes ignoran las instrucciones que ha escrito.
Una skill es una carpeta de instrucciones y scripts que el modelo carga cuando considera que la skill es relevante. Esa decisión es la finalidad de una skill y también su límite: el modelo sigue decidiendo. Puede ver ambas características en una skill como Ponytail, que orienta al agente hacia el cambio mínimo que funciona, porque determina cómo se aborda una tarea completa de una forma que ningún hook puede ofrecer, y sólo mientras el modelo decide cargarla.
Un servidor MCP (model context protocol) proporciona al modelo nuevas herramientas que puede invocar. Amplía el alcance del agente. No hace que el agente utilice ninguna herramienta y, además, es un proceso independiente que debe administrar, lo que constituye una tarea propia: consulte cómo ejecutar servidores MCP en un VPS.
Un hook es el único de los cuatro que se ejecuta sin que el modelo lo elija. Use un archivo de reglas para una preferencia y una skill para un procedimiento que el modelo deba seguir cuando corresponda. Use un hook para el paso que deba ejecutarse siempre o para la acción que no deba ocurrir nunca. La comparación detallada, incluido cuándo una skill es mejor que un archivo de reglas, está en la comparación entre skills, MCP y archivos de reglas.
Un plugin es un formato de empaquetado, no un quinto mecanismo. Agrupa hooks y skills en una unidad instalable. Así, un equipo puede distribuir la misma protección a todas las máquinas: consulte cómo funcionan los plugins de Claude Code.
La decisión de seguridad en un VPS compartido
Un hook es código que activa el agente y se ejecuta con el usuario que inició Claude Code. Hereda el entorno y los permisos de archivos de ese usuario. En un portátil, esto es una cuestión de flujo de trabajo. En un VPS donde un agente se ejecuta sin supervisión, es una cuestión de seguridad con cuatro aspectos prácticos.
Un hook incluido en un repositorio es código que usted no escribió. .claude/settings.json está incluido en el repositorio, por lo que clonar un repositorio e iniciar una sesión dentro de él puede registrar los hooks incluidos en el repositorio. Claude Code somete los hooks del proyecto al diálogo de confianza del espacio de trabajo para esa carpeta. Por tanto, aceptar la confianza es el momento en que decide ejecutarlos. Lea primero el bloque hooks.
Un hook ve toda la entrada de la herramienta. Un hook de auditoría que registra tool_input escribe todos los argumentos de todos los comandos en un archivo, incluido cualquier token que estuviera en una línea de comandos. Ese registro necesita la misma protección que el secreto. Esto forma parte del problema más amplio de mantener los secretos fuera del alcance de un agente de IA.
Un hook puede escribir en el contexto del modelo. Todo lo que un hook SessionStart o UserPromptSubmit imprime en stdout se añade a la conversación. Un hook que introduce texto desde una fuente externa, un sistema de seguimiento de incidencias o un archivo de registro está entregando texto no confiable al modelo como si usted lo hubiera escrito. Trate ese stdout como entrada, no como salida.
El privilegio es el control real. Ejecute el agente con un usuario dedicado sin privilegios y sólo con las reglas sudo que necesite. Conviene configurar un rechazo PreToolUse, aunque está diseñado para ofrecer una garantía limitada: la documentación de referencia indica lo mismo sobre el filtro if y recomienda usar el sistema de permisos cuando necesite un rechazo estricto. Las reglas de permisos y la cuenta con la que se ejecuta el proceso son los elementos que siguen funcionando bajo presión.
Hay una propiedad que se mantiene en todas las configuraciones. Los hooks PreToolUse se ejecutan antes de la comprobación del modo de permisos en todos los modos de permisos, por lo que un hook que devuelve deny bloquea la herramienta incluso con bypassPermissions. Los hooks pueden restringir lo que permiten las reglas de permisos. No pueden ampliarlo.
¿Por qué no se ejecuta mi hook?
Siga estos pasos en orden. Cada paso indica el síntoma que verá.
- Ejecute
/hooksy compruebe que el hook aparezca en el evento esperado. Si un hook no aparece en el menú, normalmente el archivo de configuración contiene un error de sintaxis JSON, porque no se permiten comas finales ni comentarios, o el archivo no se encuentra en ninguna de las seis ubicaciones anteriores. - Compare exactamente el matcher con el nombre de la herramienta. Los matchers distinguen mayúsculas de minúsculas, por lo que
"bash"nunca coincide con la herramientaBash. - Ejecute el script manualmente con datos de entrada de ejemplo, como en el ejemplo 1 anterior. Un código de salida inesperado indica un error en el script. Claude Code lo informa como un error del hook y no como una decisión.
- Un aviso que muestra
jq: command not foundsignifica que faltajqen esa máquina. Uncommand not foundpara su propio script significa que la ruta no se pudo resolver, así que use${CLAUDE_PROJECT_DIR}o una ruta absoluta. Si el script no se ejecuta en absoluto, probablemente no tiene permisos de ejecución. - El hook imprime JSON válido, pero no ocurre nada. Un hook en formato shell se ejecuta mediante
sh -c. Si el perfil del shell imprime un banner, ese banner se antepone al JSON. La salida estándar ya no comienza por{, por lo que Claude Code interpreta todo el contenido como texto sin formato e ignora la decisión. Con la salida 0 no se informa nada en ningún sitio, salvo en el registro de depuración. Encierre cualquierechodel perfil para que sólo se ejecute en shells interactivos. - Si el problema continúa, inicie la sesión con
claude --debug-file /tmp/claude.logy ejecutetail -f /tmp/claude.logen un segundo terminal. El registro de depuración indica qué hooks coincidieron, qué código de salida devolvió cada uno y todo lo que escribieron en la salida estándar y en la salida de error.
FAQ
¿Cuál es la diferencia entre un hook de Claude Code y una instrucción de CLAUDE.md?
Una instrucción CLAUDE.md es texto incluido en el contexto del modelo. Por tanto, compite por la atención con la conversación y con la solicitud actual, y el modelo puede ponderarla frente a ellas. Un hook es un comando de shell que Claude Code ejecuta en un punto fijo de su ciclo de vida. Por tanto, se ejecuta cada vez que ocurre su evento, independientemente de lo que haya decidido el modelo. Use una instrucción para expresar una preferencia. Use un hook para un paso que siempre deba ejecutarse o una acción que nunca deba realizarse.
¿Cómo puedo impedir que Claude Code ejecute un comando de shell específico?
Registre un hook PreToolUse con un comparador Bash que lea el comando desde .tool_input.command, escriba el motivo en stderr y termine con el código 2. Claude Code cancela la llamada y muestra el motivo al modelo. Esto ocurre antes de comprobar el modo de permisos, por lo que la denegación se mantiene incluso en el modo bypassPermissions. La coincidencia de patrones sobre una cadena de comando es una medida de protección, no un límite de seguridad, porque el mismo comando puede escribirse de una forma que el patrón no detecte. Refuércela con reglas de permisos y con una cuenta sin privilegios.
Mi hook muestra JSON válido, pero no ocurre nada. ¿Por qué?
La causa más habitual es el perfil de shell. Un hook sin un campo args se ejecuta mediante sh -c, y algunos perfiles muestran un banner en cada shell. Ese texto aparece en stdout antes del JSON. Como la salida ya no empieza por {, Claude Code trata todo el contenido como texto sin formato e ignora la decisión. Además, si termina con el código 0, no se informa de nada en la transcripción. Proteja cualquier echo del perfil con una comprobación de shell interactivo. Después, confirme la corrección leyendo el registro de depuración desde claude --debug-file /tmp/claude.log.
¿Es seguro ejecutar hooks de Claude Code en un servidor compartido?
Los hooks se ejecutan con la cuenta que inició Claude Code y con los permisos de archivos de esa cuenta. Por tanto, un hook puede hacer todo lo que esa cuenta pueda hacer. Dos prácticas cubren la mayor parte del riesgo: ejecute el agente con una cuenta dedicada sin privilegios y una política sudo limitada, y lea el bloque hooks de cualquier repositorio antes de aceptar su cuadro de diálogo de confianza del espacio de trabajo, porque los hooks del proyecto se incluyen en .claude/settings.json. Establezca "disableAllHooks": true en el archivo de configuración cuando no quiera que se ejecute ninguno de ellos.