DEV Community

Cover image for You got a take-home test from a recruiter. You have 5 minutes before npm install
cryptostrapon
cryptostrapon

Posted on Edited on

You got a take-home test from a recruiter. You have 5 minutes before npm install

A recruiter likes your GitHub. The pay is 20 to 40 percent above market, the call goes well, and then comes the take-home: clone the repo, run npm install, finish it in 48 hours.

That last step is the whole attack. Nobody hacked you. You ran the malware yourself.

This is a real campaign

Researchers call it Contagious Interview and link it to North Korea. Socket counted 338 malicious npm packages and about 50,000 downloads as of October 2025. A newer wave, PolinRider, covers 108 malicious packages and extensions, and 1,951 public GitHub repositories were compromised as of April 2026.

What they take

Not your project files. Your wallet data, browser sessions and saved logins, SSH keys and cloud credentials, and tokens from .env files. One take-home can become a company breach.

How it works

npm install runs a postinstall hook with your permissions before you have read anything. A hidden payload decodes itself, searches browser profiles and config folders, and sends what it finds to the attacker. You see no error, and the project may even run.

The tells, before you clone anything

  • The message: pay far above market, your own GitHub quoted back to you, a recruiter with few connections, a quick move to Telegram or Discord.
  • The call: the camera is "broken", the face looks off, or you are asked to run the project live on screen share.
  • The task: a deadline built to stop you reading, a private repo from an organization with no history, "just run npm install", a request to turn off antivirus.

One tell alone proves nothing. Several together are a pattern, so slow down.

Check the message, then the repo

Paste the recruiter's message into our free AI scam detector (our project, disclosed because it is ours) and it lists the red flags it finds. It reads text for patterns and cannot confirm who is behind an account.

Then check the repo. This script only reads files: it lists install hooks and flags shell commands, hidden base64, long encoded strings and paths to wallet, browser and key data. It never runs the project and never touches the network. It is also in a small MIT repo with a self test: github.com/cryptostrapon/npm-install-audit.

#!/usr/bin/env node
// npm-install-audit: look before you run.
// Usage: node audit.js ./some-repo
// Read only, no network, no dependencies. A clean result is not proof of safety.
const fs = require("fs");
const path = require("path");

const target = process.argv[2] || ".";
let root;
try {
  root = path.resolve(target);
  if (!fs.statSync(root).isDirectory()) throw new Error("not a directory");
} catch (e) {
  console.error("Cannot read directory: " + target);
  process.exit(2);
}

const HOOKS = ["preinstall", "install", "postinstall", "prepare"];
const SKIP_DIRS = new Set([".git", "node_modules"]);
const RISKY = [
  [/child_process|execSync|execFileSync|spawnSync|\bspawn\(/, "runs shell commands"],
  [/\beval\(|new Function\(/, "evaluates code at runtime"],
  [/\batob\(|Buffer\.from\([^)]*base64/, "decodes base64 at runtime"],
  [/(curl|wget)\s[^\n]*\|\s*(sh|bash)/, "pipes a download into a shell"],
  [/Login Data|keystore|wallet\.dat|\.ssh[\/\\]|id_rsa|\.aws[\/\\]credentials|Local Storage[\/\\]leveldb/i, "touches key, wallet or browser data paths"],
];

let findings = 0;
const flag = (file, msg) => {
  findings++;
  console.log("! " + path.relative(root, file) + ": " + msg);
};

function checkPackage(file) {
  let pkg;
  try { pkg = JSON.parse(fs.readFileSync(file, "utf8")); }
  catch { flag(file, "package.json could not be parsed, read it by hand"); return; }
  for (const h of HOOKS) {
    if (pkg.scripts && pkg.scripts[h]) flag(file, h + " hook runs: " + pkg.scripts[h]);
  }
}

function checkSource(file) {
  let text;
  try { text = fs.readFileSync(file, "utf8"); } catch { return; }
  for (const [re, why] of RISKY) if (re.test(text)) flag(file, why);
  if (!/\.min\.js$/.test(file)) {
    const blob = text.match(/[A-Za-z0-9+\/=]{300,}/);
    if (blob) flag(file, "long encoded string (" + blob[0].length + " chars)");
  }
}

function walk(dir, depth) {
  if (depth > 8) return;
  let entries;
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
  for (const e of entries) {
    if (SKIP_DIRS.has(e.name)) continue;
    const p = path.join(dir, e.name);
    if (e.isSymbolicLink()) continue;
    if (e.isDirectory()) { walk(p, depth + 1); continue; }
    if (!e.isFile()) continue;
    if (e.name === "package.json") { checkPackage(p); continue; }
    if (/\.(js|cjs|mjs|sh)$/.test(e.name)) {
      let size = 0;
      try { size = fs.statSync(p).size; } catch { continue; }
      if (size < 2e6) checkSource(p);
    }
  }
}

walk(root, 0);
if (findings) {
  console.log("\n" + findings + " finding(s). Read them before you run npm install.");
  process.exit(1);
}
console.log("No obvious install hooks or risky patterns found. Still read the code.");
Enter fullscreen mode Exit fullscreen mode

Run it on the cloned folder:

git clone <repo> task && node audit.js ./task
Enter fullscreen mode Exit fullscreen mode

On a repo with a postinstall that decodes base64 and reads ~/.ssh/id_rsa, it prints:

! package.json: postinstall hook runs: node ./scripts/setup.js
! scripts/setup.js: runs shell commands
! scripts/setup.js: decodes base64 at runtime
! scripts/setup.js: touches key, wallet or browser data paths
! scripts/setup.js: long encoded string (380 chars)

5 finding(s). Read them before you run npm install.
Enter fullscreen mode Exit fullscreen mode

It is a first filter, not a verdict. It cannot see code fetched after install, obfuscation it does not recognize, or VS Code task files (.vscode/tasks.json) that run code the moment you open a folder.

A safer habit

  • Run npm install --ignore-scripts first and read before you enable anything.
  • Open unknown folders in VS Code in Restricted Mode and do not click Trust.
  • Use a throwaway VM or container, never the machine with your wallets and browser sessions.
  • Verify the job on the company's own careers page.

If you already ran it, treat the machine as compromised. From a clean device, move your funds first, then rotate passwords and keys and revoke sessions.

Full write up: fake test project investigation.

Question for the comments: has a recruiter ever sent you a take-home that made you stop? What was the tell?

Top comments (2)

Collapse
 
sven_esser_94287e300ee703 profile image
Sven Esser •

As a freelance developer I get so many scam projects, mainly containing hidden obfuscated code that steals MetaMask wallet keys. The main use for Claude Code has become checking projects before I run them, which saves me so much time.

Collapse
 
cryptostrapon profile image
cryptostrapon •

Same pattern here. The obfuscated part usually sits behind a postinstall hook, so reading the scripts block first catches most of them before anything runs.