SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-23

Hooks de Claude Code: eventos, ubicación y código 2

Aprende dónde se configuran los hooks de Claude Code, qué eventos los activan y cómo el código de salida 2 bloquea una herramienta antes de ejecutarse.

Qué es un hook de Claude Code

Los hooks de Claude Code son comandos de shell que Claude Code ejecuta por sí mismo en puntos concretos de su ciclo de vida. Esa es toda la diferencia entre un hook y un archivo de reglas. Una instrucción en CLAUDE.md es una recomendación, y el modelo la valora junto con todo lo demás que tiene en su contexto. Un hook es código y se ejecuta tanto si el modelo está de acuerdo como si no. Si el agente sigue omitiendo el formateador aunque ya se lo haya indicado dos veces, no necesita una instrucción más firme. 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 produce 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 devuelve un estado de salida. El estado de salida 2 de un hook PreToolUse cancela la llamada a la herramienta antes de que se ejecute, y cualquier contenido que el script escriba 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 con rapidez, por lo que debe consultar la referencia correspondiente a su propia versión antes de copiar JSON de cualquier publicación 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 incluido en un archivo de configuración. Seis ubicaciones pueden contener uno, y el ámbito del archivo determina el ámbito del hook.

  • ~/.claude/settings.json: todos los proyectos de su máquina, pero no los de otros usuarios.
  • .claude/settings.json: un proyecto, confirmado en el repositorio, para que todos los que lo clonen obtengan el hook.
  • .claude/settings.local.json: un proyecto, sólo en su máquina.
  • Configuración de políticas administradas: afecta a toda la organización y la establece un administrador.
  • hooks/hooks.json dentro de un plugin: permanece activo mientras el plugin esté habilitado.
  • Frontmatter de una skill o un subagente: permanece 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 enumerar todos los hooks registrados actualmente, agrupados por evento, con el archivo de origen y el matcher de cada uno. El menú es de sólo lectura, por lo que debe cambiar un hook editando el archivo de configuración. El observador de archivos suele detectar la edición sin necesidad de reiniciar.

Qué eventos de hooks existen en Claude Code

La versión 2.1.232 incluye treinta y un eventos, desde SessionStart hasta SessionEnd, para compaction, subagents, worktrees y archivos de configuración. Para tareas de administración de servidores sólo se utiliza una parte de ellos.

  • PreToolUse: antes de ejecutar una llamada a una herramienta. Es el evento que puede bloquearla.
  • PostToolUse: después de que una llamada a una herramienta termina correctamente. PostToolUseFailure se activa cuando falla. Por tanto, un hook que deba recibir todos los resultados necesita ambos eventos.
  • PermissionRequest: cuando una llamada a una herramienta requiere una decisión de permisos. Es el momento en que aparecería el aviso de aprobación.
  • UserPromptSubmit: al enviar un prompt, antes de que Claude lo procese. Todo lo que este hook escriba en stdout se añade al contexto del modelo.
  • SessionStart y SessionEnd: en cada extremo de una sesión. SessionStart también se activa después de la compaction, con el valor compact en matcher.
  • 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 en qué casos se ejecuta el hook. En los eventos de herramientas, filtra por nombre de herramienta. Por ejemplo, "Edit|Write" se activa en las ediciones de archivos y en ningún otro caso. Los matchers distinguen mayúsculas y minúsculas. Un matcher vacío se activa en todos los casos. Las herramientas de un servidor MCP (model context protocol) se nombran como mcp__<server>__<tool>. Por tanto, un matcher "mcp__github__.*" captura las herramientas de un servidor y deja intactas las de los demás.

Los hooks Stop tienen un comportamiento importante que debe conocer antes de escribir uno. Un hook Stop que bloquea devuelve el modelo al trabajo, y Claude Code anula el hook después de ocho bloqueos consecutivos. Lea el campo stop_hook_active de la entrada del hook y termine con código 0 cuando su valor sea true. De lo contrario, el hook se repetirá hasta alcanzar ese límite.

Qué recibe un hook en stdin

