# Hooks

Los hooks convierten invariantes comprobables en gates mecánicos. La base recomienda proteger:

- comandos destructivos, force, amend y stage indiscriminado;
- escritura de secretos o credenciales;
- lectura de credenciales conocidas o declaradas, en los runners que tienen una herramienta de lectura;
- edición manual de código generado y drift respecto a OpenAPI/SQL;
- commits sin Verify aplicable;
- apagado o borrado de la prueba que juzga el cambio;
- cambio de producto sin un WIP activo que traiga el plan;
- cierre de sesión con planning o integraciones inválidas;
- modificación del protocolo durante una tarea de producto.
- escrituras fuera de las raíces declaradas del workspace;
- reescritura de migraciones y SQL destructivo;
- publicación, instalaciones globales y drift entre manifests y lockfiles.

La lógica portable vive en `engine/hooks/run.js`; los `guard-*.sh` son entradas ejecutables comunes. Cada
adaptador de `runners/` conecta los eventos de su herramienta con esos mismos guards.

## Qué son y qué no son

**Son andamiaje de disciplina: coincidencia de texto sobre el comando o la ruta que el agente propone.**
Convierten un descuido en un alto y hacen visible una intención en el momento de tenerla. Para eso
sirven, y sirven bien.

**No son un límite de seguridad, y conviene saber exactamente por qué.** Comprobado corriendo los
guards directamente:

```text
git push origin main       → bloquea      g=push; git $g origin main   → pasa
.env .npmrc .netrc id_rsa  → bloquean
```

La primera línea es la que importa y no se arregla con más expresiones regulares: cualquier guard que
lea el texto de un comando se esquiva componiéndolo, y una shell tiene infinitas formas de hacerlo.
Los nombres de credencial conocidos sí se taparon —antes `.npmrc`, `.netrc` e `id_rsa` pasaban—, y eso
mismo muestra el límite: se tapan los nombres que alguien enumeró, no la clase.

El riesgo real de esta página no es el bypass: es **la confianza que un guard inspira**. Un repositorio
con los guards puestos parece más protegido de lo que está, y esa lectura es peor que no tenerlos,
porque reemplaza controles que sí son límites —permisos, tokens acotados, revisión humana de lo que se
publica— por la sensación de que ya está cubierto.

Regla práctica: si algo **tiene** que ser imposible, no lo pongas acá. Ponelo donde no dependa de leer
una cadena — permisos del runner, alcance del token, aprobación de un PR.

### Leer una credencial

Dos guards frenan leer un archivo que `secrets` frenaría al escribir —los nombres conocidos y las
identidades que declara `organization/secrets.json`—: `secrets-read` en las herramientas de lectura y de
búsqueda del runner (`Read` y `Grep` en Claude, `read_file` y `grep_search` en Gemini) y `secrets-shell` en
el shell de los cuatro runners, cuando un comando la muestra —`cat`, `head`, `grep`, `sed`, `source`, una
redirección `<`, un intérprete en línea—. Un comodín que la nombra —`rg -g '.env*'`, `--include='*.env'`—
cuenta igual. Lo que sólo la nombra —`ls`, `test -f`, `rm`, `cp .env.example .env`— pasa, y lo que la
persona pidió en el chat también.

Lo que no ve ninguno, comprobado en sesiones reales: una búsqueda sobre la carpeta que no nombra el archivo
—un `rg` de todo el árbol—, un nombre armado en una variable, y en Gemini el propio entorno del runner,
que carga el `.env` de la carpeta al arrancar (documentado en geminicli.com/docs/reference/configuration):
un `env` muestra sus valores sin leer ningún archivo.

Hasta 0.80.0 Claude traía además reglas nativas `permissions.deny` `Read(...)`. Las aplica Claude mismo, sin
pasar por ningún hook, así que frenaban también lo que la persona pedía, mientras en los otros runners el
shell no tenía freno (caso 104). `automation install` las retira de una instalación anterior y conserva las
que escribió la empresa: quien quiera un bloqueo nativo total lo escribe como regla propia.

### Lo que pide la persona

Un guard ve la llamada a la herramienta y nada de la conversación, así que frenaba igual lo que la persona
pidió con todas las letras y lo que el agente decidió solo. `chat` corre sobre el mensaje de la persona —el
runner lo dispara cuando ella manda algo, nunca por el resultado de una herramienta— y lo deja donde los
guards lo leen:

- lo que la persona **pidió nombrándolo** pasa: «leé el `.env`» autoriza leer el `.env`; «no toques el
  `.env`» no, y tampoco una pregunta o un comentario que sólo lo nombra —«¿qué tiene el `.env`?»—;
