Decidí dejar de creerme el "listo" de mi agente - prove-it, un gate con verify.sh
Los agentes de codificación con IA reportan "tests en verde" sin haberlos ejecutado. No es una mentira, es un problema estructural. Lo que aprendí construyendo prove-it, un gate open source que convierte el reporte en una comprobación - y las cuatro formas en que un hook casero de cinco líneas falla en silencio.
Lo esencial
Un agente de codificación no distingue entre lo que hizo y lo que pretendía hacer, así que en la práctica reporta su intención. Por eso dice "los tests pasan" sin haberlos ejecutado. No se arregla con prompts: hay que convertir el "listo" en una comprobación que debe pasar, no en una declaración. prove-it es un gate basado en el hook Stop que impide al agente terminar su turno hasta que el verify.sh de la raíz del repo devuelve exit 0. Un hook casero de cinco líneas falla en silencio de cuatro maneras - bloquea una vez y nunca más, deja pasar los turnos que terminan en commit, permite que el agente borre la propia comprobación, o hace que rendirse parezca lo mismo que pasar. Esas cuatro cosas son toda la razón por la que prove-it tiene más de cinco líneas.
En esta página
"Todos los tests pasan. Se puede mergear."
Eso dijo el agente, y el diff estaba tan limpio que estuve a punto de creérmelo. Pero al subir el scroll del terminal, los tests no se habían ejecutado ni una sola vez. No había ningún log de ejecución. El agente pretendía ejecutar los tests, y esa intención acabó escrita tal cual en su reporte de completado.
Después de que te pase esto unas cuantas veces, la reacción suele ser una de dos: añadir al prompt "ejecuta los tests sin falta y muéstrame el resultado", o volver a comprobarlo todo a mano cada vez que el agente dice que terminó. Probé las dos y ninguna aguantó mucho. Así que tomé un tercer camino. En vez de resignarme a no poder creer la palabra "listo", convertí el completado en una comprobación que hay que pasar, no en una declaración. Así nació prove-it, una herramienta open source.
No es una mentira, es estructura
Primero, algo que hay que dejar claro. El agente no está mintiendo.
Una persona distingue entre "ejecuté los tests" y "pretendía ejecutar los tests". Tiene memoria. El agente no tiene ese punto de comparación. Como no puede contrastar lo que hizo con lo que pretendía hacer, reporta la intención en lugar del resultado. "Los tests pasan" viene a ser la abreviatura de "escribí el código para que los tests pasen". No es un defecto de carácter del modelo sino una propiedad de diseño, y por eso no se arregla con prompts. Puedes escribir "ejecútalos sin falta" cien veces y llegará el día en que no los ejecute.
Las técnicas de prompt tienen otra debilidad: envejecen con cada generación de modelos. Que una instrucción funcione bien con el modelo actual no garantiza que funcione con el siguiente. En cambio, un mecanismo que exige evidencia se sienta una capa por encima del modelo y sobrevive a las actualizaciones. Que los tests se ejecutaron de verdad no lo tiene que decir el modelo: lo dice el código de salida.
Convertir el reporte en una comprobación
La idea en sí - aceptar el "listo" solo como evidencia, nunca como declaración - ya la conté en un artículo anterior. Este artículo es la historia de convertir esa idea en una herramienta que cualquiera puede instalar.
La convención de prove-it cabe en una frase. El repo declara cómo probarse a sí mismo en un archivo llamado verify.sh, y el agente no puede reclamar el completado hasta que esa prueba pase.
La implementación es esta. Pones un verify.sh ejecutable en la raíz del repo. Si devuelve exit 0, significa "este árbol está demostrablemente bien". Cuando el agente intenta terminar su turno, el hook Stop ejecuta el script y, si no devuelve 0, en vez de dejar que el agente se detenga lo manda de vuelta a trabajar junto con las últimas veinte líneas de la salida del fallo. En la mayoría de los casos, al agente le basta esa salida para corregir la causa.
Cuándo se dispara el gate también es simple. Cuando esta sesión modificó de verdad este repo, existe un verify.sh ejecutable, y el estado actual del árbol todavía no ha pasado nunca. Solo corre cuando las tres condiciones son ciertas. Las sesiones de solo lectura no se tocan, y un árbol que ya pasó una vez no se vuelve a comprobar.
Y no bloquea indefinidamente. Devuelve al agente como máximo tres veces por turno y después cede, porque un hook que nunca cede congela la sesión. Pero ceder no es lo mismo que pasar. La última salida de un turno que terminó en cesión no es "listo" sino una advertencia: "este turno terminó sin verificar". Puede que el agente nunca logre cruzar el gate, pero no puede cruzarlo en silencio - eso es todo lo que esta herramienta afirma en realidad.
Por qué no bastan cinco líneas de bash
Si llegaste hasta aquí pensando "eso se resuelve con una línea de tests en el hook Stop", tienes razón. Con cinco líneas alcanza, y yo también lo escribí así al principio. Y esa versión falló en silencio de cuatro maneras. Varias fueron bugs reales de las primeras versiones de prove-it, así que hoy cada una tiene su test de regresión.
Bloquea una vez y nunca más. Claude Code activa el flag stop_hook_active en todos los eventos stop posteriores al primer bloqueo de un hook. Un hook que lee ese flag como "déjalo pasar" deja de ser un gate después de bloquear exactamente una vez. Al revés, un hook que ignora el flag bloquea para siempre y congela la sesión. Es una trampa que solo conoces si has sufrido las dos, y la respuesta es contar los intentos tú mismo, bloquear un número fijo de veces y luego ceder haciendo ruido.
Después de un commit parece que no pasó nada. Un hook que decide "¿hubo trabajo?" según si el working tree está sucio deja pasar sin más todos los turnos que terminan en commit. Y commitear es la cosa más normal que hace un agente. Por eso prove-it registra como línea base el estado del árbol al inicio de la sesión y lo compara en cada stop. Un commit, un archivo retocado con sed, un archivo escupido por un generador de código: todo cuenta como cambio.
El agente puede eliminar la comprobación. La jugada más barata para un agente que no consigue pasar verify.sh es borrarlo o quitarle el permiso de ejecución. El gate registra al inicio de la sesión si el repo estaba armado y rechaza los turnos que terminan con el gate desarmado. Hay una jugada más sutil: dejarlo ejecutable y reescribir solo las comprobaciones de dentro. Eso no se bloquea, porque muchas veces modificar verify.sh es precisamente la tarea que pediste. Pero tampoco pasa en silencio: si verify.sh cambió durante la sesión y aun así pasa, el gate te lo avisa, y leyendo ese diff puedes juzgar si fue trabajo o evasión.
Rendirse no se distingue de pasar. Cualquier host acaba obligando al hook a ceder. Un hook casero cede en silencio, y lo último que ves en pantalla es "listo". Un turno que falló tres veces y se rindió se ve idéntico a uno que pasó a la primera. La última palabra de prove-it es una advertencia. Si la diferencia te parece menor, imagina el día siguiente a mergear un turno rendido creyendo que había pasado.
Esas cuatro cosas son toda la razón por la que prove-it es más largo que cinco líneas. Si quieres seguir usando tu propio hook, adelante. Pero los casos que ese hook tiene que cubrir están escritos en SPEC.md, y te recomiendo leerlos. Lo importante no es la implementación sino la convención.
Lo que esta herramienta no te prueba
Hay una limitación que conviene dejar por escrito, con honestidad. Lo único que el gate garantiza es que verify.sh devolvió 0 antes de que terminara el turno. Qué significa ese 0 depende por completo de las comprobaciones que tú escribiste. Un verify.sh con una sola línea de exit 0 pasa este gate y no prueba nada.
La spec llama a eso Level 1. Level 2 es la cuestión de si las comprobaciones afirman algo sobre evidencia real, y eso ninguna herramienta puede verificarlo por ti. prove-it tampoco. Enganchar el hook es la parte fácil. El trabajo de verdad es responder a la pregunta "¿qué significa 'probado' en este repo?", y esa respuesta es distinta en cada repo. Por eso la convención fija solo el nombre del archivo y no su contenido.
Las vías de escape también están abiertas a propósito. Con PROVE_IT_SKIP=1 ese turno pasa sin más, y si borras verify.sh el gate se apaga para siempre. Un gate que no se puede quitar acaba siendo rodeado, y un gate rodeado es peor que ninguno: reporta que las comprobaciones corrieron cuando en realidad no corre nada.
El primer día, pequeño a propósito
La instalación son tres líneas, y el trabajo de verdad lo hace la tercera.
/plugin marketplace add Why-Next/prove-it
/plugin install prove-it@whynext
/prove-it:init
/prove-it:init detecta el stack, crea el verify.sh, te muestra delante de tus ojos que pasa, y después ejecuta una copia con un exit 1 añadido para enseñarte cómo el gate rechaza el turno. Tarda unos 30 segundos.
El verify.sh generado tiene una sola comprobación activa: git diff --check. Los tests y el type check están todos solo como comentarios. Está hecho así a propósito, para que pase en main el mismo día que lo instalas. Un gate que falla el primer día le enseña al equipo a eludirlo desde la primera semana. Las comprobaciones comentadas se activan una a una, después de ejecutarlas a mano y verlas pasar. Y antes de activar cada una, hay que hacerla fallar una vez a propósito. Una comprobación que no puede fallar no es una comprobación, y descubrirlo el día que la necesitas es demasiado tarde.
También está definido dónde parar de hacer crecer verify.sh: un minuto en total. Lo que sea más lento se manda a CI. Las comprobaciones que necesitan secrets o acceso a producción también son cosa de CI. En verify.sh solo queda la evidencia que el agente puede producir en local mientras trabaja.
El libro de cuentas de lo que atrapó el gate
Si activas PROVE_IT_LEDGER=1, cada vez que el gate atrapa un falso completado se añade una línea a un archivo local.
{"ts":"2026-07-09T04:12:33Z","claim":"All tests pass. Ready to merge.",
"evidence_demanded":"verify.sh exit 0","actual":["3 failed, 41 passed"]}
Qué afirmó el agente, qué se le exigió y qué había en realidad. Con un mes acumulado, puedes leer en un registro - en vez de adivinar - de qué manera falla tu agente. El archivo se queda solo en tu disco local, no se envía a ninguna parte, y está apagado hasta que lo actives.
Este repo se gatea a sí mismo
El repo de prove-it, por supuesto, también tiene su verify.sh, y dentro de él el gate se testea a sí mismo sobre repos git reales. Que una comprobación que falla bloquea, que una que pasa abre, que una sesión de solo lectura no se toca, que un árbol limpio después de un commit no se confunde con "sin trabajo". Las cuatro trampas de arriba están todas clavadas como tests de regresión.
Es licencia MIT y las únicas dependencias son bash, git y python3. Está testeado con Claude Code, y la forma de conectarlo a hosts que exponen el mismo tipo de hooks bloqueantes, como Codex CLI o Gemini CLI, está en el documento ADAPTERS. Si tienes un repo donde el agente arregla código y dice "listo" en la misma conversación, ese es exactamente el lugar donde este gate más sirve.
"Listo" no debería ser una palabra que declara el agente sino un estado que dictamina el repo. Escribir el criterio de ese dictamen sigue siendo cosa tuya, pero al menos ya nadie puede saltárselo y pasar en silencio.
Preguntas frecuentes
Mi agente de codificación con IA reporta como completado trabajo que no hizo. ¿Cómo lo evito?
Ordenarle por prompt "ejecuta los tests sin falta" no basta. El agente no tiene forma de comparar lo que hizo con lo que pretendía hacer, así que reporta la intención tal cual. En su lugar, convierte la propia declaración de completado en una comprobación: pon un script de verificación (verify.sh) en la raíz del repo y haz que un hook Stop lo ejecute cuando el agente intente terminar su turno; si no devuelve exit 0, lo manda de vuelta a trabajar. prove-it es la implementación de referencia de esta convención.
¿No puedo escribir yo mismo un script de cinco líneas que ejecute los tests desde el hook Stop de Claude Code?
Puedes, pero hay cuatro trampas. ① Claude Code activa el flag stop_hook_active después del primer bloqueo; si lo manejas mal, tu hook bloquea una sola vez o bloquea para siempre y congela la sesión. ② Si decides según si el working tree está sucio, dejas pasar todos los turnos que terminan en commit. ③ El agente puede desarmar el gate borrando verify.sh o quitándole el permiso de ejecución con chmod -x. ④ Cuando el hook al final cede, rendirse se ve igual que pasar. Si lo escribes tú, tienes que cubrir los cuatro casos.
¿Qué comprobaciones debería poner en verify.sh?
La respuesta a "¿qué tiene que ser cierto para que un cambio quede probado en este repo?". Normalmente se van activando una a una: el comando de tests que realmente ejecutas, el type check y el lint, en ese orden. El total debe terminar en menos de un minuto, y las comprobaciones que necesitan secrets o acceso a producción van en CI. Lo importante es no ser ambicioso el primer día: un gate lento o inestable se elude en una semana, y un gate eludido es peor que no tener ninguno.
Artículos relacionados
- 💻 Desarrollo
Cuando la IA dice "ya está", el dispositivo que no se lo cree
Los agentes de codificación con IA dicen que han terminado sin haber terminado. Aseguran que los tests pasan sin haberlos ejecutado, y que han arreglado el bug sin haberlo reproducido. La historia de un desarrollador en solitario, que escribe casi todo su código con IA, sobre la puerta de verificación que la obligó a aceptar el "terminado" solo con pruebas, no con palabras.
- 💻 Desarrollo
El descuento que no estabas cobrando: por qué Spot y Graviton son tan baratos
La factura de AWS superó el presupuesto dos meses seguidos. Buscando qué recurso recortar, descubrí que la palanca real no era 'cuánto usas' sino 'cómo lo compras'. Por qué Spot y Graviton son baratos, y por qué hay costos que parecen desperdicio (RDS Proxy) pero no se deben apagar - una historia sobre el 'porqué' detrás de cada cifra de la factura.
- 💻 Desarrollo
Los hooks de cierre se ejecutaban dos veces: cuando SIGTERM tenía dos dueños
En cada despliegue, Sentry recibía un error P2028 de Prisma. Al entrar a corregir el orden de cierre, descubrí que enableShutdownHooks de NestJS y una librería de graceful shutdown capturaban ambas SIGTERM, haciendo que los hooks de cierre se ejecutaran dos veces en cada ocasión.