Cuando Claude está a punto de ejecutar npm test, un hook PreToolUse en Bash lee lo siguiente 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 añaden tool_name, tool_input y tool_use_id. Los demás eventos incluyen sus propios campos: UserPromptSubmit recibe el texto de 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, y una imagen mínima del servidor 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 no equivale a una aprobación y el flujo normal de permisos continúa. En UserPromptSubmit y SessionStart, 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 convierte en el motivo que se muestra al modelo. En los eventos que no se pueden bloquear, como PostToolUse, el bloqueo se ignora, aunque stderr sigue llegando al modelo como información de retorno.
  • Cualquier otro código de salida es un error que no bloquea la acción. La acción continúa. La transcripción muestra un aviso de error del hook que incluye la primera línea de stderr después del texto Failed with non-blocking status code:.

Para cualquier comportamiento que no sea bloquear o permanecer en silencio, use exit 0 y escriba un objeto JSON en stdout. Un hook de PreToolUse toma la decisión con permissionDecision:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database drops go through a migration, not through the agent."
  }
}

"allow" omite el mensaje interactivo, "deny" cancela la llamada y envía el motivo al modelo, y "ask" muestra el mensaje con normalidad. Elija un estilo por hook. Si mezcla exit 2 con una decisión JSON en stdout, obtendrá un resultado cuyo significado tendrá que consultar.

Cuando varios hooks coinciden con un evento, se ejecutan en paralelo y todos llegan hasta el final. Un deny de un hook no detiene a los demás, por lo que un hook de registro sigue escribiendo su línea mientras un hook de protección deniega la misma llamada. Claude Code combina después las respuestas y conserva la opción más restrictiva, en este orden: deny, defer, ask, allow.

Ejemplo 1: bloquear un comando destructivo antes de que se ejecute

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 0

Conviértalo en ejecutable 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 su propia entrada permite 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. Introduzca un comando inocuo, como ls -la, y no debería ver ninguna salida; el código de salida debe ser 0. En una sesión, la llamada bloqueada 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 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 claro sobre lo que ofrece esta técnica. La coincidencia de patrones sobre una cadena de comando es una protección contra la ejecución descuidada por parte de un agente. No es un límite contra un agente que actúe de forma inteligente, porque el mismo comando puede escribirse de una forma que su grep nunca detecte. Las reglas estrictas deben estar 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 matcher 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 de Python y, después, abra el archivo. El archivo aparece con el formato corregido. Esta 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 haya terminado, por lo que la edición queda guardada en disco en cualquier caso. Lo que aporta el código de salida 2 es que la salida de ruff check llega al modelo como información de retorno, de modo que corrige 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 matchers. Edit|Write no detecta los archivos modificados por un comando de shell, y Claude escribe archivos mediante Bash con suficiente frecuencia como para que esta diferencia sea relevante. Para cubrir cada llamada, haga coincidir también Bash y haga que el script enumere los archivos modificados con git status --porcelain. Para cubrir cada turno una sola vez, coloque el análisis en un hook Stop en su lugar.

Ejemplo 3: registrar cada llamada a una herramienta para auditoría

Un comparador vacío en PostToolUse se ejecuta con cada herramienta. Enviar el registro al journal del sistema, en lugar de a un archivo del directorio personal, impide que el propio shell del agente pueda acceder a él:

{
  "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"
          }
        ]
      }
    ]
  }
}

Léalo de nuevo con journalctl -t claude-code -o cat | tail -n 5. Debe aparecer 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 solución de problemas siguiente explica cómo investigarlo.

Añada el mismo bloque bajo PostToolUseFailure para capturar las llamadas que fallaron, porque PostToolUse sólo se ejecuta correctamente y un comando fallido suele ser el más relevante. La razón para usar logger en lugar de añadir contenido a un archivo del directorio personal es la propiedad: un hook se ejecuta con el mismo usuario que el shell del agente. Por tanto, todo lo que ese usuario pueda modificar añadiendo contenido también puede truncarlo. El journal lo escribe systemd-journald con su propia cuenta.

Tiempo de ejecución permitido para un hook

ChartDefault hook timeout in seconds, by hook type and event
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 mucho ese tiempo. Los hooks de SessionEnd comparten un presupuesto de 1.5 segundos entre todos ellos, por lo que la limpieza al final de 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.