- lo que se frenó sin que lo nombrara queda anotado, y su confirmación en el mensaje siguiente —con las
  palabras que sea— aprueba exactamente eso. Si ese mensaje niega, frena o pregunta, no aprueba nada; y lo
  que se frenó mientras ella decía que no, no queda esperando;
- lo que un guard dejó pasar porque ella lo pidió sigue valiendo mientras dure la sesión, **salvo en los
  gates de un commit** —`governance`, `verify` y `dependencies`—, que preguntan cada vez: ahí vale lo que
  pidió el mensaje en curso o su confirmación al bloqueo, igual que para publicar;
- `plan-first` no aplica: el plan es del trabajo que va por tareas.

No cuenta cuando no hay persona —CI, o un aviso del runner como el de un subagente que terminó—, cuando lo
que pidió es un recorrido de Cauce (`/autobuild`, `$flow`…), ni en la llamada de un subagente, que Claude
marca con `agent_id`. En Claude y Codex cada llamada trae el identificador del mensaje que la originó;
Gemini no lo manda, y ahí vale el último mensaje. El registro vive en el temporal del sistema, uno por
sesión, y los guards de límites lo cuidan junto con `planning/.ops-approval`: el registro no lo escribe
nunca una herramienta, y en la aprobación sólo entran las líneas que la persona nombró en ese mismo
mensaje —por shell, ninguna: un comando no dice con qué va a quedar el archivo—. Como todo lo de esta página, frena la forma habitual y no un script decidido.

## Cómo se ejecutan

```text
Claude / Codex / Antigravity / Gemini
        ↓ evento del runner
automatization/hooks/guard-shell.sh · guard-files.sh   (grupo)
o automatization/hooks/guard-<nombre>.sh               (guard suelto)
        ↓ nombre del grupo o del guard
automatization/hooks/run-hook.sh
        ↓ localiza el runtime
node_modules/@ingeniomaps/cauce/engine/hooks/run.js (instancia)
o engine/hooks/run.js (repositorio del toolkit)
```

Los `guard-*.sh` son wrappers pequeños a propósito: ofrecen una entrada ejecutable estable para cada
runner, mientras la lógica se prueba y mantiene una sola vez en `engine/hooks/run.js`.

## Grupos por evento

`hookGroups` en `engine/hooks/run.js` es la única definición de qué guards corren en cada evento:

| Grupo | Guards | Wrapper |
|---|---|---|
| `pre-shell` | destructive, git-add, dependencies, governance, verify, shell-boundary, secrets-shell, ops-config-shell | `guard-shell.sh` |
| `pre-files` | secrets, generated, workspace-boundary, engine, migrations, integration-snapshot, test-evidence, plan-first, ops-config | `guard-files.sh` |
| `pre-read` | secrets-read | `guard-secrets-read.sh` |
| `prompt` | chat | `guard-chat.sh` |
| `stop` | planning-drift | `guard-planning-drift.sh` |

`prompt` corre sobre el mensaje de la persona —`UserPromptSubmit` en Claude y Codex, `BeforeAgent` en
Gemini— y no sobre una herramienta, y su shim sale siempre con 0.

Registrar el grupo gasta un proceso por herramienta en lugar de cinco, con el mismo orden y la misma
semántica: el primer guard que bloquea corta la ejecución. Un runner que necesite granularidad fina puede
seguir registrando los wrappers individuales.

Para inspeccionar qué hace cada hook y cuándo se ejecuta:

```bash
node tools/ops.js automation list-hooks .
```

## El motor no se edita desde la empresa

`engine` es el único guard cuyo comportamiento depende de dónde corre, y la distinción es deliberada:
en una instancia bloquea toda escritura bajo `node_modules/@ingeniomaps/cauce`; en el repositorio del
toolkit —`mode: toolkit` en `ops.config.json`— queda inerte, porque ahí el motor es el producto.

`workspace-boundary` no alcanzaba: en una instalación `node_modules/` cae dentro de la raíz declarada,
así que editar el motor le parecía legítimo. Y el daño de esa edición es silencioso por partida doble.
El próximo `npm install` la borra, de modo que el arreglo se pierde justo cuando alguien creyó haberlo
hecho; y hasta entonces la empresa corre un motor que no coincide con la versión que declara, que es la
forma habitual de un bug irreproducible.

La regla que codifica: **un problema del motor se reporta y se arregla arriba.** Lo que sí es de cada
empresa —sus cargos, sus equipos, sus integraciones, su planificación— queda abierto y el guard no lo
toca.
