Los hooks son comandos que Claude Code ejecuta en puntos fijos en una sesión: antes de una llamada de herramienta, después, cuando termina un turno. Así es cómo formatea archivos, rechaza ediciones a .env y registra comandos shell sin recordar preguntar. Los comandos slash que un humano escribe viven en comandos slash personalizados. Esta página es la ruta automática.

Dónde se configuran los hooks

Una definición de hook tiene tres niveles: el evento, un grupo de matcher y uno o más manejadores.

Pieza Ejemplo
Evento PreToolUse, PostToolUse, Stop
Matcher Nombre de herramienta o tipo de notificación. Omite solo en eventos que ignoran matchers
Manejador "type": "command" más un comando shell
Archivo Alcance Compartir con git
~/.claude/settings.json Todos los proyectos en esta máquina No
.claude/settings.json Este repo Sí
.claude/settings.local.json Este repo, solo tú Ignorado por git cuando Claude Code guarda un ajuste allí

En Windows, ~/.claude es %USERPROFILE%\.claude. La política gestionada y los complementos pueden añadir más hooks. Los cinco ejemplos abajo pertenecen al archivo del proyecto para que el equipo los comparta.

Los manejadores pueden ser un comando shell, un POST HTTP, un prompt, un agente o una herramienta MCP. Estos ejemplos usan type: command porque el script es visible en el repo. Los hooks de comando reciben el evento JSON en stdin.

$CLAUDE_PROJECT_DIR es la raíz del proyecto que Claude Code exporta para comandos de hook. Ponlo entre comillas. Las rutas con espacios se dividen de lo contrario.