Un hook que alcanza su tiempo de espera se cancela y no emite ninguna decisión. En un mecanismo de protección PreToolUse, esto significa que no bloquea: la llamada a la herramienta continúa por el flujo normal de permisos. Por ese motivo, los scripts de protección deben ser pequeños. Para tareas lentas que no requieren una respuesta inmediata, como enviar un registro a otra ubicación, configure "async": true. El hook se ejecutará en segundo plano sin retrasar la llamada a la herramienta.

Hooks, archivos de reglas, skills y servidores MCP

Cuatro elementos se confunden entre sí porque todos cambian lo que hace 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 impone nada. En una conversación larga, un diff grande y una solicitud nueva del usuario, una de sus líneas puede quedar relegada. Ese es el mecanismo habitual por el que ocurre que los agentes ignoran las instrucciones que escribió.

Una skill es una carpeta de instrucciones y scripts que el modelo carga cuando considera que la skill es relevante. Esa decisión es el objetivo de una skill y también su límite: el modelo sigue decidiendo. Puede ver ambos aspectos en una skill como Ponytail, que orienta al agente hacia el cambio más pequeño que funciona, porque determina cómo se aborda una tarea completa de una forma que ningún hook podría conseguir, y sólo mientras el modelo decide cargarla.

Un servidor MCP (model context protocol) proporciona al modelo nuevas herramientas que puede utilizar. Amplía el alcance del agente. No hace que el agente utilice ninguna herramienta, y es un proceso independiente que debe operar, 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 debe ejecutarse siempre o para lo que nunca debe ocurrir. 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 una forma 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 del 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 de 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 protege los hooks del proyecto mediante el cuadro de 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, lo que 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 mismo lo hubiera escrito. Un hook que reenvía una nota desde otra sesión de Claude Code en el mismo VPS hace lo mismo. La salida de un agente no merece más confianza que la del sistema de seguimiento de incidencias. 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 de sudo que necesite. Un rechazo de PreToolUse es útil y está diseñado como mecanismo de mejor esfuerzo: la 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 siendo efectivos bajo presión.

Hay una propiedad que se cumple 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 /hooks y compruebe que el hook aparece en el evento esperado. Si un hook no aparece en el menú, normalmente significa que el archivo de configuración tiene un error de sintaxis JSON, porque no se permiten comas finales ni comentarios, o que el archivo no está en una de las seis ubicaciones anteriores.
  • Compare el matcher exactamente con el nombre de la herramienta. Los matchers distinguen entre mayúsculas y minúsculas, por lo que "bash" nunca coincide con la herramienta Bash.
  • Ejecute el script manualmente con una entrada de prueba, 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, no como una decisión.
  • Un aviso que indique jq: command not found significa que falta jq en esa máquina. Un command not found para su propio script significa que la ruta no se resolvió. Use ${CLAUDE_PROJECT_DIR} o una ruta absoluta. Si el script no se ejecuta en ningún caso, probablemente no tiene permiso 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 como texto sin formato e ignora la decisión. Con el código de salida 0, no se informa de nada en otro lugar, salvo en el registro de depuración. Modifique cualquier echo del 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.log y ejecute tail -f /tmp/claude.log en 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 esos elementos. 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 impido que Claude Code ejecute un comando de shell específico?

Registre un hook PreToolUse con un matcher Bash que lea el comando desde .tool_input.command, escriba el motivo en stderr y salga con el código 2. Claude Code cancela la llamada y muestra al modelo el motivo. Esto ocurre antes de comprobar el modo de permisos, por lo que la denegación se mantiene incluso en el modo bypassPermissions. La coincidencia 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 imprime JSON válido, pero no ocurre nada. ¿Por qué?

La causa más habitual es el perfil del shell. Un hook sin el campo args se ejecuta mediante sh -c, y algunos perfiles imprimen un banner en cada shell. Ese texto aparece en stdout antes del JSON. Como la salida ya no empieza por {, Claude Code interpreta todo como texto sin formato e ignora la decisión. Además, si termina con el código 0, no se muestra 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 el usuario que inició Claude Code y con los permisos de archivos de ese usuario. Por tanto, un hook puede hacer todo lo que esa cuenta tenga permitido hacer. Dos medidas cubren la mayor parte del riesgo: ejecute el agente con una cuenta dedicada sin privilegios y con una política sudo limitada, y lea el bloque hooks de cualquier repositorio antes de aceptar el 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.