Malware Audit & Cleanup Playbook (JavaScript / Node projects)
# Malware Audit & Cleanup Playbook (JavaScript / Node projects)
Copy this file into any project and either follow it by hand or give it to Claude Code with:
> Follow `malware-audit-playbook.md` in this project. Obey the Safety Contract in section 0 exactly: remove only the malware, delete nothing else, commit/push nothing, and finish with the instructions for me.
Written from a real incident (Oct 2026): an obfuscated payload was appended to `postcss.config.mjs`, a deploy script and other config files, launched hidden background `node` processes, tried to contact a remote server, and spread to ~10 projects on one machine.
---
## 0. SAFETY CONTRACT (binding for Claude Code, Cursor, any AI agent, and scripts)
The goal of an automated run is narrow: **neutralise the malware and report.** Everything else is left to the human, with written instructions.
### 0.1 The agent MAY (automatically)
| Action | Condition ||--------|-----------|| **Read / search** files, `git log`, `git show`, `git status`, process list, `lsof`, launch-agent listings | Always; read-only || **Run the read-only scanner** (§3.2) | Always || **Stop a running payload process** (`kill <PID>`, never `kill -9` first, never `killall node`) | Only a PID whose command line matches a payload marker (§1.1), after printing its PID, start time, working directory and command (first 120 chars) || **Delete the malware itself** — (a) remove the payload code from an infected file, by restoring that single file from the last clean commit or by cutting the payload off the end of the line (§5); (b) delete a file that consists *only* of malware (a dropper that is not in any clean commit and has no legitimate content) | Only what the scanner/inspection confirmed as malware (MARKER or HIDDEN). **No backup copy of the malware is kept** — don't create quarantine folders or `.infected` copies. Show the user the file list and the `git diff --stat` afterwards. Legitimate code in the same file is preserved || **Re-run the scanner** to verify | Always || **Write the final report** (§10) and the **instructions for the user** (§0.4) | Always |
### 0.2 The agent MUST NOT (without the user typing an explicit "yes" for that exact action in the chat)
-**Delete anything other than the confirmed malware itself (§0.3)**: no `rm`, `rm -rf`, `git clean`, `git rm`, `find -delete`, `trash`, `mv` to trash — not files, folders, `node_modules`, build output, logs, launch agents, cron entries, extensions, or apps. The only deletions allowed are the payload code and malware-only files confirmed under §0.3. If anything else *should* be deleted, put it in the user instructions (§0.4) instead.-**Rewrite or discard history/work**: no `git reset --hard`, `git checkout -- .` / `git restore .` on the whole tree, `git stash drop`, `git rebase`, `git filter-repo`, `git push --force`, branch deletion.-**Commit, push, tag, open PRs, or change remotes.** Leave the cleaned files as an uncommitted change for the human to review.-**Touch any file that is not confirmed infected** — no "while I'm here" fixes, formatting, dependency upgrades, lockfile edits.-**Run project code**: `npm/yarn/pnpm install|ci|run`, `dev`, `build`, `start`, tests, deploy scripts, config files, or the payload (it can re-infect and exfiltrate).-**Change credentials or accounts**: no logging out/in, no token/key creation or revocation, no editing `.env*`, no reading `.env` contents into the chat.-**Modify system persistence or software**: no editing/unloading launch agents/daemons, cron, shell rc files, login items; no uninstalling apps or extensions; no `sudo`.-**Scan or modify other projects/directories** than the one it was asked to work in, unless the user names them.-**Deobfuscate by executing**, or paste payload contents, IPs-as-clickable-links, tokens or secrets into the chat. Describe the payload; don't reproduce it.-**Continue after a surprise.** If the situation doesn't match this playbook (unexpected files, scanner errors, a process that won't die, a file with both malware and real code you can't separate), **stop and report** instead of improvising.
### 0.3 How malware is deleted (no quarantine, no backups of the payload)
The malware is **deleted, not preserved**. Do not copy it to a quarantine folder, rename it to `.infected`, or paste it anywhere. Recovery of the legitimate content comes from git history, which still has the clean versions.
1.**Confirm it is malware** (scanner MARKER/HIDDEN, or you inspected the long line's start with `awk ... substr`).2.**Make sure the legitimate part is recoverable**: `git status` shows the file's real code is in git (last clean commit exists), or the legitimate lines are visible before the payload. If neither is true — e.g. the file is untracked, has uncommitted legitimate edits mixed with the payload, or you can't tell where the real code ends — **stop and ask** instead of deleting.3.**Delete only the payload**:- Preferred: restore the single file from the last clean commit (§5.1) and re-apply any legitimate edits the entry commit made.- Otherwise: cut the payload text off the line, keeping the legitimate code (§5.2).- If the whole file is malware (no legitimate content, not in any clean commit): delete that one file.4.**Verify** with the scanner that the markers are gone and that the file's size/line count matches a normal version.5. Leave the change **uncommitted** for the user to review (§0.4).
### 0.4 Final output the agent must give the user (always)
End every run with a message in this shape (plain language, no payload contents):
```MALWARE AUDIT RESULT — <project>
STATUS: INFECTED / CLEAN / NEEDS YOUR DECISION
WHAT I DID (automatically): - Stopped process(es): <PID, started, folder it ran from> (or "none found") - Deleted the malware from: <file list> (legitimate code kept; clean versions are in git history <hash>) - Re-ran the scanner: clean / still flagged <files>
WHAT I FOUND: - Infected files: ... - Entry commit: <hash, date, subject>; pushed to remote: yes/no - Last clean commit: <hash> - Persistence found: none / <details, NOT changed> - Suspicious package scripts / extensions: ...
WHAT I DID NOT DO (on purpose): delete files, commit, push, install, run the project, touch credentials or system settings.
WHAT YOU NEED TO DO (in order): 1. Review the change: git diff --stat and git diff -- <files> 2. (If happy) commit it: git add -p && git commit -m "Remove injected malware from <files>" — then push only after step 5. 3. Reinstall dependencies safely: <exact commands for this package manager with --ignore-scripts> 4. Check the other projects on this machine (run this playbook in each) — these also contain markers: <list if known> 5. Rotate credentials (see section 7), AFTER the machine is clean: <specific list for this project: .env keys, cloud tokens, GitHub tokens, SSH keys...> 6. Tell collaborators if the malware was pushed to a shared branch; they must scan and rotate too. 7. Optional hardening: pre-commit guard (§8), editor whitespace/wrap settings, run unknown code in a VM.
QUESTIONS FOR YOU: <anything that needed a decision, e.g. file mixes real code and payload>```
### 0.5 General rules
1.**Never execute, import, `eval` or "test run" the suspicious code.** Read files with `cat`, `head -c`, `grep`, `git show`.2.**Don't "deobfuscate by running it".** Analysis is static only; it isn't needed for this playbook.3.**Stop processes before cleaning files**, otherwise they can re-infect the files you just fixed.4.**Assume secrets on the machine were exposed** if any infected file was ever executed (§7) — tell the user; don't act on credentials yourself.5. Don't print `.env` values, tokens or payload bodies into chats, tickets or public posts.6. Shell caveat (macOS zsh): an unmatched glob like `--include=*.js` aborts the command with `no matches found`. Quote globs or use `find`/`grep -r --include='*.js'`.
---
## 1. What you are looking for
### 1.1 Known indicators (this campaign)
| Indicator | Where it appears ||-----------|------------------|| String `5-2-326-du` (campaign/build tag; also seen as `global.i='5-2-326-du'` or `global.o='5-2-326-du'`) | Payload, and the command line of the running process || `global.r=require` / `global[...]= require` / `global.m=module` | Payload header || `global.e='NPM'` | Payload variant || `For only test` comment | Payload variant || `var _$_xxxx=(function(n,c){...` (identifiers like `_$_1aa7`, `_$_40e4`) | Payload (string-array decoder) || `String.fromCharCode(127)` + `.split('%').join(` | Payload decoder || `createRequire(import.meta.url)` lines **added at the top of an ES-module file that never needed them** (`const require = createRequire(...)`) | Injector adds them so `require` works in `.mjs` || Outbound HTTPS from a `node` process to `198.105.127[.]210` | Network || Process: `node -e global.i='...';global.r=require;...` with **parent PID 1** | Process list |
> The attacker can change these strings between variants. **Don't rely on the markers alone** — also run the heuristic checks in §1.2.
### 1.2 Heuristic indicators (survive variants)
- A config / script file with **one very long line (> 1,000 characters)**, especially a file that is normally < 30 lines.- A line that contains **a run of 80+ spaces or tabs followed by non-whitespace code** (the payload is hidden off-screen to the right).- A file whose last line is `main();` / `export default config;` / `module.exports = ...;` followed by lots of trailing spaces and then more code on the **same line**.-`eval(`, `new Function(`, `Function("return this")()`, `global[...] = require`, `child_process` + `detached: true` + `unref()` in a file that has no reason for them (CSS/PostCSS/Babel/Next/Tailwind/ESLint/Metro/Jest config).- Large hex/base64/obfuscated string tables (`\x25`, `\x23\x31`, `String.fromCharCode`) in config files.- Files modified recently that you didn't edit (`git status` shows `M` on config files after a clean checkout).
### 1.3 Files this campaign targets (check these first, in every project root and `scripts/`)
```postcss.config.js|mjs|cjs tailwind.config.* next.config.*babel.config.* .babelrc.js metro.config.js react-native.config.jsvite.config.* webpack.config.* rollup.config.* eslint.config.* .eslintrc.jsjest.config.* vitest.config.* svelte.config.* nuxt.config.* astro.config.*gulpfile.js Gruntfile.js scripts/*.mjs|js|ts any file named in package.json "scripts"```Configs are favored because they run automatically on `dev` / `build` / `start`.
---
## 2. Step 1 — Stop active infection (do this first; agent may kill only marker-matching PIDs, §0.1)
```bash# Look for payload processes (macOS/Linux). The marker is the campaign tag; the generic patterns catch variants.ps -axopid,ppid,lstart,command | grep -E"node .*-e .*(global\.|_\\\$_|require=|5-2-326)" | grep -vgrep
# Any node process whose parent is launchd/init (PPID 1) is suspicious — normal dev servers have a shell/IDE parent.ps -axopid,ppid,lstart,command | awk '$2==1' | grep -inode | grep -vgrep```
For each suspicious PID, record evidence **before** killing:
```bashps -opid,ppid,lstart,command-p <PID> | cut -c1-200lsof -p <PID> | grep -E"cwd|TCP|UDP"# working dir = which project launched it; TCP = who it talks to```
Then stop **only** processes that matched a marker, and re-check:
```bashkill <PID> # use kill -9 only if it survivessleep 1; ps -axopid,command | grep -E"global\.|5-2-326" | grep -vgrep || echo "none left"```
If a process refuses to die, or it doesn't match a marker but looks suspicious: **do not escalate** (`kill -9`, `sudo`) — list it in the user instructions. The user may turn Wi-Fi off while cleaning.
---
## 3. Step 2 — Scan the project (read-only)
### 3.1 Quick marker search (excludes `node_modules` and `.git`; also scans them in 3.3)
```bashcd /path/to/projectgrep -rIlF"5-2-326-du".--exclude-dir=node_modules--exclude-dir=.gitgrep -rIlE'global\[_\$_|_\$_[0-9a-f]{4}=\(function|global\.r=require|For only test'.--exclude-dir=node_modules--exclude-dir=.git```
### 3.2 Heuristic scan script (catches variants)
Save as `scan-malware.sh`, `chmod +x`, run from the project root. It is read-only.
```bash#!/usr/bin/env bash# Read-only scanner for the hidden-payload-in-config-file malware pattern.# Usage: ./scan-malware.sh [dir] (default: .)set -uROOT="${1:-.}"hits=0note() { printf '%s\n'"$*"; hits=$((hits+1)); }
# Build output / generated dirs are skipped (compiled code has long lines by design); scan SOURCE and config.EXCL=(-not-path'*/node_modules/*'-not-path'*/.git/*'-not-path'*/.next/*'-not-path'*/dist/*'-not-path'*/build/*'-not-path'*/out/*'-not-path'*/.yarn/*'-not-path'*/coverage/*'-not-path'*/.vercel/*'-not-path'*/.output/*'-not-path'*/.turbo/*'-not-path'*/functions/lib/*'-not-path'*/.expo/*'-not-path'*/android/app/build/*'-not-path'*/ios/Pods/*')XDIRS=(--exclude-dir=node_modules--exclude-dir=.git--exclude-dir=.next--exclude-dir=dist--exclude-dir=build--exclude-dir=out--exclude-dir=coverage--exclude-dir=.vercel--exclude-dir=.output--exclude-dir=.turbo--exclude-dir=.expo--exclude-dir=Pods)CODE=(--include='*.js'--include='*.mjs'--include='*.cjs'--include='*.ts'--include='*.mts'--include='*.cts'--include='*.jsx'--include='*.tsx'--include='*.json'--include='*.sh')
echo "== 1. Known markers"while IFS= read -rf; do note "MARKER $f"; done < <( grep -rIlE -e '5-2-326-du' -e 'global\[_\$_' -e '_\$_[0-9a-f]{4}=\(function' -e 'For only test' -e "global\.[a-z]='[0-9]+-[0-9]+-[0-9]+" \ "${CODE[@]}" "${XDIRS[@]}" "$ROOT" 2>/dev/null)
echo "== 2. Very long lines (>1000 chars) in source/config files"while IFS= read -r f; doif awk 'length($0) > 1000 { found=1; exit } END { exit !found }' "$f" 2>/dev/null; then note "LONGLINE $f (longest: $(awk '{ if (length($0)>m) m=length($0) } END{print m}' "$f") chars, $(wc -l <"$f") lines)"fidone< <(find "$ROOT" -type f \( -name '*.js' -o -name '*.mjs' -o -name '*.cjs' -o -name '*.ts' -o -name '*.mts' -o -name '*.cts' -o -name '*.jsx' -o -name '*.tsx' \) "${EXCL[@]}" -size -2000k 2>/dev/null \ | grep -v -E '\.min\.|\.bundle\.|\.d\.ts$|/public/|/vendor/')
echo "== 3. Lines with 80+ whitespace followed by code (hidden-to-the-right payload)"while IFS= read -r f; do note "HIDDEN $f"; done< <( grep -rIlE -e '[;})][[:space:]]{80,}[^[:space:]]' "${CODE[@]}" "${XDIRS[@]}" "$ROOT" 2>/dev/null)
echo "== 4. Suspicious primitives in config/script files"while IFS= read -r f; doif grep -qE 'eval\(|new Function\(|Function\(["'\'']return this|detached: *true|\.unref\(\)|String\.fromCharCode\(127\)|\\x25.*\\x23\\x31' "$f" 2>/dev/null; then note "PRIMITIVE $f"fidone< <(find "$ROOT" -maxdepth 3 -type f \( -name '*.config.*' -o -name '.babelrc*' -o -name '.eslintrc*' -o -name 'gulpfile*' -o -name 'Gruntfile*' -o -path '*/scripts/*' \) "${EXCL[@]}" 2>/dev/null)
echo "== 5. Lifecycle scripts in package.json files (preinstall/postinstall/prepare/install)"while IFS= read -r f; doif grep -qE '"(preinstall|install|postinstall|prepare|prepublish|preprepare|postprepare)"[[:space:]]*:' "$f" 2>/dev/null; then echo " $f:"; grep -nE '"(preinstall|install|postinstall|prepare|prepublish)"[[:space:]]*:' "$f" | sed 's/^/ /'fidone< <(find "$ROOT" -maxdepth 4 -name package.json -not -path '*/node_modules/*' 2>/dev/null)
echo "== 6. Git: files modified but not committed (config files you did not touch?)"if git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then git -C "$ROOT" status --short | head -50; else echo " (not a git repo)"; fi
echoif [ "$hits" -eq0 ]; then echo "RESULT: no indicators found (still review section 5 output above)."; else echo "RESULT: $hits suspicious finding(s). Do NOT run build/dev/install until reviewed."; exit 2; fi```
This scanner skips build output (`out/`, `dist/`, `.next/`, `functions/lib/`…). If a project *commits* build output, also scan it manually — the payload can live there too. Don't scan this playbook/report files themselves (they quote the markers).
Interpretation: **MARKER** = confirmed. **HIDDEN** = almost certainly confirmed. **LONGLINE / PRIMITIVE** = inspect by hand (minified vendored files are the usual false positives). Section 5 output is informational: a `postinstall` you don't recognise deserves a look.
### 3.3 Inspect a flagged file safely (never run it)
```bashwc -lcFILE# a 9-line config that is 8 KB+ is a red flagawk '{ print NR": "length($0)" chars" }'FILE | sort -t:-k2-n-r | head -3# find the long linehead -c600FILE# beginning (normal code)awk 'length($0) > 1000 { print NR": "substr($0, 1, 120) " ... [" length($0) " chars]" }'FILE# show only the START of long linescat -AFILE | cut -c1-160 | head # makes trailing spaces visible as nothing / ^M for CRLF```
### 3.4 Scan dependencies and tooling (the likely entry points)
```bash# Installed packagesgrep -rIlF"5-2-326-du"node_modules 2>/dev/null | head # may take a while on big trees# Lockfile: packages you don't recognise, or recently added onesgit log-p--follow--package.jsonyarn.lockpackage-lock.jsonpnpm-lock.yaml | grep -E'^[+-].*"?(version|resolved)' | head -80npm audit# or: yarn audit / pnpm audit# Global toolinggrep -rIlF"5-2-326-du"~/.nvm~/.npm~/.config~/.cursor/extensions~/.vscode/extensions 2>/dev/null | head```
---
## 4. Step 3 — Establish when and how it got in (git forensics)
### 4.1 Which commit introduced it
For each infected file, find the first commit containing a marker (adjust the marker if you found a variant):
```bashFILE=postcss.config.mjsfor c in $(git log--format=%h--all--"$FILE"); do n=$(git show"$c:$FILE" 2>/dev/null | grep -cE'5-2-326-du|global\[_\$_|_\$_[0-9a-f]{4}=\(function') echo "$c $(git log -1 --format='%ad | %an <%ae>|%s' --date=iso "$c") | payload_lines=$n"done```
The oldest commit with `payload_lines>0` is the entry commit; the one before it is the last clean version.
Other useful checks:
```bashgit show <entry-commit> --stat# what else changed (injected code often rides inside a real commit)git log--all--format='%h author=%an <%ae> committer=%cn <%ce>%ad' --date=iso -- <file>git branch -a; git stash list # payload on other branches/stashes?git rev-parse HEAD origin/main # already pushed?git branch -r --contains <entry-commit> # which remote branches contain itgit grep -lF "5-2-326-du" $(git rev-list --all) | sed 's/^[0-9a-f]*://' | sort | uniq -c # every file in all history```
Note: commit author/committer are **self-reported** and prove nothing about who actually injected the code. The usual story: malware on the dev machine modified files, and the developer committed them with `git add -A`.
### 4.2 Timeline questions to answer
- When did the first infected commit land? What was installed/cloned/opened in the days before (new npm packages, editor extensions, a "test project" from a recruiter/client, a downloaded template)?- Which projects were last built/run **after** the infection date? Those machines/CI runners executed the payload.- Did CI or a deploy pipeline run an infected file? If yes, treat CI secrets as exposed too.
---
## 5. Step 4 — Delete the malware (agent: the malware only, §0.3; everything else is user instructions)
### 5.1 Restore from the last clean commit (preferred)
Per §0.3, for each confirmed-infected file, **one file at a time**:
```bashgit show <last-clean-commit>:path/to/file > path/to/file# repeat per infected filegit diff <last-clean-commit> HEAD--path/to/file | cut -c1-120 | grep -E'^[-+]' | grep -v'^+++\|^---'```
Check the second command: if the entry commit also made **legitimate** edits to that file, re-apply them by hand (don't restore blindly). Injected lines to remove typically include `import { createRequire } from 'module'; const require = createRequire(import.meta.url);` if the file didn't have them before.
### 5.2 If there is no clean commit (or not a git repo)
Open the file, keep everything up to the real end of the code, and delete the rest of the long line. Re-check with `wc -c` (size should drop dramatically) and the scanner.
### 5.3 Re-verify
```bash./scan-malware.sh # expect: no indicatorsgit diff--stat# only the files you meant to changeps -axopid,command | grep -E"global\.|5-2-326" | grep -vgrep || echo "no payload processes"```
### 5.4 Reinstall dependencies cleanly (USER ACTION — the agent must not run or delete anything here; put it in the instructions)
```bashrm -rfnode_modules.nextdistbuild# the USER runs this, not the agent# keep the lockfile; review its diff first (§3.4)npm ci--ignore-scripts# or: yarn install --frozen-lockfile --ignore-scripts / pnpm i --frozen-lockfile --ignore-scripts# then rebuild only what's needed, after reading package scripts```
### 5.5 Commit the cleanup (USER ACTION — the agent must not commit or push)
```bashgit add-p# review hunks; do NOT use git add -Agit commit-m"Remove injected malware payload from <files>"git push# only after secrets are rotated and the user is happy```
The payload **remains in git history** after this. To purge it you'd need a history rewrite (`git filter-repo`) plus force-push and everyone re-cloning — usually not worth it; instead rotate secrets and make sure no one runs old commits. Decide with the owner.
---
## 6. Step 5 — Persistence & machine checks (READ-ONLY for the agent; report findings, change nothing)
```bash# Launch items — look for unfamiliar names, or ones that run node/sh/curlls -la~/Library/LaunchAgents/Library/LaunchAgents/Library/LaunchDaemons 2>/dev/nullgrep -lE"node|curl|bash -c|osascript|base64"~/Library/LaunchAgents/*.plist/Library/LaunchAgents/*.plist/Library/LaunchDaemons/*.plist 2>/dev/nullcrontab -l# Shell startup filesgrep -nE"node -e|curl .*\|.*(ba)?sh|base64|eval|5-2-326"~/.zshrc~/.zprofile~/.zshenv~/.bash_profile~/.bashrc~/.profile 2>/dev/null# Login items / remote access tools (confirm each is yours): System Settings → General → Login Items; AnyDesk/TeamViewer/etc.# Network: any node process talking outlsof -nP-iTCP-sTCP:ESTABLISHED,SYN_SENT | grep -inode# Browser extensions & recently installed apps/extensions: Cursor/VS Code extensions list, Chrome extensionsls -lt~/.cursor/extensions~/.vscode/extensions 2>/dev/null | head -20```
In this incident no persistence was found: the payload only came back when an infected file was executed.
### Scan every project on the machine
```bash# Markersgrep -rIlF"5-2-326-du"~/Desktop~/Documents~/Projects~/code~/dev 2>/dev/null--exclude-dir=node_modules--exclude-dir=.git# Heuristic (long lines in config files)find ~/Desktop~/Documents-typef \( -name'*.config.*'-o-name'.babelrc*' \) -not-path'*/node_modules/*'-not-path'*/.git/*'-execawk'length($0)>1000 {print FILENAME": line "NR" ("length($0)" chars)"; exit}'{} \; 2>/dev/null```
Run `scan-malware.sh` in each project that turns up.
---
## 7. Step 6 — Treat credentials as compromised (USER ACTION — the agent only lists what to rotate)
If any infected file was executed (build, dev server, deploy script, tests), assume the machine's secrets were readable. Rotate **after** the machine is clean (otherwise the new secrets leak too).
| Area | Action ||------|--------|| Git hosting | Revoke GitHub/GitLab personal access tokens, SSH keys, deploy keys, OAuth app grants; check recent activity/audit log, new collaborators, Actions secrets, unfamiliar commits/branches || Cloud / Firebase / Vercel / AWS / GCP | `firebase logout` + re-login; rotate service-account keys; revoke CLI tokens (`firebase login:list`); check IAM for new members/keys || API keys in `.env*`, `functions/.env`, CI secrets | Regenerate every key (OpenAI, Gemini, OpenRouter, Meta/Threads/LinkedIn/X/Telegram, Stripe, SendGrid, DB URLs…) || Package registries | Revoke npm tokens; check for unexpected published versions || Browser | Log out of all sessions on sensitive sites; change passwords stored in the browser; revoke active sessions (Google, GitHub, banking) || Password manager | Rotate anything accessed on this machine; check for vault access from new devices || Crypto wallets | Move funds to a fresh wallet created on a clean device || SSH | Generate new keys; remove old public keys from servers and GitHub || Databases | Rotate passwords; review for unusual access / new users |
Then review: new GitHub tokens/deploy keys created recently, unexpected Firebase/Cloud users, unfamiliar sign-ins on Google/GitHub, billing spikes on API providers.
---
## 8. Step 7 — Prevent recurrence (suggestions for the user; the agent installs/changes nothing unless told to)
1.**Find and remove the entry point**: the package, extension, repo or download that was opened just before the first infected commit. Warn teammates who may have it.2.**Untrusted code → isolated environment** (VM, container, Codespaces, throwaway laptop). Never on the main machine with logged-in accounts. Be especially wary of recruiter/client "coding tests".3. Use `--ignore-scripts` when installing unfamiliar projects: `npm i --ignore-scripts`, `yarn --ignore-scripts`, `pnpm i --ignore-scripts`. Set `ignore-scripts=true` in `.npmrc` for sensitive machines.4.**Editor setup**: enable word wrap and render whitespace so hidden right-hand code is visible (VS Code/Cursor: `"editor.wordWrap": "on"`, `"editor.renderWhitespace": "boundary"`; keep minimap on).5.**Pre-commit / CI guard** (blocks the pattern):
```bash# .git/hooks/pre-commit (chmod +x) — or run the same logic in CI#!/usr/bin/env bashbad=0for f in $(git diff--cached--name-only--diff-filter=ACM | grep -E'\.(js|mjs|cjs|ts|mts|cts|jsx|tsx)$'); doif git show":$f" | awk 'length($0)>1000{f=1} END{exit !f}'; then echo "✖ $f has a line >1000 chars"; bad=1; fiif git show":$f" | grep -qE'5-2-326-du|global\[_\$_|_\$_[0-9a-f]{4}=\(function|[;})][[:space:]]{80,}[^[:space:]]'; then echo "✖ $f matches malware pattern"; bad=1; fidone[ $bad -eq 1 ] && { echo "Commit blocked. Inspect the file(s) above."; exit 1; }exit 0 ```6.**Never `git add -A` blindly.** Use `git add -p` and read the diff, including config files. Add `git diff --stat` to your commit routine.7. Protect `main`: required PR reviews, required status checks (CI runs the scanner), signed commits if possible.8. Dependencies: commit lockfiles; review lockfile diffs; run `npm audit`; use Socket/Snyk/Dependabot; watch for typosquats; pin and review new packages and their install scripts before adding.9. Editor extensions only from verified publishers; review ones with broad permissions; remove unused ones.10. Secrets hygiene: short-lived tokens, secret managers instead of long-lived `.env` files on dev laptops, separate browser profile for admin/cloud consoles, hardware-key/passkey 2FA.11. Endpoint protection + OS updates; consider an outbound firewall (Little Snitch / LuLu) so unknown `node` network calls are visible.
---
## 9. Decision guide
| Situation | Conclusion | Action ||-----------|------------|--------|| Markers/hidden-line found, **file never executed** (e.g. just cloned) | Infected repo, machine not exposed | Don't run it; delete the clone or clean per §5; report the source || Markers found **and** an infected file was run (dev/build/deploy/tests) | Machine exposed | §2 → §5 → §6 → §7; rotate all secrets || Process with `global.i=…`/PPID 1 found | Payload ran | Same as above; also check which project's `cwd` it ran from || Only LONGLINE hits in minified/vendored files | Probably false positive | Verify the file is third-party and unchanged vs. upstream || Payload found in git history but not in current files | Already removed | Confirm HEAD clean; still rotate if it ever ran || Infected file is on `origin/main` | Others may have it | Tell collaborators; they must scan & rotate too |
---
## 10. Report template (fill in per project)
```Project: Date audited:Auditor:Infected files: (path, size before/after)Entry commit: (hash, date, subject) — pushed to remote? Y/NLast clean commit:Processes found/killed: (PID, start time, cwd, remote IP:port)Persistence found: (none / details)Other projects infected: (list)Entry point (suspected): (package / extension / repo / download)Files restored/changed: (list; legit edits re-applied? Y/N)Verification: scanner clean Y/N · no payload processes Y/N · no outbound connections Y/NSecrets rotated: (list + date)Open items:```
---
## 11. Notes on interpretation
- The payload was **not** statically analysed in the original incident, so don't claim exactly what it stole. It matches the profile of a developer-targeted infostealer / remote-access tool seen in fake-recruiter "coding test" campaigns; treat all local credentials as exposed.- Defang IPs/URLs (`198.105.127[.]210`) when sharing publicly.- If a variant appears with different markers, rely on the heuristics (§1.2, scanner sections 2–4), then add the new markers to your copy of this playbook.
