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

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

A recruiter writes that they liked your GitHub. The role fits your stack, 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 the TODO list in 48 hours.

That last step is the whole attack. Nobody hacked you. You were asked to run the malware yourself, and you wanted the job.

This is not a one time thing

Researchers track this pattern as Contagious Interview, and Microsoft, Palo Alto Unit 42 and ESET have all written about it. The actors behind it are linked by those researchers to North Korea.

The numbers are not small. Socket counted 338 malicious npm packages and about 50,000 downloads tied to the campaign as of October 2025. A newer wave called PolinRider, reported in July 2026, covers 108 malicious packages and extensions, and 1,951 public GitHub repositories were compromised as of April 2026.

So the repo you are sent can be a fresh one built for you, or a real project that was quietly poisoned. Either way, it was built for people like you: developers with a salary, a wallet and a lot of access.

What they actually take

Not your project files. Your money and your access:

  • Wallet data. Browser extension storage (MetaMask and similar) and key files, which is enough to empty a wallet.
  • Browser sessions and saved logins. The attacker can act as you on email, GitHub or an exchange without knowing a password.
  • SSH keys and cloud credentials (~/.ssh, ~/.aws). These open servers and your employer's code, so one take-home can become a company breach.
  • Tokens and .env files from config folders.

Socket describes the stealer used in these campaigns as scanning dozens of browser profile directories, and some variants also install a backdoor so the attacker keeps access after the first theft.

How it reaches you, step by step

  1. The message. A recruiter contacts you, often on LinkedIn, with a role that matches your profile.
  2. The call. A short interview. Questions sound real.
  3. The task. A repo link and a deadline.
  4. The install. npm install triggers a postinstall hook, which runs scripts with your user's permissions, before you have read a single line of the project.
  5. The theft. A payload hidden as an encoded string is decoded at runtime, looks through browser profiles and config folders, and sends what it finds to a server the attacker controls.

You never see an error. The project may even build and run, so everything feels normal.

What to watch for before you ever clone

Most people only think about step 4. The earlier steps give you much more time to notice.

In the message

  • Pay well above market, stated upfront, to someone who never applied.
  • They quote your own GitHub or stack back to you. That is research, not a coincidence.
  • A recruiter profile with few connections, no mutual contacts, or a company you cannot find on its own careers page.
  • A quick move to Telegram or Discord, where there is no record.

On the call

  • The camera is "broken", or the face looks slightly off, or audio and lips do not match.
  • You are asked to share your screen and run the project live, sometimes without Docker.

In the task

  • A deadline built to stop you from reading ("submit within 48 hours", "other candidates are in the final round").
  • A private repo under an organization with no history, instead of a platform like HackerRank or CoderPad.
  • "Just run npm install", repeated in a friendly follow up.
  • A request to turn off your antivirus because it "flags our custom compiler". That is never a technical requirement.

One of these alone proves nothing, because real recruiters have broken cameras too. Several together are a pattern, and that is the moment to slow down.

Check the message first

The cheapest filter is the message itself. We built a free AI scam detector (our project, disclosed here because it is ours). Paste the recruiter's message, the repo link or a screenshot of the chat, and it lists the red flags it finds. It reads text for patterns. It cannot confirm that a real person is behind the account, so treat it like the script below: a first filter that tells you when to slow down.

Then look at the repo: a small audit script

This script does not run the project. It only reads files and prints what would execute on install, and what looks like hidden code. Save it as audit.js (it is also in a small MIT licensed repo with a self test: github.com/cryptostrapon/npm-install-audit).

What it does: it reads package.json and lists the install hooks, then scans .js, .cjs, .mjs and .sh files for the patterns this attack uses: 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.

#!/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 a base64 payload, spawns a shell 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

What it cannot see. A clean result does not make a repo safe, and a hit is not always malware, since real projects use these hooks too. It also misses things on purpose or by design: code that is fetched from the network after install, obfuscation it does not recognize, and malicious dependencies that are not in the repo itself. The PolinRider write-ups also describe VS Code task files (.vscode/tasks.json) that run code the moment a folder is opened, with no npm install at all. So the script is a first filter, not a verdict. If it flags something you do not understand, that is your answer.

A safer habit

  • Install with npm install --ignore-scripts first, so hooks do not run, and read before you enable anything.
  • Open unknown folders in VS Code in Restricted Mode and do not click Trust until you have read the project.
  • Use a throwaway VM or container, never the machine that holds your wallets and browser sessions.
  • If the company is real, check the job on its own careers page and write to a recruiter from there, not from the link you were sent.
  • Keep wallet keys and signed in browser profiles off the machine you use for take-home tasks.

If you already ran it: treat that machine as compromised. From a different, clean device, move your funds first, then rotate passwords, SSH and API keys, and revoke active sessions. Do not log in to anything from the infected machine.

Test yourself

If you want to see how many of these tells you catch, we have a free scam quiz built from real cases, and our full write ups on this pattern are the fake test project investigation and the deepfake interview one.

We take apart real crypto and developer scams here. If that is useful to you, follow @cryptostrapon.

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

Top comments (0)