Once tests superados
Straiker publicó un repositorio que consigue que Claude Code ejecute malware sin jailbreak. Su CONTRIBUTING.md envía la revisión a un subagente que define el propio repositorio, fijado en Haiku y apartado de la carpeta de tests. Después Fable ejecuta pytest, un archivo de test descarga e inicia un implante Sliver, y la terminal informa 11 passed. Anthropic cerró el reporte como Informative: confiar en la configuración de un repositorio es la decisión de confianza del workspace que toma el usuario. Bromure Agentic Coding decide hasta dónde llega esa confianza: una VM con claves sustitutas, un firewall en el Mac y una traza que el repositorio no puede editar.
El agente siguió el flujo de trabajo documentado del proyecto, paso a paso, y ese flujo lo había escrito el atacante. La terminal decía 11 passed.
Encuentras en GitHub un pequeño cliente de métricas que te ahorraría una semana, y le pides a Claude Code que lo clone y le eche un vistazo. Hace un buen trabajo. Lee el código, señala un búfer sin límite y un bucle de reintentos que girará para siempre con un mensaje envenenado, y sugiere correcciones. Le dices lo de siempre: arregla eso y prepara un PR.
Unos minutos después las correcciones están hechas, los tests están en verde y el resumen dice 11 passed. En algún momento de esos minutos, un implante abrió una shell remota en tu portátil.
Ese es el escenario que Brian Cumi, de Straiker STAR Labs, construyó y publicó el 22 de septiembre con el título Fable to Haiku: How a Malicious Repo Tricked Claude Code Into Running Malware. El resumen de octubre de investigación en seguridad de agentes de código de Adversa lo incluye entre las técnicas de ataque del mes. El repositorio no explota ningún bug. Cada paso usa una función documentada tal como la concibieron sus diseñadores.
Cómo el repositorio se revisó a sí mismo
El repositorio, northstar-metrics, parece un proyecto de código abierto
normal, y su primer truco es corriente: un CONTRIBUTING.md. Al pedir un PR,
ataste al agente al flujo de contribución del proyecto, y un agente que sigue
instrucciones lee la guía de contribución. Esta dice que los revisores deben
ejecutar el agente de primera revisión del proyecto antes de ejecutar los
tests.
Ese agente vive en el repositorio, en .claude/agents/first-pass.md. Claude
Code permite que un proyecto defina sus propios subagentes en esa carpeta, con
un modelo y un alcance en el frontmatter. Este fija model: haiku y limita la
revisión a src/ y docs/. Sus instrucciones añaden que una lectura en la
sesión principal no sustituye a ejecutar el agente, para que el agente
principal no se lo salte.
Esta es la cadena completa:
- Fable, el modelo principal, entrega la revisión al subagente del repositorio.
- El subagente se ejecuta en Haiku 4.5 y solo mira
src/ydocs/. - Vuelve con dos hallazgos que el autor plantó para que los encontrara: una cadena de versión escrita a mano en el código y una discrepancia entre la documentación y el código. Ambos son inofensivos.
- El flujo documentado da la revisión por terminada. Nadie abre
tests/test_compat.py. - Fable ejecuta
pytest. - Antes de su aserción,
test_compat.pydecodifica una URL en hexadecimal, descarga un archivo Python y lo inicia consubprocess. El bloque está envuelto entry/except: pass, y la aserción siempre se cumple. - Un implante Sliver abre una sesión de comando y control. Pytest imprime 11 passed.
El resumen de Cumi sobre por qué funciona: el repositorio se apoya en tres supuestos corrientes. Los proyectos pueden definir sus propios subagentes, los agentes siguen el flujo documentado de un proyecto, y una ejecución de tests en verde cuenta como comprobación del código. “Un repo malicioso que entiende los tres supuestos no necesita romper ninguno. Solo necesita situarse en la costura donde se juntan.”
Straiker lo reportó a Anthropic a través de HackerOne el 14 de septiembre. Anthropic lo cerró como Informative: respetar la configuración del repositorio entra en la decisión de confianza del workspace que toma el usuario.
La decisión de confianza, en la práctica
La respuesta de Anthropic es coherente con cómo funciona Claude Code. Cuando
abres una carpeta, se te pregunta si confías en ella, y dijiste que sí. Esa
decisión cubre el CLAUDE.md, los hooks, los subagentes y la suite de tests.
El problema es sobre qué estabas decidiendo. Confiaste en el repositorio lo
suficiente para mirarlo. Para eso lo clonaste. En un portátil, confiar en
esta carpeta lo suficiente para ejecutar sus tests significa confiarle todo lo
que tu cuenta puede alcanzar, porque ahí es donde se ejecuta pytest. Straiker
enumera lo que tenía el implante: cada archivo que puedes leer, credenciales en
memoria, claves SSH en disco y cada repositorio al que puedes hacer push.
Ningún revisor, humano o modelo, lee cada archivo de test de cada proyecto que evalúa. Aquí la revisión se acortó a propósito, pero muchas revisiones honestas también son cortas. Lo que sí puedes elegir es dónde se ejecuta la suite de tests.
El mismo repositorio en una máquina Bromure
En Bromure Agentic Coding, Claude Code se ejecuta dentro de una VM Linux en tu Mac. Cada máquina tiene su propio disco, su propio directorio personal y sus propias credenciales, y todo su tráfico sale a través de un proxy en el Mac. La decisión de confianza del workspace sigue siendo tuya. Lo que cambia es lo que el repositorio de confianza puede alcanzar.
Reproduce la cadena de Straiker dentro de una máquina Bromure. Los pasos uno a
cinco ocurren como antes: el agente lee la guía de contribución, el subagente
revisa las carpetas equivocadas y pytest se ejecuta. A partir del paso seis,
el repositorio choca contra el Mac.
La descarga pasa por el Mac
La carga útil hay que ir a buscarla. El test decodifica una URL y pide un archivo a un host que el proyecto nunca había necesitado. Dentro de una máquina Bromure solo hay un camino hacia fuera: el switch de red virtual desvía cada conexión a los puertos 80 y 443 hacia el proxy del Mac, y contrasta cualquier otra conexión, sea cual sea el protocolo, con las reglas de la máquina.
Esas reglas están en el panel Guardrails de la máquina, en Outbound connections: entradas de permitir y denegar por host, rango de IP, protocolo y puerto, con un ajuste Unmatched traffic en Allow o Deny. Una máquina que usas para evaluar código de desconocidos necesita una lista corta: el proveedor del modelo, los registros de paquetes, GitHub.
allow web api.anthropic.com
allow web pypi.org
allow web files.pythonhosted.org
allow web github.com
default deny
Con esas reglas, la descarga de test_compat.py nunca llega a conectar, y el
try/except: pass que el autor añadió para ocultar fallos ahora oculta este.
Pytest sigue imprimiendo 11 passed. La diferencia está en el Mac. La
Security Timeline de la máquina tiene una fila roja Firewall con un
veredicto de denegación, el host al que el test intentó llegar y el puerto,
con marca de tiempo segundos después de que el agente ejecutara pytest. La
suite de tests de un cliente de métricas no tiene ningún motivo para contactar
con un host del que nunca has oído hablar, y ahora sabes que lo intentó.
El tráfico posterior del implante recibe el mismo trato. Tanto si Sliver llama a casa por HTTPS, por TLS mutuo en su propio puerto, por WireGuard o por DNS, la conexión sale por el switch, y el switch contrasta cada protocolo con la misma tabla.
El implante encuentra sustitutos
Supón que la máquina permitía el tráfico no coincidente, que la carga útil llegó y que Sliver abrió su sesión. El atacante tiene ahora una shell en la VM, con el mismo alcance que Straiker enumeraba para el portátil. En una máquina Bromure ese alcance cubre menos.
- Archivos. La VM contiene su propio directorio personal y las carpetas de proyecto que elegiste compartir con esa máquina. El directorio personal de tu Mac, tus otros proyectos, tus perfiles de navegador y tu Llavero no están dentro.
- Credenciales. Bromure guarda las claves de API reales, los tokens OAuth,
los tokens de git, los secretos de la nube y las contraseñas de bases de
datos cifrados en el Mac. La VM tiene sustitutos con la forma correcta:
ANTHROPIC_API_KEY=sk-ant-api03-brm-…, un tokenghp_que GitHub rechazaría, una clave secreta de AWS que no firma nada. El proxy pone el valor real después de que la petición sale de la VM, y solo para el host al que pertenece la credencial. - Claves SSH.
ls ~/.ssh/id_*no devuelve nada. ElSSH_AUTH_SOCKde la VM es un puente hacia un ssh-agent en el Mac. La VM puede pedir una firma, pero no puede leer la clave. Activa Ask before use en una clave, y cada petición de firma abre un diálogo en el Mac.
Un implante que recoge lo que puede y lo envía a casa tropieza con un segundo
cable. Un token sustituto tiene un único destino legítimo, el host para el que
se emitió. Cuando el proxy ve sk-ant-api03-brm-… o ese token ghp_ dirigirse
a cualquier otro sitio, rechaza la petición con HTTP 451, pausa la VM y abre en
el Mac una alerta titulada This environment may have been compromised, que
nombra el token, el host para el que se emitió y el host al que se envió. Solo
salió un sustituto, así que no hay nada que rotar.
El push pregunta primero
El último elemento de Straiker, los repositorios con permisos de push, es el que convierte un portátil comprometido en un incidente de cadena de suministro. Un atacante con una shell y tus credenciales de git puede hacer push de un commit a cada proyecto que mantienes.
En una máquina Bromure, un push a GitHub, GitLab o Bitbucket es una escritura,
y las máquinas nuevas vienen con Prompt before write para cada servicio
protegido. El proxy retiene el push y macOS muestra Allow write on
"github.com" from workspace "…"? con la operación tal como apareció en la
red, POST /acme/app.git/git-receive-pack. Tú no pediste ningún push, así que
haces clic en Don't allow, y el implante recibe un 403 que termina en
blocked by Bromure Guardrails. macOS dibuja el diálogo a partir de lo que
leyó el proxy, así que nada dentro de la VM puede cambiar su texto.
La traza muestra la revisión que no ocurrió
Cuando se asienta el polvo, quieres saber qué hizo el agente. En el nivel de trazado por defecto, AI request details, el proxy guarda cada intercambio con el proveedor del modelo, cifrado en el Mac, y el Trace Inspector desglosa cada uno en llamadas a herramientas, comandos de shell y lecturas de archivos.
Para este repositorio, el registro responde a las preguntas que importan:
- ¿Qué modelo hizo la revisión? El cuerpo de la petición capturada lo
nombra:
claude-haiku-4-5, un turno después de que la sesión principal leyera.claude/agents/first-pass.md. - ¿Qué leyó? Cada llamada
Read, con su ruta. Hay llamadas para archivos bajosrc/ydocs/, y ninguna paratests/test_compat.py. - ¿Qué se ejecutó? La llamada
Bashque iniciópytest, y junto a ella, en la Security Timeline, la fila del firewall de la descarga que provocó.
El repositorio controla lo que ve el agente y lo que imprime pytest, pero no tiene forma de editar la traza. El proxy escribe ese registro en el Mac, a partir del propio tráfico.
Una máquina para el código que no has leído
El informe de Straiker termina con tres recomendaciones: exigir aprobación
antes de ejecutar un subagente que define el repositorio, leer los archivos de
test antes de ejecutarlos y tratar archivos como .claude/agents/, los hooks y
las definiciones de workflow como entrada no confiable. Las tres son sensatas,
y las tres dependen de que alguien se acuerde, cada vez, en una tarea que
parecía una revisión rutinaria.
En Bromure el hábito seguro cuesta unos pocos clics. Una máquina nueva es un clon copy-on-write de una imagen de Ubuntu firmada, así que apenas ocupa disco. Eso sugiere una configuración para evaluar código ajeno:
- Crea una máquina llamada eval. Dale un proveedor de modelo y nada más: ningún token de git con permisos de push, ninguna clave de la nube, ninguna base de datos.
- Configura su salida para denegar por defecto. En Guardrails › Outbound connections, permite el proveedor del modelo, los registros de paquetes y GitHub, y pon Unmatched traffic en Deny.
- Clona dentro de la VM, no en una carpeta compartida. Inicia la sesión en una carpeta nueva del directorio personal de la máquina. Una carpeta compartida es una ventana en directo a tu Mac, y el directorio personal de la máquina es la parte que Bromure puede rebobinar o borrar.
- O hazla desechable.
bromure-cli vm run --rminicia una máquina que se elimina, disco incluido, cuando se detiene. - Lee la Timeline antes de adoptar la dependencia. Una fila Firewall de denegación durante una ejecución de tests es un hallazgo que ningún revisor reportó, imprimiera lo que imprimiera pytest.
Si algo se cuela, limpias una sola máquina. Rewind home… devuelve el directorio personal al punto de control de un arranque anterior. Una máquina que el detector de compromiso ha marcado se niega a arrancar hasta que elijas Wipe and Launch, que reconstruye su disco y su directorio personal y conserva tus ajustes y las credenciales reales en el Mac. La cuenta de tu portátil, tus otros proyectos y tus otras máquinas nunca estuvieron dentro.
Cuando la biblioteca se haya ganado su sitio, pásala a la máquina que hace el trabajo real. Esa máquina también tiene solo claves sustitutas, y sus push también se detienen en el diálogo del Mac.
La descarga tiene que preguntar
Cada conexión que abre la VM pasa por el switch virtual y el proxy del Mac. Con el tráfico no coincidente denegado, un test que descarga una carga útil desde un host nuevo falla, y la Security Timeline muestra el intento.
La shell encuentra sustitutos
Las claves de API reales, los tokens de git y los secretos de la nube permanecen cifrados en el Mac, y las claves SSH privadas nunca entran en la VM. Un sustituto enviado a cualquier sitio que no sea su host devuelve 451 y pausa la máquina.
Un push necesita un clic en el Mac
Con Prompt before write, el valor por defecto en las máquinas nuevas, un git push espera un diálogo de macOS que muestra la operación exacta. Un implante no puede responderlo.
Un registro que el repo no puede editar
La traza en el Mac muestra qué modelo revisó el código, qué archivos leyó y qué comando se ejecutó después, imprimiera lo que imprimiera la terminal.
Confía menos en el repositorio
El repositorio no hizo nada exótico. Escribió una guía de contribución, definió un agente auxiliar y publicó un archivo de test. Los proyectos seguirán haciendo las tres cosas, porque son útiles, y los agentes seguirán siguiéndolas, porque seguir las convenciones de un proyecto es lo que hace bueno a un agente en su trabajo.
La respuesta de Anthropic deja la decisión en tus manos, y ahí es donde debe estar. Tómala en una máquina hecha para eso: una en la que confiar en un repositorio lo suficiente para ejecutar sus tests signifique confiarle una VM, unos cuantos sustitutos y una lista corta de hosts. Luego deja que la suite de tests imprima lo que quiera.