Claude Code puede correr tus propios comandos en distintos puntos del ciclo de vida. Un hook PostToolUse sobre las herramientas de edición mantiene el formato consistente sin tener que recordarle al modelo que lo haga. Pero un hook mal hecho pelea con el modelo y te llena los diffs de ruido. Esta guía te arma un hook que solo formatea lo que cambió, sale limpio y no estorba.

En resumen
- Un hook es un comando de shell que Claude Code corre por sí solo en un punto del ciclo de vida, sin que el modelo tenga que cooperar.
- Usa PostToolUse con un matcher Edit|Write para que dispare solo después de editar archivos, no en cada llamada a una herramienta.
- Lee las rutas que cambiaron desde el JSON de stdin (la fuente confiable), no de una variable de entorno, y formatea solo esos archivos.
- Sal con 0 ante cualquier tropiezo del formateador para que uno roto jamás bloquee el trabajo de Claude; deja el exit 2 solo para errores reales.
- Haz commit de .claude/settings.json para que el hook aplique a todo el repo, y mantenlo rápido, porque los hooks corren en línea.
Pedirle a Claude que "no se olvide de correr Prettier" funciona hasta que deja de funcionar. Para la quinta edición ya se le olvidó, y ahora la mitad de tu diff la reformateó tu editor al guardar y la otra mitad no. El arreglo no es un mejor prompt; es sacar la regla de la cabeza del modelo y meterla en las herramientas. Un hook PostToolUse corre un formateador en el instante en que Claude toca un archivo, de forma determinista, siempre. Al terminar esta guía vas a tener un hook que formatea solo los archivos que cambiaron, nunca bloquea a Claude cuando el formateador tropieza, y queda con commit para que todos en el repo tengan el mismo comportamiento, más los errores de matcher y de exit code que convierten un hook útil en una máquina de generar ruido en los diffs.
Los hooks no son magia ni corren aislados: un hook es solo un comando de shell que Claude Code corre por ti, con tus permisos. Ese poder es justo la razón por la que importa acotarlo bien. Si lo ajustas, el formato se vuelve infraestructura invisible. Si lo dejas suelto, le acabas de dar a un proceso automático un cheque en blanco sobre tu working tree.
01 · Requisitos y cómo disparan los hooks en realidad
Antes de escribir nada, deja las piezas en su lugar. Nada de esto es pesado, pero el detalle que falta te cuesta una sesión entera de depuración.
- Claude Code instalado y autenticado. Corre claude en un directorio de proyecto y confirma que te aparece el prompt.
- Un formateador que funcione desde la CLI. Eso es prettier, ruff format, gofmt, rustfmt, lo que use tu stack. Córrelo a mano una vez primero; un hook no arregla un formateador que no está configurado.
- Un repo git, para que veas exactamente qué toca el hook y puedas revertirlo si la acotación quedó mal.
- Permiso de escritura en la raíz del proyecto, porque ahí es donde vive la config del hook, en .claude/settings.json.
Un hook es un comando atado a un evento del ciclo de vida. El que quieres es PostToolUse: corre después de que una herramienta termina bien, que es el momento justo para reformatear un archivo que Claude acaba de escribir. Un matcher decide qué herramientas lo disparan. Para formato, son las que escriben archivos, Edit y Write. Cuando el evento dispara, Claude Code corre tu comando y le pasa por stdin un JSON que describe lo que pasó, incluyendo qué rutas se tocaron. El exit code de tu comando es el canal de vuelta: cero significa "todo bien," y un código distinto de cero específico puede devolverle un error a Claude.
Nota
PostToolUse corre después de que la edición ya quedó escrita en disco. No puede vetar la edición; para eso está PreToolUse. Un hook de formato va en PostToolUse precisamente porque para ese momento ya hay un archivo real que formatear.
02 · El hook mínimo
Abre o crea .claude/settings.json en la raíz de tu proyecto y agrega un bloque de hooks:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/format.sh"
}
]
}
]
}
}
Aquí hay tres cosas haciendo el trabajo:
- La clave del evento. PostToolUse corre después de que una herramienta termina bien. Pon Edit|Write bajo un mismo matcher; el pipe es una alternación de regex, así que dispara con cualquiera de las dos herramientas.
- El matcher. Se compara contra el nombre de la herramienta. Déjalo solo en las que escriben archivos. Un matcher . (que machea cualquier cosa) dispararía tu formateador también después de cada read, cada grep y cada llamada a bash.
- El comando. Apuntar a un archivo de script en lugar de un one-liner inline mantiene el JSON legible y te deja probar el script por separado. Lo escribimos enseguida.
Puedes meter inline un one-liner que formatee todo el proyecto, pero no lo hagas: formatear cada archivo en cada edición es lento y reescribe código que Claude nunca tocó, lo que entierra el cambio real entre el ruido. Lo importante es formatear solo lo que cambió.
03 · Lee los archivos cambiados desde stdin
La forma robusta de saber qué cambió es el JSON que Claude Code le manda a tu hook por stdin. Para Edit y Write incluye el input de la herramienta, que trae la ruta del archivo. Leer eso es mejor que depender de una variable de entorno, porque la variable no está garantizada en toda herramienta ni en toda versión, mientras que el payload de stdin sí es el contrato documentado.
Crea .claude/hooks/format.sh y hazlo ejecutable:
#!/usr/bin/env bash
set -euo pipefail
# Claude Code manda un evento JSON por stdin. Saca de ahi la ruta del archivo editado.
payload="$(cat)"
file="$(printf '%s' "$payload" | jq -r '.tool_input.file_path // empty')"
# Sin ruta, o el archivo ya no existe? Nada que hacer, sal limpio para no bloquear nunca.
[ -z "$file" ] && exit 0
[ -f "$file" ] || exit 0
# Solo formatea lo que sabemos formatear. Lo demas se ignora en silencio.
case "$file" in
*.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md)
npx --no-install prettier --write --log-level warn "$file" || exit 0
;;
*)
exit 0
;;
esac
exit 0
Luego chmod +x .claude/hooks/format.sh. Varias decisiones de ese script valen la pena:
- Lee stdin una sola vez a una variable y después parsea con jq. El campo es .tool_input.file_path, así que protégelo con // empty para que un campo ausente quede como string vacío y no como el literal "null".
- Sale temprano y limpio cuando no hay archivo o la ruta no existe. Un hook que revienta en un caso borde es peor que no tener hook.
- Acota por extensión con un case, así un formateador que solo entiende archivos JS/TS nunca recibe un binario ni un .env.
- Usa --no-install para que el hook nunca dispare en silencio una descarga de paquete a mitad de una edición. Si Prettier no está, el comando falla y el || exit 0 se lo traga en vez de bloquearte.
Atención
Un hook corre con tu shell, tus credenciales, tu acceso al filesystem. No está aislado. Nunca dejes que el output del modelo decida qué comando se corre. La ruta del archivo viene del input de herramienta de Claude, así que ponla entre comillas ("$file") y valídala; una ruta que interpolas a ciegas dentro de un string de shell es una inyección de comandos esperando para suceder.
04 · Ejemplo resuelto: un loop limpio de formato al editar
Conéctalo todo y mira una edición fluir de punta a punta. Digamos que Claude reescribe un componente de React.
- Claude llama a Edit sobre src/components/Toolbar.tsx. La edición se aplica a disco.
- PostToolUse dispara. El matcher Edit|Write machea con Edit, así que Claude Code corre format.sh y le manda el JSON del evento.
- El script saca src/components/Toolbar.tsx de .tool_input.file_path, ve la extensión .tsx, y corre prettier --write sobre ese único archivo.
- Prettier normaliza comillas, espaciado y comas finales. El archivo en disco queda formateado. El script sale con 0.
- Claude sigue. Tu git diff muestra solo el cambio real más el formato consistente, no un reformateo de cuarenta archivos que no tienen nada que ver.
Ese último paso es toda la ganancia. Compáralo con la versión floja, un hook que corre prettier --write . en cada edición. Eso reformatea el proyecto entero, así que un cambio de una línea te aparece en el diff junto con un montón de cambios en archivos que Claude nunca abrió. Quien revisa no logra distinguir la intención entre tanto ruido, y para entonces ya te acostumbraste a ignorar el output del hook, con lo que deja de servir para algo.
Consejo
Prueba el script sin meter a Claude en el medio. Mándale un evento falso y mira qué hace: echo '{"tool_input":{"file_path":"src/foo.ts"}}' | .claude/hooks/format.sh. Si formatea foo.ts y sale con 0, sabes que todo quedó bien conectado antes de que Claude siquiera lo corra.
Cuándo el inline está bien
Para un proyecto pequeño con un solo formateador y un solo lenguaje, un comando inline en settings.json es aceptable. A ese tamaño la legibilidad no es problema. En el momento en que necesitas filtrar por extensión, manejar errores o más de un formateador, pásate a un script. El script además se versiona limpio y se puede cubrir con tests unitarios; un one-liner en JSON no.
05 · Exit codes, y por qué lo deciden todo
El exit code es la manera en que tu hook le contesta a Claude Code, y equivocarte ahí es la forma más común de que un hook de formato pase de útil a hostil.
- Exit 0 es éxito. Claude sigue normal. Esto es lo que un hook de formato debería devolver casi siempre, incluso cuando el formateador en sí falló, porque un punto y coma faltante en tu config de Prettier jamás debería congelar una sesión de edición.
- Exit 2 es un error que bloquea. Claude Code toma stderr como feedback y se lo muestra al modelo, que puede intentar arreglarlo. Reserva esto para casos en que de verdad quieres tratar la edición como incompleta, digamos, una regla de lint que tiene que pasar.
- Otro distinto de cero es un error que no bloquea: queda en el log, pero no le vuelve al modelo de la misma manera.
La trampa está en usar set -e (que sí usamos, por seguridad) y después olvidar que un formateador que falla aborta el script con un estado distinto de cero. Por eso cada llamada al formateador termina en || exit 0: convierte la falla del formateador en una salida limpia para que el hook se degrade con elegancia en vez de molestar a Claude con errores que no puede accionar. Decide a propósito qué fallas deben bloquear y cuáles tragarse. No dejes que set -e tome esa decisión por ti sin querer.
Importante
Haz commit de .claude/settings.json y de los scripts de .claude/hooks/ en el repo. Un hook que vive solo en tu máquina formatea solo tus ediciones; uno en control de versiones significa que cada clone, y cada compañero que corra Claude Code, recibe exactamente el mismo comportamiento. Esa consistencia es toda la razón para automatizar el formato en vez de dejárselo a un prompt.
Un buen hook de formato al editar es de esa clase de automatización que se te olvida que existe, que es justo la meta: acotado a los archivos que cambiaron, callado cuando funciona, inofensivo cuando falla, y compartido con todos en el repo. Empieza con el script de arriba, mira unas cuantas ediciones pasar por tu git diff, y ajusta la lista de extensiones a tu stack. Una vez que esté callado y confiable, el mismo patrón (leer stdin, acotar bien, salir limpio) es como vas a construir cada hook que venga después de este.
Puntos clave
- Saca el formato del prompt y métele un hook PostToolUse para que corra de forma determinista después de cada Edit y Write.
- Acota el matcher a Edit|Write y formatea solo el archivo que cambió; nunca reformatees el proyecto entero en cada edición.
- Lee la ruta del archivo del JSON de stdin (.tool_input.file_path con jq), ponla entre comillas y valídala; el hook no está aislado.
- Sal con 0 aun cuando el formateador falle para que un tropiezo no congele la sesión; reserva el exit 2 para chequeos donde de verdad quieras bloquear.
- Haz commit de .claude/settings.json y del script del hook para que todo el repo reciba el mismo formato, invisible e idéntico.
Preguntas frecuentes
¿Por qué usar un hook en vez de simplemente decirle a Claude que formatee en el prompt?
Porque un prompt es una petición que el modelo puede olvidar, sobre todo en una sesión larga. Un hook es infraestructura determinista: Claude Code lo corre después de cada edición que machea, se acuerde el modelo o no. El formato es una regla, no algo a criterio, así que va en las herramientas y no compitiendo por la atención del modelo.
¿PostToolUse o PreToolUse para formatear?
PostToolUse. La edición tiene que estar en disco antes de poder formatearla, y PostToolUse dispara después de una llamada exitosa. PreToolUse corre antes de la acción y puede bloquearla; eso es para guardas y validaciones (rechazar una edición a una ruta protegida), no para reformatear un archivo que todavía no existe.
¿El hook debería salir con un código distinto de cero cuando el formateador falla?
Casi nunca para formato. Sal con 0 aun cuando el formateador falle, para que una config rota o una dependencia faltante no congele una sesión de edición. El formato es cosmético y no debería bloquear el trabajo. Reserva el exit 2 para chequeos donde de verdad quieres poner una puerta, como una regla de lint que tiene que pasar antes de dar la edición por terminada.
¿Por qué leer la ruta del archivo desde stdin y no de una variable de entorno?
El payload JSON de stdin es el contrato documentado: siempre trae el input de la herramienta, incluida la ruta del archivo, sin importar la herramienta ni la versión. Una variable de entorno es cómoda pero menos confiable; puede no venir poblada para toda herramienta ni mantenerse estable a medida que Claude Code evoluciona. Parsear .tool_input.file_path con jq es el camino que aguanta.
¿Es seguro correr un comando de shell sobre una ruta que me da Claude?
Solo si tratas la ruta como input no confiable. Ponla entre comillas ("$file"), valida que existe, y nunca armes un comando concatenando el output del modelo, porque por ahí se cuela una inyección de comandos. El hook corre con todos tus permisos y sin sandbox, así que la misma disciplina que aplicarías a cualquier input de usuario aplica también aquí.
¿Esto no va a hacer lenta cada edición?
Solo si formateas más de lo necesario. Los hooks corren en línea, así que el costo es lo que tarde tu comando. Formatear un solo archivo cambiado con Prettier es rápido; correr prettier --write . sobre todo el proyecto en cada edición no lo es, y encima reescribe archivos que ni tocaste. Acota a la ruta que cambió y la latencia es despreciable.
¿Prefieres que lo hagamos por ti?
Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.
Escríbenos por WhatsAppPrimera conversación gratis. Te responde el fundador.
Recursos relacionados