mkdir -p .claude/hooks
chmod +x .claude/hooks/*.sh

En macOS y Linux el script debe ser ejecutable. En Windows nativo, instala Git para Windows. Si un hook reporta que Git Bash no fue encontrado, el hook puede bloquear la llamada de herramienta hasta que Bash esté disponible. Los pasos de instalación cubren ese instalador.

/hooks lista lo que la sesión cargó. "disableAllHooks": true en un archivo de configuración desactiva hooks en ese nivel después de que se aplique la precedencia. Una política gestionada aún puede forzar hooks activados.

Si editas el archivo de configuración mientras una sesión está abierta, el observador de archivos generalmente recarga hooks por su cuenta.

Eventos que vale la pena conocer

La referencia documenta muchos eventos. Estos son los que los ejemplos usan, más los vecinos que verás en /hooks.

Evento Cuándo se dispara Matcher
PreToolUse Antes de que una herramienta se ejecute. Puede bloquearlo Nombre de herramienta: Bash, Edit, Write
PostToolUse Después de que una herramienta tenga éxito Nombre de herramienta
Notification Cuando Claude Code emite una notificación Tipo de notificación, incluido permission_prompt, idle_prompt, auth_success, agent_needs_input, agent_completed
Stop Cuando Claude termina una respuesta Ninguno. Siempre se ejecuta
SubagentStop Cuando un subagente termina Tipo de agente
UserPromptSubmit Cuando un prompt se envía, antes de que Claude lo maneje. También en turnos que Claude Code comienza a sí mismo Ninguno. Siempre se ejecuta

agent_completed solo se dispara mientras la vista de agente está abierta, así que es una señal pobre «la tarea está hecha». Usa Stop para el final de un turno.

También documentado, y dejado fuera de los scripts aquí: SessionStart, SessionEnd, Setup, PostToolUseFailure, PermissionRequest, PermissionDenied, SubagentStart, PreCompact, PostCompact, StopFailure, FileChanged, ConfigChange. Los matchers y campos JSON para esos viven en la referencia de hooks. Las llamadas a herramientas EndConversation omiten PreToolUse y PostToolUse.

Un matcher es una expresión regular JavaScript probada contra el campo, y una coincidencia en cualquier lugar del valor cuenta. Edit por lo tanto también coincide con NotebookEdit. Los patrones abajo usan ^Edit$|^Write$ así que solo esas dos herramientas ejecutan el script. La referencia acepta | entre alternativas en eventos de herramienta. Los ejemplos oficiales a menudo muestran la forma más corta Edit|Write. Usa la forma anclada cuando un nombre de herramienta más largo debe mantenerse fuera.

Stop y UserPromptSubmit ignoran matchers. Un matcher en esos eventos no los filtra.

Los campos comunes en stdin incluyen session_id, cwd y hook_event_name. Los eventos de herramienta añaden tool_name, tool_input y tool_use_id. Para Bash, la cadena de shell es tool_input.command. Para Edit y Write, la ruta es tool_input.file_path.

Exit codes y JSON

Exit code Resultado
0 Éxito. Un objeto JSON en stdout se aplica. Otro stdout es texto plano
2 Error bloqueante en eventos que pueden bloquearse. stderr es la razón, a menos que JSON ya haya llevado una razón de decisión
Cualquier otro Error no bloqueante. La acción continúa, y la transcripción muestra un error de hook

En PreToolUse, este JSON niega la llamada sin confiar en exit 2:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Protected path"
  }
}

permissionDecision puede ser deny, ask o allow. Exit 2 sigue bloqueando incluso si el JSON dice allow.

Stdout es JSON solo cuando es un objeto único y nada más se imprime. jq -n es una forma confiable de imprimir ese objeto. Dos líneas de registro antes del JSON convierten la salida en texto plano.

En PostToolUse, la herramienta ya ha terminado. Exit 2 no deshace la edición. Alimenta stderr de vuelta a Claude. Exit 0 stdout de PostToolUse va al registro de depuración, no al modelo. UserPromptSubmit es diferente: el stdout de texto plano se añade al contexto que Claude puede leer, y exit 2 rechaza el prompt.

Stop puede bloquearse y pedirle a Claude que siga adelante. Después de ocho bloques seguidos sin una llamada de herramienta entre ellos, Claude Code continúa de todas formas. Los scripts en este laboratorio salen con 0 en Stop así que solo notifican.

Cinco hooks

Los scripts asumen que jq está instalado (brew install jq o tu paquete de distro). Son envoltorios originales alrededor de los campos de stdin anteriores.

1. Formato con Prettier o Black

.claude/hooks/format-file.sh

#!/bin/bash
# Format the file Claude just wrote, by extension.
input=$(cat)
path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
[ -z "$path" ] && exit 0
[ -f "$path" ] || exit 0

case "$path" in
  *.js|*.jsx|*.ts|*.tsx|*.css|*.md|*.json|*.yml|*.yaml)
    npx prettier --write "$path" || exit 0
    ;;
  *.py)
    if command -v black >/dev/null 2>&1; then
      black "$path" || exit 0
    fi
    ;;
esac
exit 0

Los fracasos del formateador salen con 0 a propósito. Una instalación de Prettier faltante no debería parecer una edición bloqueada. Si quieres que Claude vea un fallo del formateador, sal con 2 e imprime el error en stderr en su lugar.

Regístralo en PostToolUse:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "^Edit$|^Write$",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-file.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

timeout está en segundos. El valor por defecto para un comando hook es 600, que es demasiado largo para un formateador. Un comando Bash que reescribe un archivo no coincide con Edit o Write. Un hook FileChanged es el evento que observa una ruta sin importar qué herramienta la escriba.

Prettier y Black ambos reescriben el archivo en disco después de la edición de Claude. La siguiente lectura ve la versión formateada.

2. Bloquea ediciones a archivos protegidos

.claude/hooks/protect-files.sh

#!/bin/bash
# Deny Edit/Write on secrets, lockfiles, and git internals.
input=$(cat)
path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
path=${path//\\//}

case "$path" in
  *.env|*.env.*|*/.env|*/.env.*)
    echo "Blocked: $path is an env file. Edit a sample template instead." >&2
    exit 2
    ;;
  */.git/*|*.git/*)
    echo "Blocked: $path is inside .git." >&2
    exit 2
    ;;
  *package-lock.json|*pnpm-lock.yaml|*yarn.lock|*poetry.lock|*Cargo.lock)
    echo "Blocked: $path is a lockfile. Update it through the package manager." >&2
    exit 2
    ;;
esac
exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^Edit$|^Write$",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

Claude recibe el texto de stderr y puede elegir otra ruta. Esto no detiene un comando Bash como rm .env o una redirección. Añade un segundo grupo PreToolUse con matcher ^Bash$ si las escrituras shell a esas rutas importan. Para una negación dura que aún sale con 0, imprime el JSON permissionDecision de la sección del exit code y sal con 0.

3. Registra comandos shell

.claude/hooks/log-bash.sh

#!/bin/bash
# Append each Bash command to a local log. Never block.
input=$(cat)
log="${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/bash-log.jsonl"
printf '%s\n' "$input" | jq -c '{t: (now | todate), cmd: .tool_input.command, cwd: .cwd}' >> "$log"
exit 0

Añade .claude/hooks/bash-log.jsonl a .gitignore. El registro contendrá lo que sea que Claude ponga en la cadena de comando, incluidos secretos si un comando los incrusta.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^Bash$",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-bash.sh"
          }
        ]
      }
    ]
  }
}

if puede reducir esto más en eventos de herramienta, usando sintaxis de regla de permisos como Bash(git *). Sin if, cada llamada Bash añade una línea. Un if que no coincide omite el proceso completamente.

4. Notifica cuando el turno termina

Stop no tiene matcher. Mantén el comando corto. El timeout por defecto es largo, y un notificador atrapado sostiene el final del turno.

macOS, después de que Script Editor está permitido notificar:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Turn finished\" with title \"Claude Code\"'",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Linux, cuando un demonio de notificación está corriendo:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' 'Turn finished'",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Windows PowerShell abre un diálogo, que puede caer detrás de la terminal:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "powershell.exe -NoProfile -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null; [System.Windows.Forms.MessageBox]::Show('Turn finished','Claude Code') | Out-Null\"",
            "timeout": 15
          }
        ]
      }
    ]
  }
}

notify-send no hace nada útil sobre SSH o en un contenedor. Prueba el comando en una terminal normal antes de ponerlo en configuración. Un hook Notification con matcher permission_prompt es el caso separado «Claude te está esperando», que es lo que quieres durante una pausa larga, no al final.

SubagentStop es el evento de coincidencia para un subagente terminando. El matcher es el nombre del agente, por ejemplo code-reviewer. Un hook Stop en la propia portada del agente se entrega como SubagentStop.

5. Ejecuta pruebas después de una edición

Ejecutar una suite completa después de cada pulsación de tecla es ruidoso. Este script se ejecuta solo para archivos de prueba Python y JavaScript, y solo cuando existe un punto de entrada conocido.

.claude/hooks/test-after-edit.sh

#!/bin/bash
# Re-run a narrow test file after Claude edits it.
input=$(cat)
path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
case "$path" in
  *test_*.py|*tests/*.py)
    pytest "$path" >&2 || exit 2
    ;;
  *.test.js|*.test.ts|*.spec.js|*.spec.ts)
    # Extra args are appended to the npm test script. Point this at the
    # runner binary if that script ignores a file path.
    npm test -- "$path" >&2 || exit 2
    ;;
esac
exit 0

Pytest escribe a stderr aquí así que un fallo es la razón de bloqueo que Claude lee. Exit 2 en PostToolUse no revierte el archivo. Le dice a Claude que la edición llegó y el comando de prueba falló.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "^Edit$|^Write$",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-after-edit.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

Reemplaza npm test con el ejecutor que tu repo ya documenta. Un comando slash llamado /test es el mejor ajuste cuando un humano debe decidir ejecutar la suite. El hook es para las ediciones que no quieres fusionar sin una comprobación roja/verde.

Un archivo de configuración

Fusiona los grupos. Dos grupos PostToolUse son normales. Los manejadores en un grupo se ejecutan cuando el matcher coincide.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^Edit$|^Write$",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" }
        ]
      },
      {
        "matcher": "^Bash$",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-bash.sh" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "^Edit$|^Write$",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-file.sh", "timeout": 30 },
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-after-edit.sh", "timeout": 120 }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "notify-send 'Claude Code' 'Turn finished'", "timeout": 5 }
        ]
      }
    ]
  }
}

Cambia el comando Stop por la línea macOS o Windows si no estás en Linux. Pídele a Claude que edite un archivo bajo src/ y confirma que el registro creció y el formateador se ejecutó. Pídele que edite .env y confirma que la llamada de herramienta es rechazada.

Los hooks son automatización local. No son un sustituto para CI, y no son un servidor MCP. Un servidor MCP añade herramientas que Claude puede llamar. Un hook envuelve herramientas que Claude ya tiene.

Preguntas frecuentes

¿Dónde van los hooks de Claude Code?

Añade un objeto hooks a .claude/settings.json para compartir hooks con el repo, o a ~/.claude/settings.json para ejecutarlos en cada proyecto. .claude/settings.local.json contiene anulaciones de proyecto personal e es ignorado por git cuando Claude Code guarda un ajuste allí. Ejecuta /hooks en una sesión para ver la lista fusionada. Establece disableAllHooks en verdadero para desactivar hooks en ese nivel de configuración.

¿Cuál es la diferencia entre exit code 0 y exit code 2?

Exit 0 significa éxito. Si stdout es un objeto JSON, Claude Code aplica esos campos. Exit 2 bloquea eventos que pueden bloquearse, y stderr es la razón que Claude ve. Cualquier otro exit code es un error no bloqueante: la herramienta sigue ejecutándose, a menos que ese hook esté configurado para que el fallo bloquee. En PreToolUse, un permissionDecision JSON válido puede denegar la llamada incluso cuando el proceso sale con 0.

¿Qué hook se ejecuta antes de que Claude edite un archivo?

PreToolUse, con un matcher para las herramientas Edit y Write. El script lee JSON en stdin. La ruta objetivo es tool_input.file_path. Exit 2 para cancelar la edición e imprime la razón en stderr. PostToolUse solo se ejecuta después de que la herramienta haya tenido éxito, así que es el lugar correcto para formatear, no para evitar la escritura.

¿Puede un hook ejecutar Prettier o Black?

Sí. Un hook de comando PostToolUse puede leer tool_input.file_path y ejecutar Prettier en archivos JavaScript y TypeScript o Black en archivos Python. El stdout del hook no se muestra a Claude en exit 0. Si el formateador falla y sales con 2, Claude ve stderr aunque la edición ya haya ocurrido.

¿Aceptan Stop y UserPromptSubmit un matcher?

No. Stop y UserPromptSubmit se disparan en cada ocurrencia. Los matchers se aplican a eventos de herramienta como PreToolUse y PostToolUse, donde el patrón se compara contra el nombre de la herramienta, y a Notification, donde se compara contra el tipo de notificación. SubagentStop compara el tipo de agente.

Cómo se verificó esta guía

Escrita a mano a partir de la documentación oficial, con cada comando comprobado de nuevo y una segunda revisión antes de publicar. Última revisión: .

Fuentes oficiales consultadas para este artículo

Share.

Los comentarios están cerrados.