← Volver al Blog

Claude Code hooks: 3 decisiones invisibles

La primera vez que enchufé un hook PreToolUse a Claude Code no fue para bloquear nada. Fue por curiosidad, aunque en aquel momento me convencí de que era “auditoría profesional”.

Yo daba una instrucción alta (“arregla el bug de login”), Claude respondía con un diff, yo aceptaba y seguíamos. En medio pasaban decisiones enteras que nunca aparecieron en el chat: qué archivos abrir, cuándo delegar a un subagente, cuándo escribir en disco. Yo firmaba resultados, no procesos.

Este artículo no va sobre bloquear a Claude. Va sobre convertir esos pasos silenciosos en líneas de log que puedes leer con tail -f. El hook cabe en un bloque JSON corto y se instala sin reiniciar nada.

Lo que “sin pedir permiso” quiere decir

Claude Code aplica una política de permisos por herramienta y, encima, un modo (default, acceptEdits, auto, bypassPermissions, plan) que decide cuándo pedirte confirmación. En cualquier modo distinto del manual, la mayoría de las acciones internas de una respuesta no piden confirmación individual. Eso incluye pasos que sí escriben en disco o que abren un subagente, no solo pasos “de lectura”.

La palabra clave del docs oficial es PreToolUse. Es un evento que se dispara antes de cada invocación de una herramienta, con acceso al tool_name y al tool_input completo en JSON por stdin. Si tu hook devuelve exit 0, la herramienta se ejecuta. Si devuelve exit 2, se bloquea. Si además escribes a un archivo desde ahí, el registro es tuyo.

Cuando digo “sin pedir permiso” no quiero decir “a escondidas”. Quiero decir: el runtime aplica su política sin interrumpirte, y esa política emite decisiones que no vas a repasar de una en una en el chat. El hook las guarda para cuando decidas repasarlas de golpe.

El hook mínimo

Este es el bloque que puse en ~/.claude/settings.json. Loguea cada llamada a Read, Write y Edit a un archivo local, sin bloquear nada:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '\"[\\(now|todate)] \\(.tool_name) \\(.tool_input | tostring | .[0:120])\"' >> ~/.claude/audit.log"
          }
        ]
      }
    ]
  }
}

Dos piezas hacen el trabajo. El matcher filtra por nombre de herramienta, así que el hook no se dispara con cada mensaje. El command recibe el evento por stdin en JSON: jq extrae el timestamp, el tool_name y los primeros 120 caracteres del tool_input, y todo eso va anexado a audit.log.

El hook no cubre subagentes. Para eso hay un evento aparte llamado SubagentStart que se registra con la misma forma pero cambiando la clave. Lo dejé fuera del bloque de arriba para no mezclar dos eventos en una lección. Más abajo describo qué mira cuando lo enchufas.

Tres decisiones invisibles de Claude Code: qué se lee, cuándo se abre un subagente, qué se escribe

Decisión 1: qué lee Claude cuando le pides “cualquier cosa”

La primera fila de mi log fue casi todo Read. Y no Read de los archivos que yo había nombrado.

Al pedir “arregla el bug de login”, el log mostraba lecturas a CLAUDE.md, a archivos de configuración vecinos y a un README.md que yo había olvidado que existía. Ninguna era sorpresa técnica (la documentación describe que Claude Code carga contexto de su entorno), pero verlas en secuencia, con timestamps, cambia la percepción. Yo sospechaba que “leía cosas”. Ahora abro el log y sé cuáles.

Lo que hago con eso: cuando una respuesta llega desalineada con mi intención, miro qué archivos entraron al prompt en esa vuelta. A veces sobra algo (un CLAUDE.md viejo que ya no aplica). A veces falta algo (una convención que está en otro directorio que Claude no visitó). El log convierte “no entiendo por qué respondió así” en “abrió estos archivos y no abrió estos dos”.

Decisión 2: cuándo se abre un subagente

