A LinkedIn Recruiter Got a Developer to npm install a Backdoor. We Scanned the Repo.
The malicious GitHub repo used a prepare lifecycle script to execute arbitrary remote commands the moment you ran npm install. The dependencies looked clean. The attack was already in the repo.
Today’s HN front page: a developer wrote up a social engineering attack that used LinkedIn to deliver a npm backdoor. A fake recruiter — using a real arts journalist’s stolen profile — reached out about a full-stack role. The job task: clone a GitHub repo and “check out the deprecated Node modules issue.”
The repo looked legitimate. 39 commits attributed to a real developer whose GitHub history appeared authentic. A React frontend, a Node backend. Standard stuff.
The trap was in package.json:
"scripts": {
"prepare": "app:pre",
"app:pre": "node app/index.js"
}
npm’s prepare lifecycle script runs automatically after npm install.
The prepare hook called app:pre, which invoked node app/index.js.
The payload — 250 lines of code hidden in app/test/index.js, disguised as test infrastructure —
assembled a URL from fragmented strings and executed whatever the server sent back.
The author caught it in seconds using an AI agent in read-only mode. But most developers would have just run the install.
What the dependency profile looked like
The malicious repo was a React/Node project. We reconstructed the likely dependency profile — the standard packages you’d expect in a full-stack interview task — and ran them through Commit’s behavioral audit.
| Package | Score | Publishers | Downloads/wk | Age | Flags |
|---|---|---|---|---|---|
jest | 94 | 5 | 45.8M | 14.3y | — |
express | 92 | 5 | 110.8M | 15.5y | — |
react | 90 | 2 | 143.6M | 14.6y | — |
supertest | 86 | 6 | 15.4M | 14y | — |
dotenv | 86 | 3 | 146.1M | 12.9y | — |
body-parser | 87 | 4 | 115M | 12.4y | — |
cors | 80 | 3 | 60.7M | 13.4y | — |
axios | 88 | 1 | 122.7M | 11.8y | CRITICAL (sole publisher) |
lodash | 80 | 1 | 161.3M | 14.1y | CRITICAL (sole publisher) |
nodemon | 81 | 1 | 13M | 15.4y | CRITICAL (sole publisher) |
The dependencies themselves are legitimate. Most score in the 80–94 range. Three are flagged CRITICAL — not because they’re malicious, but because they have a sole npm publisher and massive download counts. That structural profile is a standing attack target, as axios demonstrated in March.
But none of these packages were the attack. The attack was already in the repo.
The gap: behavioral scoring covers the registry. This attack bypassed it.
Commit’s behavioral scoring — longevity, publisher depth, release consistency, provenance —
evaluates packages on the npm registry. The LinkedIn attack didn’t use the registry.
The attacker cloned a GitHub repository directly and hid the payload in application code,
using npm’s prepare lifecycle hook as the trigger.
This is the same mechanism every npm supply chain attack uses. The only difference is delivery: instead of a published package, it was a private repository. The hook, the obfuscation, the remote execution pattern — identical.
If the attacker had published this as an npm package instead:
| Signal | Legitimate package | This attack (if on npm) |
|---|---|---|
| Publisher history | Years of prior publishes | New account, no history |
| Download count | Millions/week | 0 before targeting |
| Release consistency | Regular versioned releases | 1 version, v1.0.0 |
| Provenance | Linked CI/CD pipeline | None |
| Behavioral score | 80–94 | <10 (bottom percentile) |
The Atomic Arch packages we scanned last week scored 12 and null.
This would score lower. Zero downloads. Zero history. A prepare script that runs
arbitrary code on install. Every behavioral dimension would be zero.
The prepare hook is the tell
npm has seven lifecycle scripts that run automatically: preinstall, install,
postinstall, prepublish, prepare, prepublishOnly,
prepack. Developers know this intellectually. They still npm install without checking.
The attack author notes that Pi — an AI agent running in read-only mode — “stopped almost immediately” and flagged the suspicious code. An automated tool caught in seconds what manual code review might have missed entirely.
proof-of-commitment hook intercepts every npm install (and bun add,
pip install, cargo add). It checks behavioral scores before execution.
Packages with no history get blocked before the lifecycle scripts run. In the LinkedIn attack scenario,
that interception point is exactly where the payload triggers.
One command to add it to your environment — and to Cursor, Claude Code, Windsurf, and any other tool that runs installs on your behalf:
npx proof-of-commitment hook The real lesson isn’t the mechanism — it’s the vector
Every supply chain attack we’ve covered this year started with a plausible reason to install something. Shai-Hulud used compromised maintainer accounts. TrapDoor poisoned AI assistant suggestions. Atomic Arch used AUR orphan adoption. This attack used LinkedIn.
The delivery mechanism is always different. The payload mechanism is always the same: get a developer to run an install, trigger the lifecycle hook, execute before review.
Behavioral scoring is designed for npm registry packages. It doesn’t scan GitHub repos you clone for interview tasks. That gap is real. But the 1,451 people who upvoted this article on HN are largely developers who do install npm packages — from registries, from CI scripts, from AI coding tools that add dependencies automatically. That surface is exactly what behavioral scoring covers.
And if this attack had been delivered as a package instead of a repo, it would have scored in the single digits. The signal would have been there before the first install.