Un gusano entró a npm por la puerta de adelante: procedencia válida, atestación SLSA, firmado por los flujos de compilación del proyecto. Después dejó de necesitar que instalaras nada. Le basta con que abras la carpeta.
No hubo npm install. No hubo dependencia nueva. No hubo CVE.
Hubo un archivo de configuración, versionado en el repositorio, que tu editor obedece sin preguntar.
Treinta y dos minutos y veintitrés segundos
keyv es una librería de caché. cacheable, flat-cache y cache-manager son sus vecinas: dependencias que nadie elige y que todo el mundo tiene, arrastradas tres niveles abajo por otra cosa. Entre las once que cayeron, keyv sola pesa 604 millones de instalaciones al mes.
El 4 de agosto de 2026, a las 09:02:37 UTC, un commit sin firmar entró directo a la rama main de keyv. A las 09:04, un commit verificado —firmado, con su sello verde— agregó los hooks de ejecución de Claude y de VS Code. A las 09:23, otro quitó la prueba del preinstall. A las 09:35:00.763, keyv@6.0.0 salió a npm por trusted publishing con OIDC y atestación SLSA válida.
Treinta y dos minutos y veintitrés segundos entre el primer commit y el paquete firmado en el registro. Sin revisión de nadie: los cambios entraron derecho a main y la versión salió detrás.
Once paquetes semilla en la primera hora. Después el gusano se soltó solo: 433 paquetes más y 2.201 versiones antes de las 13:20. Al corte de las 18:10, 444 y 2.212, con más de 2.000 millones de instalaciones al mes.
Lo que pasa cuando encuentra un token
El vector de entrada es una función legítima de npm: un preinstall, que corre comandos arbitrarios antes de instalar el paquete. Lo que cuelga de ahí no tiene nada de legítimo.
setup.mjs es un cargador que baja el binario legítimo de Bun v1.3.13 desde las publicaciones oficiales en GitHub —tráfico impecable, dominio impecable— para correr debajo de él la segunda etapa: 728 KB de JavaScript ofuscado con aplanamiento de flujo de control y cadenas en Base91.
Esa etapa barre el disco: 290 patrones en Linux, 129 en macOS, 50 en Windows contra material de SSH, Git, npm, Docker, Kubernetes y la nube. Y busca once archivos que las olas anteriores no buscaban: los de Claude, Codex, Cursor, OpenAI, Gemini. El botín ya no es solo tu nube. Es tu asistente.
El hecho que define el caso llega cuando la carga encuentra un token ghs_. Aikido lo describe así: "It adds malicious hooks to .claude/settings.json and .vscode/tasks.json so that the payload executes automatically the next time any developer opens the repository in VS Code or starts a Claude Code session inside it, with no npm install required."
Los commits salen a nombre de claude, con el mensaje chore: update config.
Y los dos archivos están cruzados. Chainguard lo deletrea: el SessionStart del agente llama a .vscode/setup.mjs, y la tarea de VS Code —etiquetada "Environment Setup", con runOn: folderOpen— llama a .claude/setup.mjs. Cycode advierte lo que eso implica al limpiar: están "cross-wired, each invoking the other's setup.mjs, so cleaning only one leaves a live path". De los tres gatillos, dos nunca tocan node_modules.
El C2 tampoco vive donde uno lo busca. Vive en Ethereum: el gusano interroga un contrato con un eth_call, rotando por decenas de servidores RPC públicos hasta que alguno conteste, y así recibe el dominio de exfiltración vigente. El 4 de agosto una transacción lo movió a otro recién registrado. No hay servidor que tumbar. Habría que convencer a Ethereum. Y por si rotas las llaves, queda un interruptor de hombre muerto: un monitor que ejecuta una carga del atacante cuando el token robado es revocado.
El archivo que ningún escáner lee
En marzo contamos cómo un pip install de LiteLLM ejecutaba código sin que nadie escribiera import litellm: bastaba con que un archivo .pth existiera en el ambiente. La jugada es idéntica, un piso más arriba. Antes era el intérprete de Python el que arrancaba cosas que nadie clasificó como código. Ahora es el agente.
El día anterior al ataque, Datadog publicó el inventario: un proyecto puede provocar ejecución de código en un agente "through hooks, skills, MCP servers, editor tasks, development-container settings, environment variables, runtime startup files, and ordinary executables." De esa lista, los servidores MCP ya tienen su propia campaña. ChainDrop se quedó con dos. Y el precedente ya estaba catalogado como MAL-2026-3648: tres paquetes de npm que instalaban hooks SessionStart.
Ahí está el punto ciego. Un escáner de dependencias lee el package.json y el package-lock.json, recorre node_modules. No lee .claude/settings.json. No lee .vscode/tasks.json. Nadie los puso en la lista de lo ejecutable porque terminan en .json, y un .json son datos. Hasta que alguien los ejecuta.
Nada de esto ocurre sin un clic, y la versión honesta es peor que la del ataque invisible. El clic existe: es el diálogo de confianza de la carpeta. Zero Hunt lo formula mejor que nadie — es "the workspace-trust prompt they've long since learned to click through". Lo que apruebas ahí no dice "ejecuta este comando"; dice "confío en esta carpeta". Aceptado una sola vez, los hooks del proyecto corren al arrancar la sesión sin volver a preguntar. ¿Cuántas veces diste ese clic hoy?
Hubo un momento en que ni el clic hacía falta: CVE-2025-59536 describía hooks SessionStart que corrían antes de que se dibujara el diálogo, y Anthropic lo corrigió. ChainDrop no usó ese camino. Entró por el legítimo, el de después del clic, el que funciona tal como está documentado. Un agujero se parchea. Esto no es un agujero.
La firma era válida
El 22 de septiembre de 2025, después del Shai-Hulud original, GitHub publicó su plan para un npm más seguro: 2FA obligatoria, tokens granulares de siete días, trusted publishing. El camino recomendado. El mismo que usó ChainDrop, diez meses y trece días más tarde, para firmar el gusano.
StepSecurity lo resume en dos frases que deberían ir grabadas en la puerta del registro: "Provenance proves which commit was built. It cannot prove the commit was authorized." Snyk explica el mecanismo: la fuente maliciosa ya estaba en el estado etiquetado del repositorio, así que el flujo legítimo compiló y certificó el artefacto malicioso. ¿Qué garantiza una firma emitida correctamente sobre un commit que nadie autorizó?
Tampoco hay CVE. Phoenix lo dice completo: "There is no CVE for this incident, and provenance signatures on the poisoned releases were valid, so neither a CVE feed nor an SLSA badge would have flagged it."
Socket marcó keyv@6.0.0 unos seis minutos después. npm empezó a despublicar a las 10:39. A las 11:16 —una hora y cuarenta y un minutos después de la publicación— el inventario de Snyk todavía encontraba ocho versiones maliciosas en la etiqueta latest. Detectado en seis minutos. Servido durante ciento uno.
La medida más limpia del alcance salió del CI ajeno: StepSecurity escaneó unas 44.000 corridas públicas de flujos de trabajo en ocho horas y encontró quince coincidencias. Cinco eran de su propio repositorio de pruebas. Las otras diez "were all from a single real project, backstage/backstage": un proyecto de la CNCF instalando dependencias frescas fuera del package-lock.json. PAILA en su propio CI.
El 6 de agosto, Singapur emitió el boletín AD-2026-009: quitar las versiones afectadas, tratar los sistemas como comprometidos, rotar credenciales. Siete acciones. Ninguna nombra .claude/settings.json, ni .vscode/tasks.json, ni a los agentes de código. El documento oficial describe un ataque de npm. El ataque ya había dejado de ser solo de npm.
Atribución
Perpetrador: sin nombre. Unit 42 es explícita: ChainDrop pertenece al linaje de Shai-Hulud, pero "we cannot yet say whether it's operated by the group known as TeamPCP, or by another group adapting the published toolkit for their own purposes." El grupo que mapeamos en marzo es sospechoso, no acusado. Lo único firme es la preparación: el contrato de C2 se desplegó setenta y un días antes, desde la cartera que después movió el dominio. El mantenedor no es sospechoso: es la víctima a la que le tomaron la cuenta.
Cómplices: el preinstall, que npm corre sin preguntar. El clic de confianza que la industria entrenó a todos a dar sin leer. Y una regla que hubiera bastado: alertar cuando un paquete agrega o cambia un script preinstall, postinstall o prepare entre versiones. Phoenix afirma que "The lifecycle-script delta rule alone would have caught every Shai-Hulud wave to date." Una regla. Todas las olas.
Falla sistémica: le pedimos al ecosistema una prueba criptográfica de origen y nos la dio, funcionando exactamente como fue diseñada, encima de una cuenta robada. Mientras tanto le agregamos a cada repositorio un archivo que le dice a una máquina qué comandos correr al abrirse, y no lo pusimos en ninguna lista. La cadena de suministro dejó de terminar en el paquete: termina en la configuración del asistente que instalaste para revisar código sospechoso.
Pasamos años aprendiendo a verificar de dónde viene el código que instalamos. ChainDrop no falsificó ni una firma: todas eran auténticas, todas verificaban. Solo esperó a que abrieras la carpeta.
¿Cuántas cosas más, en el repositorio que acabas de clonar, están esperando a que tu editor les pregunte qué hacer?