El segundo evento útil es SubagentStart. Se dispara cuando Claude decide delegar parte del trabajo a otro agente. En el chat aparece como una tarjeta plegable, pero en el log llega como una línea con timestamp.

Yo pensaba que los subagentes se abrían solo cuando yo los pedía a mano. El log mostró que también se abren en respuestas largas cuando el runtime estima que dividir el trabajo va a ser más rápido. Es una decisión razonable — no la voy a bloquear — pero saber cuándo pasa me ayuda a leer los tiempos de respuesta. Una respuesta lenta puede ser un modelo tardando, o puede ser varios subagentes coordinándose. El log lo distingue en dos segundos.

No inventé un porcentaje aquí. No te voy a decir “en tantas sesiones se abre un subagente”, porque ese cálculo depende de tu tipo de trabajo, no del mío. Lo interesante es tener el dato en tu propio log, no el mío.

Decisión 3: qué se escribe (y dónde)

La tercera categoría es la más incómoda para mí: Write y Edit. Y no tanto por los archivos de código (esos los reviso yo en el diff), sino por escrituras “de mantenimiento” dentro de .claude/.

Claude Code guarda memoria, plans, historial de sesiones y otros artefactos en .claude/ y directorios cercanos. Esas escrituras pasan por el mismo Write o Edit que las escrituras en tu código de aplicación. Sin log, no las ves. Con log, aparecen mezcladas con el resto y te ubicas rápido cuál es cuál por la ruta.

Este dato me sirvió para dos cosas. Una: entender que .claude/ puede crecer y conviene revisar de vez en cuando qué hay ahí. Dos: cuando algo cambia y no tengo claro quién lo tocó, el log tiene timestamp y herramienta, y en más de un caso me evitó culpar a un colega inocente.

Cómo leer el log sin sacar conclusiones apresuradas

El error fácil es abrir audit.log después de una hora, contar líneas y publicar el resultado como si fuera una medición. No lo es. Es un registro de tus propias sesiones, con tu propio contexto, en tu propio repo. Extrapolar a “así se comporta Claude Code en general” es exactamente el tipo de conclusión que después no se sostiene.

Lo que sí sirve:

  • Comparar sesiones tuyas contra sesiones tuyas. Cuando cambias CLAUDE.md o mueves un archivo grande, mira cómo cambia el patrón de Read en las siguientes respuestas.
  • Detectar sorpresas puntuales. Si un Write toca un path que no esperabas, ese es un ítem concreto para investigar, no una estadística.
  • Dejar el log corriendo un rato antes de agregar reglas. Antes de escribir “no toques X”, conviene saber si Claude toca X. Puede que no lo haga nunca.

Cierre

PreToolUse no es una herramienta de seguridad — para eso está exit 2, que ya cubrí en el artículo sobre hooks con exit code 2. Este es el uso más humilde: un hook que mira y guarda.

Antes de instalarlo, “sin pedir permiso” era una frase que yo repetía sin datos. Después, “sin pedir permiso” se volvió unas líneas en audit.log que puedo revisar con tail -f. No hay revelación oculta en el log. Hay simplemente registro. Y para casi todo lo que pasa en una sesión de Claude Code, tener el registro es suficiente. Después toca acordarse de leerlo, que es la parte que yo sigo olvidando.

Si estás organizando tu contexto de Claude Code y no sabes por dónde empezar, tengo una checklist de 7 archivos para revisar en 5 minutos que va bien con este hook: primero decides qué debería leer Claude, después lees el log y verificas que efectivamente lee eso.

Si buscas el marco más amplio de cómo pensar en tu harness personal, lo escribí en Harness Engineering Guide.


ken imoto · WebRTC & Voice AI engineer · kenimoto.dev

Harness Engineering Libro relacionado Harness Engineering Harness Engineering desde cero — cinco interpretaciones (OpenAI, Anthropic, LangChain, Martin Fowler, academia) unificadas en un solo sistema para ingenieros en producción Ver la página del libro →