Hooks en Claude Code 2026: configuración paso a paso y 7 ejemplos listos para usar
Markdown7 hooks listos para usar en .claude/settings.json, preparados para copiar y pegar: protección de .env, prettier, force push a main, guiones largos y un servidor de desarrollo. 4 tipos de manejadores y 31 eventos.
Leer el artículo completo
Los hooks en Claude Code son comandos de shell, llamadas HTTP o comprobaciones breves con LLM que se ejecutan automáticamente antes o después de una acción de Claude. Añaden comprobaciones a eventos concretos, formatean después de editar e insertan contexto al iniciar. Un PreToolUse coincidente puede rechazar esa llamada; los ejemplos de expresiones regulares son controles prácticos, no un límite de seguridad sobre toda ruta de archivos o Git. A continuación encontrarás un desglose de los 4 tipos de manejadores y los 31 eventos del ciclo de vida, una prueba para determinar «cuándo corresponde incluir una regla en un hook», 7 hooks listos para copiar en .claude/settings.json y 3 errores que me he encontrado personalmente.
Documentación de Ailoot: API, claves y configuración.
¿Qué son los hooks en Claude Code?
Punto clave
Un hook es un comando de shell, una llamada HTTP, un prompt breve de LLM o un subagente que Claude Code ejecuta en respuesta a un evento específico del ciclo de vida. A diferencia de las instrucciones de CLAUDE.md, que el modelo puede ignorar u olvidar, un hook se ejecuta siempre de la misma manera: es código, no una sugerencia.
Durante medio año mantuve la regla «nunca editar .env directamente» en CLAUDE.md. Claude la incumplía una vez al mes. No lo hacía con mala intención; simplemente la olvidaba al final de una sesión larga. Una vez acabó un token adicional en .env; otra vez, una variable con un nombre duplicado sobrescribió la que estaba en uso.
Trasladé esa regla a un hook, para que fuera imposible olvidarla. Cuando una herramienta de edición directa coincidente aporta una ruta .env, el hook rechaza la llamada. Redirección Bash, scripts, MCP y otras rutas necesitan controles separados; el ejemplo no vuelve inmutable el archivo.
Los hooks son comandos de shell, endpoints HTTP o prompts de LLM que defines y que se ejecutan automáticamente en puntos específicos del ciclo de vida de Claude Code.
En otras palabras, un hook es un comando tuyo que Claude Code ejecuta ante un evento específico. Cada edición de archivo, lanzamiento de Bash, inicio de sesión o finalización de un subagente es un evento al que puedes asociar un hook.
Por qué esto importa para quienes programan con vibe coding:
- Proteger de ediciones accidentales los archivos que contienen secretos.
- Ejecutar prettier/eslint automáticamente después de cada generación de código.
- Bloquear
git push --forcea main sin permiso explícito. - Insertar contexto actualizado al principio de cada sesión.
- Registrar las acciones de Claude para auditarlas.
CLAUDE.md se ocupa del contexto. Los hooks se ocupan de las acciones. Son dos cosas distintas, y confundirlas es el principal error de los principiantes.
¿En qué se diferencian los hooks de CLAUDE.md y los skills?
Punto clave
CLAUDE.md es texto que el modelo lee al inicio de una sesión e intenta seguir. Un hook es un comando de shell que el proceso de Claude Code ejecuta en respuesta a un evento, independientemente del modelo. Un skill es un paquete de instrucciones con nombre que Claude puede invocar con la herramienta Skill, como si fuera una función. Los tres sirven para distintos tipos de tareas.
La documentación de Anthropic explica expresamente la diferencia entre CLAUDE.md y los hooks:
El contenido de CLAUDE.md se entrega como un mensaje del usuario después del prompt del sistema, no como parte del prompt del sistema. Claude lo lee e intenta seguirlo, pero no hay garantía de que lo cumpla estrictamente.
Dicho de otro modo, el contenido de CLAUDE.md le llega a Claude como el primer mensaje del usuario, no como una instrucción del sistema. El modelo lo lee e intenta seguirlo. Pero no hay garantías, sobre todo al final de una sesión larga, cuando el contexto crece y las instrucciones anteriores quedan desplazadas.
Un hook funciona de otra manera. Está en settings.json, no depende del modelo y lo ejecuta el proceso de Claude Code ante un evento específico: PreToolUse, PostToolUse, SessionStart y otros. El comando se ejecuta antes de que Claude reciba el resultado y puede bloquear físicamente una acción (exit 2) o modificar un archivo después de editarlo (exit 0).
Comparación:
| Característica | CLAUDE.md | Hook | Skill |
|---|---|---|---|
| Dónde está | CLAUDE.md en la raíz del proyecto | .claude/settings.json | Carpeta .claude/skills/ |
| Quién lo ejecuta | El modelo lo lee como parte de un mensaje del usuario | El proceso de Claude Code ante un evento | El modelo mediante la herramienta Skill |
| Garantía de ejecución | No (mejor esfuerzo) | Un hook de comando configurado y coincidente ejecuta código; los fallos y códigos dependen del evento | Mediante una llamada al modelo (con autorización, pero no a nivel de shell) |
| Para qué sirve | Contexto, tono, estilo y enrutamiento del proyecto | Restricciones, formato y comprobaciones de archivos específicos | Procedimientos complejos de varios pasos con parámetros |
| Cuándo se ejecuta | En cada mensaje del modelo durante una sesión | En un evento del ciclo de vida | Cuando lo invoca el modelo |
| ¿Puede bloquear una acción? | No | Sí (exit 2) | No |
La plantilla completa de CLAUDE.md y 6 reglas para mantener un archivo vivo están en la guía Cómo configurar CLAUDE.md en 2026: una plantilla lista para usar y 6 reglas. Descubre los skills como capacidades con nombre en Claude Code Skills: cómo crear tu propia biblioteca.
Un hook es el nivel más bajo: sin modelo, sin interpretación, solo shell.
Los hooks son una de las herramientas de ingeniería del contexto, junto con Plan Mode y /compact.
4 tipos de hooks: Command, HTTP, Prompt, Agent: ¿cuál elegir?
Punto clave
Claude Code tiene 4 tipos de manejadores. Command es un script de shell, el tipo más habitual. HTTP envía una solicitud POST a tu URL, para equipos e integraciones. Prompt hace una llamada breve a Haiku para una comprobación semántica. Agent crea un subagente con acceso a herramientas para una validación exhaustiva. Los principiantes necesitan Command y Prompt; cubren el 90% de las tareas.
| Tipo | Qué hace | Cuándo elegirlo | Complejidad |
|---|---|---|---|
| Command | Ejecuta un comando de shell, recibe JSON por stdin y responde con un código de salida y stdout | 90% de los casos: comprobar rutas, dar formato, buscar en el contenido con grep, lsof, operaciones de Git | baja |
| HTTP | Envía una solicitud POST con un cuerpo JSON a tu URL y recibe JSON como respuesta | Cuando una regla corresponde a un equipo/CI: validación centralizada, panel de estado compartido, auditoría remota | media (requiere un servidor) |
| Prompt | Envía un prompt breve a Haiku (de forma predeterminada) y obtiene un sí/no para una evaluación semántica | Cuando grep no basta: «evalúa si este texto suena corporativo» | baja |
| Agent | Crea un subagente con acceso a Read/Grep/Glob para comprobaciones de varios pasos | Validación exhaustiva: «encuentra todos los archivos modificados y comprueba que existan las pruebas correspondientes» | alta |
El error más común es intentar resolver con Command algo que requiere Prompt. Por ejemplo, «no uses guiones largos en archivos .md» se puede comprobar con grep, así que Command es ideal. Pero «no escribas con tono corporativo» es una cuestión semántica, por lo que necesita Prompt.
Si estás empezando, usa Command. Los 7 hooks listos para usar que aparecen a continuación son todos de tipo Command. HTTP/Prompt/Agent vendrán después, cuando estés preparado.
¿Dónde está settings.json y cuáles son los 4 niveles de configuración?
Punto clave
Claude Code busca la configuración en 4 lugares: empresa (implementada mediante MDM), usuario (~/.claude/settings.json), proyecto (.claude/settings.json en la raíz del repositorio) y local (.claude/settings.local.json, no se incluye en Git). Los 4 archivos se combinan al inicio de la sesión y prevalece el nivel inferior.
| Nivel | Ruta | Cuándo se aplica | ¿En Git? |
|---|---|---|---|
| Empresa | /Library/Application Support/ClaudeCode/settings.json (Mac), /etc/claude-code/settings.json (Linux) | En todo el equipo mediante MDM | no |
| Usuario | ~/.claude/settings.json | Todos los proyectos de un usuario | no |
| Proyecto | .claude/settings.json en la raíz del repositorio | Este proyecto, para todo el equipo | sí |
| Local | .claude/settings.local.json | Solo este proyecto y este usuario | no (en .gitignore) |
Guardo los 7 hooks descritos a continuación en el archivo .claude/settings.json del proyecto, para que se incluyan automáticamente al clonar el repositorio y Claude siga las mismas reglas en una ventana nueva. Los hooks experimentales personales van en .claude/settings.local.json, para que no interfieran con el equipo.
Los 4 archivos se combinan al inicio de la sesión. Si las reglas entran en conflicto, prevalece el nivel inferior (local sobre proyecto, proyecto sobre usuario).
La forma más rápida de crear tu primer hook es usar el menú interactivo /hooks directamente en Claude Code. Elige un evento de la lista, selecciona un matcher (por ejemplo, Bash o Edit) y pega un comando. Claude Code escribirá JSON válido en settings.json.
31 eventos del ciclo de vida: ¿cuáles necesitan los principiantes?
Punto clave
Claude Code tiene actualmente 31 eventos del ciclo de vida, divididos en 3 frecuencias: una vez por sesión (SessionStart/SessionEnd), una vez por turno (UserPromptSubmit/Stop) y en cada llamada a una herramienta (PreToolUse/PostToolUse). Los principiantes necesitan 4 de los 31 eventos: PreToolUse para restricciones, PostToolUse para ediciones automáticas, SessionStart para el contexto y Stop para las comprobaciones finales.
Los eventos se producen con tres frecuencias: una vez por sesión (SessionStart, SessionEnd), una vez por turno (UserPromptSubmit, Stop, StopFailure) y en cada llamada a una herramienta dentro del bucle agéntico (PreToolUse, PostToolUse).
Es importante entender estas tres frecuencias para no confundirse:
- Una vez por sesión -
SessionStart,SessionEnd,Setup. Se ejecutan una vez cuando Claude Code se abre y se cierra. Úsalos para inicializar el contexto y limpiar archivos temporales. - Una vez por turno -
UserPromptSubmit,UserPromptExpansion,Stop,StopFailure. Se activan con cada uno de tus mensajes y cada vez que el modelo se detiene. Úsalos para registrar la conversación y comprobar el resultado final. - En cada llamada a una herramienta -
PreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest,PermissionDenied,PostToolBatch,SubagentStart,SubagentStop. Se activan con cada edición de archivo, lanzamiento de Bash, lectura o búsqueda. Esta es la capa más activa; aquí van las restricciones, el formato y las validaciones.
La lista completa de 31 eventos incluye eventos asíncronos/reactivos: TeammateIdle, PreCompact, PostCompact, Elicitation, ElicitationResult, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Notification. Son para casos avanzados; déjalos de lado por ahora.
Prueba de cinco preguntas: ¿cuándo corresponde incluir una regla en un hook y cuándo debe quedarse en CLAUDE.md?
Punto clave
Si una regla se refiere a una acción (no al contexto), afecta a la seguridad/dinero/datos, se puede comprobar con grep/regex, es difícil de deshacer si se incumple y Claude ya la ha incumplido al menos una vez, corresponde ponerla en un hook. Si menos de tres de estas cinco respuestas son «sí», se queda en CLAUDE.md. Apliqué la prueba a mis 80 líneas y pasé 5 reglas a hooks.
Para mí, esto es una ley, no una sugerencia. Reduje la decisión entre CLAUDE.md y un hook a una sencilla prueba de cinco preguntas. Si al menos tres respuestas son «sí», la regla va en un hook. Si hay menos de tres, se queda en el texto.
La prueba «¿cuándo corresponde incluir una regla en un hook?»
- ¿Esta regla trata de una acción, en lugar de contexto? («no ejecutes
rm -rf» es una acción; «tengo un proyecto TypeScript» es contexto.) - ¿Esta regla trata de seguridad, dinero o datos? («no toques
.env», «no publiques claves».) - ¿Se puede comprobar con grep, un regex o el código de salida de un comando de shell? («después de editar
.md, comprueba que no haya ningún guion largo».) - Si se incumple la regla, ¿serían difíciles de deshacer las consecuencias? («hacer force push a main» es difícil de deshacer; «olvidar un punto al final» es fácil.)
- ¿Ya has visto a Claude olvidar esta regla al menos una vez?
Ejemplo: «No uses guiones largos; utiliza solo guiones cortos». ¿Es una acción? Sí. ¿Seguridad? No. ¿Se puede comprobar con grep? Sí. ¿Es difícil de deshacer? No. ¿Ya se ha incumplido? Sí. 3 de 5: ponlo en un hook.
Contraejemplo: «Tengo vibe coders, no desarrolladores; escribe sin jerga». ¿Es una acción? Más bien contexto. ¿Seguridad? No. ¿Se puede comprobar con un regex? Demasiado difícil. ¿Es difícil de deshacer? No. ¿Ya se ha incumplido? Sí. 1 de 5: déjalo en CLAUDE.md.
Cinco de mis reglas superaron la prueba. Pasaron a hooks. El 75% restante se quedó en el archivo de texto.
7 hooks listos para copiar
Punto clave
A continuación hay 7 hooks listos para usar en .claude/settings.json: bloquear ediciones de .env, ejecutar prettier automáticamente, impedir los force push a main, detectar guiones largos, liberar un puerto antes de npm run dev, hacer commit automáticamente después de pruebas satisfactorias e insertar contexto actualizado al inicio de una sesión. Copia cada uno en el archivo, reinicia Claude Code y funcionará.
Los 7 hooks están guardados en un único archivo .claude/settings.json en la raíz del proyecto. Cada entrada tiene un tipo de evento, un filtro de herramienta (matcher) y un comando de shell. Exit 2 rechaza una llamada PreToolUse coincidente antes de ejecutarse. En PostToolUse, la acción ya ocurrió: el hook informa y no puede deshacerla.
En la mayoría de los eventos de hook, solo el código de salida 2 bloquea una acción. Claude Code trata el código de salida 1 como un error no bloqueante y continúa la acción, aunque en Unix normalmente signifique fallo.
exit 0 significa éxito. exit 1 significa error, pero Claude continúa (este es el primer antipatrón que aparece a continuación). exit 2 depende del evento: puede bloquear PreToolUse, mientras PostToolUse solo informa sobre una acción ya realizada.
Hook 1. Proteger ediciones directas de .env (PreToolUse, exit 2)
Bloquea los intentos de editar directamente cualquier archivo que coincida con .env, .env.local, .env.production, etc.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Do not edit `.env` directly. Tell me which variable to add, and I’ll open the file myself.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Qué hace: Antes de cada Edit/Write/MultiEdit, obtiene la ruta del archivo, comprueba con grep si coincide con el patrón .env y, si es así, devuelve el código de salida 2 con un mensaje. Claude ve stderr y me dice: «He intentado editar .env, pero el hook me ha bloqueado; dime qué variable debo añadir».
Hook 2. Ejecutar prettier automáticamente después de cada edición de código (PostToolUse, exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
}
]
}
]
}
}
Qué hace: Después de cada Edit/Write, obtiene la ruta y, si la extensión es .ts/.tsx/.js/.jsx/.json, ejecuta prettier en segundo plano con &. Siempre termina con exit 0 porque esto es formato, no una restricción. Si prettier falla, no pasa nada; el archivo se queda como estaba.
La diferencia con el hook 1 es fundamental: PreToolUse + exit 2 supone un bloqueo estricto antes de la acción. PostToolUse + exit 0 hace una edición ligera después de la acción. Son dos modos distintos para tareas distintas.
Hook 3. Bloquear git push --force a la rama main (PreToolUse, exit 2)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Force push to main is forbidden. Create a new branch or ask me to do it manually.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Qué hace: En cada lanzamiento de Bash, comprueba si el comando incluye push, incluye --force/--force-with-lease/-f y menciona main. Si se cumplen las tres condiciones, lo bloquea con un mensaje claro.
Antes tenía esta regla escrita en mayúsculas en CLAUDE.md. Claude aun así la incumplía más o menos una vez cada dos semanas cuando le pedía «hacer force push para deshacer mi commit incorrecto». Este hook detecta las formas literales mostradas, pero alias, scripts, rama destino implícita u otro cliente Git pueden eludir la cadena. Impón protección de main en el remoto y limita las credenciales del agente.
Hook 4. Bloquear guiones largos en archivos de texto (PostToolUse, exit 2)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"File $FILE contains an em dash (U+2014). Replace it with a regular hyphen (U+002D).\" >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Qué hace: PostToolUse con exit 2 informa a Claude de un fallo posterior a la edición. No revierte ni impide la escritura. Si Claude edita un archivo y deja un guion largo, el hook falla, Claude recibe comentarios y lo vuelve a intentar. Tras 2–3 sesiones, Claude deja de insertar este carácter por completo.
Ten en cuenta que grep busca específicamente el carácter de guion largo (U+2014) mediante $'\xe2\x80\x94', la notación de Bash para escribirlo como bytes UTF-8. Si escribes simplemente grep -q '-', también encontrará los guiones cortos y se ejecutará en todos los archivos. Un carácter invisible puede romper toda la lógica.
Hook 5. Liberar el puerto 3000 antes de npm run dev (PreToolUse, exit 0)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Port 3000 freed before launch' >&2; }; exit 0"
}
]
}
]
}
}
Qué hace: Antes de cada npm run dev (o pnpm/yarn), mata el proceso que ocupa el puerto 3000. Siempre devuelve exit 0 porque es una preparación, no una restricción. Así se evitan ciertos problemas en los que Claude ve «port in use», cree que hay un problema de código y empieza a arreglar cosas, cuando en realidad solo seguía en ejecución el servidor anterior.
Hook 6. Hacer commit automáticamente después de que las pruebas pasen (PostToolUse, exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "INPUT=$(cat); CMD=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty') || exit 1; EXIT=$(printf '%s' \"$INPUT\" | jq -r '.tool_response.exit_code // empty') || exit 1; case \"$CMD\" in 'npm test'|'pnpm test'|'yarn test') ;; *) exit 0 ;; esac; [ \"$EXIT\" = \"0\" ] || exit 0; git diff --cached --quiet || { echo 'Existing staged changes: review manually' >&2; exit 0; }; git add -- src/orders.ts tests/orders.test.ts || exit 1; git diff --cached --quiet && exit 0; git commit -m 'tests: checkpoint after successful run'"
}
]
}
]
}
}
Qué hace: Lee JSON de stdin una vez y admite solo los comandos de prueba simples exactos mostrados. Continúa únicamente si tool_response.exit_code es explícitamente cero; si la versión instalada no expone ese campo, omite el commit sin suponer éxito. Sustituye src/orders.ts y tests/orders.test.ts por los archivos exactos de la tarea. Si hay cambios preparados, se detiene para no incluir trabajo ajeno. Un add fallido la detiene, un diff vacío no crea commit y siguen activos los hooks normales. Prueba sin conexión entradas correctas, fallidas y sin resultado antes de activarlo.
Antipatrón: añadir git commit --no-verify a cada Edit. Mantén activas las comprobaciones de commit. Si una activa esta automatización recursivamente, acota el activador o rediseña la comprobación; no omitas la verificación por defecto. Revisa el diff preparado y la propiedad de archivos antes de activar commits automáticos.
Hook 7. Insertar contexto actualizado al inicio de la sesión (SessionStart con additionalContext)
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Current branch: $BRANCH. Uncommitted changes:\\n$STATUS\\\"}\""
}
]
}
]
}
}
Qué hace: Al inicio de la sesión, recopila la rama actual de Git y la lista de cambios sin confirmar, y luego devuelve JSON con un campo additionalContext. Claude Code inserta este texto en el primer mensaje del sistema, así que Claude sabe desde el principio dónde está y qué queda pendiente de la sesión anterior.
Usa additionalContext para información que Claude debe conocer sobre el estado actual del entorno o una operación recién completada: estado del entorno (rama actual, destino del despliegue, indicadores de funcionalidad activos), reglas condicionales del proyecto (qué comando de prueba corresponde al archivo que acabas de editar), datos externos (incidencias abiertas que tienes asignadas, resultados recientes de CI).
Posibles ampliaciones de este hook: obtener el último commit y su descripción, el estado de GitHub Actions, el sprint activo en Linear/Jira y las incidencias abiertas del repositorio actual.
3 errores de los hooks que le ocurren a una de cada dos personas
Punto clave
El primer error: un hook con exit 1 deja pasar la acción en silencio, y Claude ignora el error. El segundo: el trabajo pesado de un hook se interrumpe al cumplirse un tiempo de espera de 60 segundos. El tercero: un hook solo ve las variables de entorno de ~/.zshrc, no el .env del proyecto. Cada problema se arregla con una línea, pero si no conoces estos errores, puedes pensar que «los hooks no funcionan».
Error 1. Un hook con exit 1 deja pasar la acción en silencio
Es el más peligroso. Durante una semana pensé que mi protector de .env funcionaba, pero el hook tenía jq -r '.tool_input.file_path' sin comprobar si el valor era null. Cuando tool_input estaba vacío, jq terminaba con el código 1. Claude interpretaba exit 1 como «error del hook, continúa», así que la acción se ejecutaba. No entendía por qué seguían tocando .env. Solo vi la causa cuando ejecuté manualmente el hook en una terminal con la entrada vacía.
La solución es una regla: empieza cada hook comprobando si el valor es null y termínalo con || exit 0. Así, cualquier salida inesperada provoca una salida «segura», en lugar de dejar pasar la acción en silencio.
# Safe start and end template:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... your logic ...
exit 0 # or || exit 0 on one line
Error 2. Los hooks lentos se interrumpen al agotarse el tiempo de espera
Tenía un hook que ejecutaba eslint --fix en todo el archivo después de Edit. Los archivos grandes (1000+ líneas) tardaban 8–10 segundos en procesarse. El tiempo de espera predeterminado de los hooks de Claude Code es de 60 segundos, así que pensaba que estaba dentro del límite. Pero cuando se ejecutan varios Edit en un minuto, los hooks se acumulan y quedan en cola, y algunos simplemente se omiten. Solo me di cuenta cuando el registro mostró «hook timed out».
Solución: si un hook requiere mucho trabajo, ejecútalo en segundo plano. eslint --fix "$FILE" & sin esperar el resultado. Así, el hook devuelve exit 0 en milisegundos mientras el trabajo real se ejecuta en segundo plano.
# Heavy operation in background:
npx eslint --fix "$FILE" &
exit 0
Error 3. Un hook no puede ver las variables de entorno del proyecto
Pensaba que process.env estaría disponible en un hook igual que en una aplicación Node.js. No es así. El hook se ejecuta en un shell creado por el proceso principal de Claude Code y solo ve lo que está en ~/.zshrc o ~/.bashrc, además de lo que Claude Code le pasa explícitamente. Si tu hook depende de OPENAI_API_KEY de .env, tendrás que leer .env manualmente.
Solución: lee .env expresamente en el comando del hook.
# Passing project env into hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Or with dotenv:
dotenv -e .env -- node my-validator.js
Esto es especialmente molesto en un monorepo, donde los distintos paquetes tienen sus propios archivos .env. Resuélvelo pasando PWD y ejecutando explícitamente cd "$PWD" al principio del comando.
¿Cómo probar los hooks antes de hacer commit?
Punto clave
Un hook es un comando de shell. Puedes ejecutarlo manualmente en una terminal con una entrada stdin ficticia, comprobar su código de salida y stderr, y hacer todo esto antes de que lo ejecute Claude Code. Así ahorrarás horas de depuración y evitarás hooks que bloqueen su propia operación.
Escenario básico de prueba. Toma el comando del hook del JSON, abre una terminal y pásale el JSON que enviaría Claude Code:
# Test hook 1 (protect .env):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
echo "BLOCKED" >&2
exit 2
} || exit 0
'
echo "Exit code: $?"
Deberías ver BLOCKED en stderr y Exit code: 2. Si obtienes exit 0, el regex no funciona; corrige grep.
Escenarios que comprobar:
- Caso esperado - el archivo debe quedar bloqueado.
.envpara el hook 1,git push --force mainpara el hook 3. - Caso negativo - el archivo no debe quedar bloqueado.
README.mdpara el hook 1,git push origin feature-branchpara el hook 3. - Casos límite - entrada vacía, valores null, caracteres inesperados. El hook debe devolver exit 0 o fallar con un error claro, no dejar pasar la acción en silencio.
Dónde poner los hooks experimentales. No en .claude/settings.json, sino en .claude/settings.local.json (fuera de Git). Pon allí los hooks nuevos bajo PostToolUse, usando echo en lugar de la acción real; comprueba si se activan y luego pásalos al settings.json de producción.
// Experiment in settings.local.json - writes to /tmp/hook-debug.log:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
}
]
}
]
}
}
Después de 5–10 ediciones en Claude Code, abre /tmp/hook-debug.log para ver qué archivos activaron el hook. Si está vacío, el matcher no encaja; corrige el regex.
¿Qué sigue?
Punto clave
Empieza con hooks PostToolUse que devuelvan exit 0; son los más seguros y no bloquean nada. Después de una semana, añade tu primer hook PreToolUse con exit 2 para proteger .env o impedir los force push. Al cabo de dos semanas, CLAUDE.md pasará de ser un contrato de 200 líneas a una descripción breve del contexto de 50–80 líneas, con comprobaciones de eventos seleccionadas en hooks y límites de acceso obligatorios impuestos por separado mediante permisos, sandbox y protección remota.
5 pasos para la próxima semana:
- Busca en tu
CLAUDE.mdpalabras como «nunca», «prohibido», «siempre» y «debe». Cada regla de ese tipo puede convertirse en un hook. Si una regla es imperativa, el archivo de texto no la hará cumplir. - Empieza con PostToolUse + exit 0. Son los más seguros: hacen cambios, pero no bloquean nada. prettier (hook 2), cerrar el servidor de desarrollo (hook 5), hacer commit automáticamente después de las pruebas (hook 6).
- Añade tu primer PreToolUse + exit 2 solo cuando tengas confianza. Por ejemplo, protege
.env(hook 1) o impide los force push (hook 3). Es un bloqueo estricto, y un error en el comando grep puede bloquearte a ti. - Prueba los hooks manualmente antes de hacer commit (consulta la sección anterior). Ahorrarás horas de depuración.
- Reserva
settings.local.jsonpara experimentos. Pon allí los hooks nuevos con echo en lugar de la acción real.
Una vez configurados tus hooks, el siguiente paso de esta serie es cómo deshacer cambios en Claude Code, por si un hook deja pasar algo peligroso. Si quieres que Claude recuerde la estructura de tu proyecto entre sesiones, crea un Second Brain en Claude Code (carpeta business/ + base de conocimiento enlazada). Y si quieres poner nombre a los skills de tu proyecto para poder reutilizarlos, consulta Claude Code Skills.
Fuentes
Si necesitas acceder a redes neuronales a través de Ailoot, echa un vistazo a Ailoot.
Más detalles
Preguntas sobre Claude Code
¿Por qué debe leer un hook su JSON de entrada una sola vez?
Stdin es un flujo: el primer jq lo consume y las siguientes lecturas llegan a EOF. Guarda la entrada una vez y extrae de ese valor comando y resultado. Un resultado ausente o desconocido no está verificado y debe omitir el commit automático.
¿Los hooks de expresiones regulares crean un límite de seguridad?
No. Comprueban eventos concretos y cadenas reconocibles. Redirecciones, alias, scripts, MCP u otras rutas pueden eludirlos; usa credenciales restringidas, permisos de archivos, sandbox y ramas protegidas para límites obligatorios.