Crea un comando slash personalizado en Claude Code (de esos que el equipo sí usa)
Cuando reescribes las mismas instrucciones en Claude Code por décima vez, ya no estás escribiendo prompts: estás capturando datos. Un comando slash convierte ese prompt repetido en una plantilla versionada, que recibe argumentos y vive dentro de tu repo. Esta guía arma uno desde cero, cubre argumentos, referencias a archivos e inyección de bash, y luego te muestra los errores de alcance y de diseño que vuelven peso muerto un comando que era útil.

El skill que describe PRs: convierte un diff en una descripción que el revisor sí va a leer
Un skill empaquetado de Claude Code lee el diff contra tu rama base y redacta la descripción del pull request: qué cambió, por qué y cómo probarlo paso a paso. Así tu revisor deja de adivinar tu intención a partir del código pelado. Qué hace, cuándo debe dispararse, cómo funciona por dentro y lo único que jamás va a saber y que te toca poner a ti.

El skill de mensajes de commit: escribe el porqué, no el diff
Un skill empaquetado de Claude Code lee el diff que tienes en stage y escribe un commit convencional cuyo cuerpo explica el razonamiento, no las líneas que cambiaron. Qué hace, cuándo debe activarse, cómo funciona por dentro, una corrida real con su salida, la config que importa y el único hábito que decide si tu git log vale la pena dentro de seis meses.