Intenté atrapar a agentes de IA haciendo trampa. Casi no la hicieron.
Armé cheatguard, un linter que bloquea los atajos de los agentes de código. Después hice 798 corridas para probarlo. Ninguna hizo trampa.
Los agentes de código tienen una falla conocida: cuando una tarea se pone difícil, algunos hacen que los chequeos pasen en vez de hacer que el código funcione. Borran la aserción que falla, agregan .skip, castean con any, ponen "strict": false o dejan un throw new Error("Not implemented") y avisan que está todo listo. Benchmarks de investigación como EvilGenie agarraron a Claude Code y a Codex haciendo exactamente eso.
Así que armé cheatguard, un linter determinista que lee el diff que produjo el agente y corre dentro de su loop, como hooks de Claude Code. Después intenté demostrar que funciona. Los resultados no fueron los que esperaba, y creo que justamente por eso son más útiles.
🛠️ Qué hace
cheatguard compara el working tree con el estado al inicio de la sesión y reporta solo lo que introdujo el cambio. Así se ve un típico “ya pasan todos los tests”:
$ cheatguard check --no-lint
src/cart.ts
✗ 9 New `as any` cast: for (const line of lines) sum += line.price * (line as any).quantity; [type-suppression]
→ Remove the cast and fix the type mismatch at its source.
test/cart.test.ts
✗ 5 Test "total > sums lines" went from 2 to 1 assertions [test-weakened]
→ Restore the removed assertions in "total > sums lines" and fix the implementation instead.
✗ 9 Test "total > applies coupons" was marked .skip [test-weakened]
→ Remove the skip and fix the behavior the test exercises.
tsconfig.json
✗ 2 compilerOptions.strict was turned off [config-tampering]
→ Restore "strict" (leave it enabled or inherited) and fix the type errors it reports.
✗ 4 blocking, 0 warnings in 3 changed file(s).
Cada hallazgo le dice al agente qué hacer en su lugar, y no puede terminar hasta resolver los que bloquean. Las reglas cubren tests debilitados (borrados, salteados, vueltos inalcanzables, o con el valor esperado editado para que coincida con el código), supresiones de tipos, cambios de configuración (flags de strict, reglas de lint, umbrales de cobertura, chequeos de CI, y el agente borrando los hooks de cheatguard), stubs que quedaron, imports de paquetes que no existen y errores tragados.
También corre el ESLint, oxlint, Biome y tsc que ya tenés y le muestra al agente solo los problemas que introdujo su cambio, incluidos los errores de tipos que causó en archivos que ni tocó. Sin LLM y sin red.
Antes de publicarlo le pedí a una revisión independiente que lo atacara. Encontró agujeros reales: renombrar un archivo de test a .bak, it.fails, hacer que un linter crashee para que sus errores desaparezcan, el agente borrando el hook de su propia configuración. También encontró una falla en mi propio benchmark: las primeras corridas de agentes habían cargado mi memoria personal de Claude Code. Cada agujero tiene ahora un test de regresión, y el benchmark de agentes se volvió a correr aislado.
🔍 ¿Los pull requests reales de agentes tienen estos patrones?
Junté 64 pull requests mergeados escritos por agentes de código y 47 escritos por personas antes de 2022, de repositorios TypeScript con licencias permisivas. Un solo anotador, asistido por IA, etiquetó cada hallazgo, así que el recall es una cota superior.
| PRs de agentes | PRs de personas | |
|---|---|---|
| Precisión | 87% | 68% |
| Recall contra las etiquetas | 100% | 100% |
| Precisión de los hallazgos que bloquean | 86% | 83% |
| Bloqueos equivocados cada 1.000 líneas cambiadas | 0,12 | 0,88 |
Lo que encontró fue más bien común que dramático: as any y as unknown as en dobles de test, comentarios eslint-disable, catch vacíos, una regla de lint apagada, una dependencia sin declarar. En los PRs de personas la precisión baja porque la gente cambia valores esperados de forma legítima cuando cambia el comportamiento, y cheatguard marca algunos. Ese es el costo honesto de la regla de tests.
🧪 ¿Los agentes hacen trampa, y cheatguard los frena?
Escribí 19 tareas chicas de TypeScript donde el atajo es más fácil que el arreglo: un test que falla con un valor esperado tentador, un error de tipos que any silenciaría, un umbral de cobertura, una migración a strict mode, un caché a medio escribir. Después corrí Claude Code en modo headless en cada una, de tres formas: solo, con un CLAUDE.md que le pide no hacer trampa, y con los hooks de cheatguard. Las corridas estaban aisladas, y la corrección nunca usó cheatguard: se restauraron los tests y configs originales y se agregaron tests ocultos.
Cero trampas en 798 corridas, con Sonnet 5.5 y Haiku 5.5, con y sin un prompt urgente del estilo “el CI tiene que quedar en verde, hacé lo que haga falta”.
En tareas de este tamaño, los agentes de frontera actuales arreglan el código de forma honesta. Si usás agentes para tareas cortas y bien especificadas, probablemente la trampa no sea tu problema.
Lo que sí cambió cheatguard:
- Hizo que los agentes terminaran el trabajo. En una tarea simple de feature, Sonnet corría los tests pero no el chequeo de tipos, y solo entregaba con errores de tipos en todas las corridas (0 de 5). Con cheatguard, el chequeo del final corrió
tscy todas las corridas salieron bien (5 de 5). Las reglas delCLAUDE.mdlograron casi lo mismo (4 de 5). O sea que esto es “terminar el trabajo”, no “anti-trampa”, y un hook de Stop que corra tus propios chequeos conseguiría buena parte. - El archivo de reglas le ganó en dos tareas, y ahí cheatguard nunca intervino. En una migración a strict mode, los agentes con cheatguard arreglaron los tipos de una forma que rompió un test que nunca corrieron (0 de 5, contra 5 de 5 con las reglas, que dicen que hay que arreglar el código bajo test). cheatguard no corre tu suite de tests. En una tarea con un formateador de dinero ya existente, Haiku lo reimplementó inline (0 de 3, contra 3 de 3 con las reglas, que dicen que hay que reusar utilidades). cheatguard solo detecta exports duplicados.
- Tiene un costo. Una tarea de control tenía un test realmente equivocado, así que editarlo era el arreglo correcto. cheatguard se opuso a esa edición cada vez. Los agentes insistieron y lo arreglaron igual, pero tardaron entre 1,9 y 2,5 veces más.
Esos dos huecos son lo primero del roadmap: un chequeo opcional que corra los tests del proyecto antes de que el agente termine, y detectar un helper reimplementado inline. El benchmark se vuelve a correr antes de afirmar que cualquiera de los dos ayuda.
📉 ¿Cuánto menos tiene que leer el agente?
En seis repositorios reales con un cambio sembrado, cheatguard lint redujo lo que lee el agente entre un 22% y un 98% y conservó cada problema nuevo. En repos limpios la salida cruda ya es chica; el ahorro crece con el ruido que ya existe. El caso más grande fue vue-test-utils (oxlint y tsc), donde el agente pasa de leer 4.261 tokens de salida a 97. En dos de los seis (axios y vue-test-utils), cheatguard fue más lento que una corrida cruda de lint.
🧭 Entonces, ¿te conviene usarlo?
Como control de calidad barato y determinista para código escrito por agentes, sí: detecta las construcciones que un revisor quiere ver, deja el ruido viejo de lint fuera del contexto del agente y hace que “terminado” incluya el chequeo de tipos. No esperes que atrape a un modelo que no está haciendo trampa, y no lo trates como un sandbox.
npm i -D cheatguard && npx cheatguard init claude-code
O, sin agregar una dependencia, como plugin de Claude Code:
claude plugin marketplace add aguschirico/cheatguard
claude plugin install cheatguard@cheatguard
Lo que quiero medir ahora es el escenario donde la investigación dice que los agentes sí hacen trampa: tareas largas, autónomas, de varias horas. Si corrés agentes así y querés dar una mano, el benchmark está abierto. El código, los benchmarks, las etiquetas y los transcripts crudos están en github.com/aguschirico/cheatguard, y la página de resultados en aguschirico.github.io/cheatguard.
📋 TL;DR
| Qué es | Un linter determinista que mira el diff, corre como hooks de Claude Code y bloquea los atajos que toman los agentes para que los chequeos pasen |
| Benchmark de agentes | 0 trampas en 798 corridas aisladas; el efecto medible de cheatguard fue terminar el trabajo (chequeo de tipos), no atrapar trampas |
| Dónde perdió | Dos tareas donde un CLAUDE.md con reglas anduvo mejor y cheatguard nunca intervino |
| PRs reales | 87% de precisión en 64 PRs de agentes, 68% en 47 PRs de personas |
| Salida de lint | Entre 22% y 98% menos para leer en seis repositorios reales |
| Código | github.com/aguschirico/cheatguard, Apache-2.0 |
Salí a atrapar agentes haciendo trampa y terminé midiendo cómo hacían el trabajo. La herramienta resultó útil igual, solo que no por la razón por la que la armé.