In October 2024, Docusaurus maintainer Sébastien Lorber noticed something odd in a pull request touching the cliui package: an npm aliasing declaration that didn't match the package it claimed to resolve to. Pulling that thread with lockfile-lint, he surfaced three packages — string-width-cjs, strip-ansi-cjs, and wrap-ansi-cjs — all published by a single npm account, himanshutester002. Each one aliased itself to a real, widely-used terminal-formatting library (string-width@^4.2.0, strip-ansi@^6.0.1, wrap-ansi@^7.0.0), and each contained no functional code whatsoever — just a bare package.json. Snyk's security research team, which documented the find, reported that strip-ansi-cjs alone had over 500 dependent packages and was pulling 7,274 weekly downloads at the time of discovery. No malicious payload was ever found. That absence is the actual story: this looks like a "warm-up" — building install-base and trust inside real dependency trees before, potentially, a later version ships something dangerous. Here's what happened, how it was caught, and what it means for how much trust your lockfile silently extends to packages you never chose.
What did the string-width-cjs packages actually do?
Functionally, almost nothing — and that's the point. Snyk's analysis found string-width-cjs, strip-ansi-cjs, and wrap-ansi-cjs contained no logic: no functions, no install scripts, no network calls, just minimal package.json manifests. What made them notable wasn't behavior but naming and dependency structure. Each package name and version pattern was built to alias a real, popular utility already trusted by the npm ecosystem — string-width, strip-ansi, and wrap-ansi are terminal-string-width and ANSI-escape utilities used deep inside tools like Yargs and CLI table renderers. By using npm's alias syntax (installing a package under one name while pointing its resolution at another), the publisher could get these hollow shells declared as dependencies inside other packages' manifests, riding into real projects without anyone auditing an empty shell as suspicious on sight.
How was this discovered, and why did it take a manual PR review?
It was caught by accident, not by automated tooling at scale. Sébastien Lorber spotted unusual aliasing syntax while reviewing a pull request against cliui in October 2024, then used lockfile-lint — a tool built specifically to catch mismatches between a declared package name and what it actually resolves to in a lockfile — to confirm the mismatch and trace it further. That's a narrow discovery path: it depended on one maintainer noticing something visually unusual in a diff, then reaching for a niche lint tool most teams don't run in CI at all. Snyk's writeup frames this explicitly as the gap the incident exposes — most CI pipelines validate that installs succeed and tests pass, not that every resolved package name in the lockfile actually matches its declared source.
Why does an empty package matter if it never shipped malware?
Because publishing benign versions before a malicious one is a documented evasion pattern, not a hypothetical. Snyk's stated working theory is that these packages functioned as a "warm-up" — establishing a plausible-looking install history and adoption footprint by aliasing into real dependency graphs, so a future version swap or update could ship malicious code to an already-trusted, already-widely-installed package with far less scrutiny than a brand-new publish would draw. The same publisher account, himanshutester002, also shipped isaacs-cliui (mimicking the legitimate scoped package @isaacs/cliui) and azure-sdk-for-net, which Snyk flagged as a plausible dependency-confusion attempt against Microsoft's real Azure SDK naming convention — a pattern of behavior across multiple packages, not an isolated incident.
What's the difference between typosquatting and this kind of aliasing?
Classic typosquatting relies on a human mistyping a package name at install time — reqeusts instead of requests. The string-width-cjs case didn't need a human to make any typo at all: the packages got pulled in as declared, intentional dependencies of other packages further up the chain, via npm's alias mechanism, which lets a package.json say "install X, but resolve it as Y." A developer running npm install on their own project never typed any of the three malicious package names — they inherited them transitively, buried at whatever depth the intermediate package sat in their tree. That's a materially harder problem than typosquat detection at the point of install, because the trust decision was made by someone else's package.json, several hops upstream, and most teams never review transitive manifests at all.
How Safeguard helps
This pattern — hollow packages aliasing trusted names to accumulate trust before a payload ever ships — is exactly what Safeguard's malware classifier, Eagle, is built to catch. Eagle scores every npm publish against seven indicator classes, including "typosquat similarity" (name within edit-distance 2 of a top-1000 package under a different publisher) and "metadata anomalies" (a newly-published package with no repository URL and unusual author history) — and it explicitly flags "package trust warm-up," publishing a run of benign versions before a malicious one, as a named bypass-attempt pattern rather than something teams have to notice by hand. Because Eagle retroactively re-scores its entire corpus whenever it's updated, a package that looked empty-but-harmless on day one gets flagged automatically if a later signal changes its classification, without waiting on a maintainer to notice odd aliasing syntax in a diff. On the transitive side, Safeguard's dependency scanning resolves graphs to 100 levels deep and cross-references every resolved package name against known typosquat patterns and internal registries, tagging mismatches as dependency-confusion findings — the automated version of the manual lockfile-lint check that caught string-width-cjs in the first place, run continuously rather than depending on one maintainer's curiosity.