<ProdhoshBlogs/>
← Back to all posts

Malware Hid in My Old Repos and Git Never Showed It

Malware hid in my old repos. And git never showed it.

I have 53 repos on GitHub. A few weeks ago, Vercel started emailing me about failed deployments. Not for something I had just pushed. For old projects, ones I had not touched in months. I counted 10 or 11 of them.

I was confused. I had not committed anything. I had not even opened those repos. The email said the production deployment had failed on main, with a build error: Command "npm run build" exited with 1. That told me nothing useful. So I did what I always do with my Next.js and TypeScript projects when a build breaks: I went straight for the config files.

The line that looked normal

In postcss.config.mjs I noticed something off. The last line, export default config;, kept going. When I scrolled right, past what felt like hundreds of spaces, there was a wall of obfuscated JavaScript sitting on that same line. It looked like blockchain related code. Nobody would ever see it in a normal editor view unless they scrolled sideways.

Here is the part that really got me. I opened the git history, expecting to find some stranger's name or a commit I did not make. There was nothing. No new author. No new date. As far as git was telling me, I wrote that file and I never touched it since.

Then I started finding it in other projects too. Same trick, different repos. That is when I stopped thinking "my bad" and started thinking "this is something bigger".

Turns out I was not the only one

I searched around and found a GitHub Community thread (#188732) full of developers describing the exact same thing: obfuscated code hiding in next.config.js and postcss.config.js in repos they had not touched.

Screenshot from the GitHub community thread where developers compared notes

Researchers at OpenSourceMalware already had a name for it: PolinRider. They link it to North Korea, to the same cluster behind the "Contagious Interview" campaign, where fake recruiters send developers a coding test that is really malware. Other security firms back up that attribution.

It is not small either. The first report in March 2026 counted 675 compromised repos across 352 owners. By April it was 1,951 repos and 1,047 owners. By July, researchers were counting more than 4,000 repos and 2,000 owners. Those are just the ones they could find.

How it works (and why git did not show it)

This is the part worth understanding, because it explains why none of us noticed.

It hides in whitespace. The payload sits after roughly 280 spaces on the export default config; line. A normal postcss config is only a couple of hundred bytes. Infected ones were reported at 5,000 bytes or more.

It rewrites history. The malware amends your last commit and force pushes it. It rewinds the system clock first, so the amended commit keeps your original author and date. A plain git log looks perfectly normal. The tells are small: GitHub says "updated x hours ago" on a repo you have not touched, a pull request timeline can show a "force pushed" line (that one is generated by GitHub, so it is harder to fake), and if you sign your commits you may suddenly see Unverified.

The real payload is not even in your repo. It is fetched from blockchain transactions (TRON and Aptos first, Ethereum in a newer variant) and decrypted in memory. There is no server to take down, and to an antivirus it can look like plain text in a config file.

And it keeps changing. The markers researchers first searched for have already been swapped out. A clean search does not prove you are clean.

Once it is on your machine, it goes after browser passwords, crypto wallets, SSH keys, cloud keys, .env files, API keys and git credentials. Then it uses your own access to spread to your other repos. That is how my old projects got hit without me doing anything to them.

How PolinRider spreads from one bad clone to every repo you can push to

How people get infected

Honestly, I still do not know how it reached me. I did not knowingly run a coding test or install a strange package, and that is the unsettling part. If you cannot trace it either, that is a good reason to assume the worst and check everything.

The research points to three common doors:

  1. A fake job interview. Someone on LinkedIn, Upwork or Freelancer sends you a coding test repo. You do not even have to run it. Opening the folder in VS Code can be enough, because a .vscode/tasks.json can run a command the moment a folder opens.
  2. A fake package. Typosquats that look like dev tools. One example from the research is the npm package tailwindcss-style-animate, which loads the payload during your build.
  3. Someone else's infected repo. A fork, a PR or a package from another victim.

It does not stay in one file

If you only clean postcss.config.mjs, you will probably get reinfected. Reports list a lot more places to look:

  • Other config files, like tailwind.config.js, eslint.config.mjs, next.config.mjs, vite.config.js, vue.config.js, astro.config.mjs and webpack.config.js
  • A fake font file, usually fa-solid-400.woff2, that is really a loader
  • .vscode/tasks.json files set to run on folder open
  • npm itself. Several people found npm/lib/cli.js replaced. It is normally a few hundred bytes. Infected copies were reported between 70 KB and about 1 MB. That means every npm, npx or npm exec can start the malware again, even after your projects are clean.

One more thing I noticed myself. When I started working on one of the old projects locally, my local env file was no longer listed in .gitignore. I have not seen this described in anything I read, so I cannot say it is part of PolinRider. But that kind of change is exactly how secrets leak, so if you see it, treat it as a red flag and check your git diff.

Check yourself right now

This is what I ran, and what I would run on every folder of projects you own. These are rough checks. A hit does not prove infection and a miss does not prove you are safe. Run them from the folder that holds your projects (macOS, Linux, WSL or Git Bash).

1. Config files with a long run of spaces followed by code:

grep -rEn --include='*.config.*' --exclude-dir=node_modules --exclude-dir=.git ' {80,}[^ ]' . | cut -c1-120

2. Config files that are suspiciously big (normal ones are usually well under 2 KB):

find . -path ./node_modules -prune -o -type f \( -name '*.config.js' -o -name '*.config.mjs' -o -name '*.config.ts' \) -size +2000c -print

3. VS Code tasks that run the moment a folder opens:

grep -rln --include=tasks.json '"runOn": *"folderOpen"' --exclude-dir=node_modules .

4. npm's own cli.js (reported as a few hundred bytes when clean):

wc -c "$(npm root -g)/npm/lib/cli.js"

5. Markers from earlier variants:

grep -rEn --exclude-dir=node_modules --exclude-dir=.git 'global\[.(_V|!).\]|_\$_1e42' .

OpenSourceMalware also publishes a scanner, polinrider-scanner.sh, in their PolinRider repo. Their own note says it was built against the March signature, so treat it as a first pass.

Also do the manual checks: look at the "updated x hours ago" on repos you have not touched, open a PR timeline and look for "force pushed", and check whether any of your signed commits suddenly say Unverified.

If you find it, do it in this order

The order matters. If you rotate tokens before killing the running process, it just collects the new ones.

  1. Stop opening the project in VS Code until you turn off automatic tasks. In settings, search "task" and set Task: Allow Automatic Tasks In Folder to off or prompt, globally.
  2. Freeze your repos. Turn on branch protection that blocks force pushes and requires signed commits. Pause auto deploys on Vercel, Netlify or whatever you use.
  3. Look for live processes. On macOS or Linux, ps aux | grep -E "(python|node).*-(c|e)" | grep -v grep is a good start. On Windows, list your node.exe processes with their command lines and look for node -e with obfuscated content.
  4. Clean the files in GitHub's web editor, not from a local pull. If you re-clone an infected branch, you can start the cycle again. Compare npm's cli.js against a clean copy, or reinstall Node and npm from official sources.
  5. Rotate everything, from a clean device. GitHub tokens, SSH keys, OAuth apps, deploy keys, Vercel, Netlify, Railway and Render tokens, every secret in every .env, cloud keys, your npm token, and any passwords saved in your browser if they were exposed. Do not forget CI and hosting tokens. Some victims saw no strange login on GitHub at all, because the stolen token belonged to a deploy platform.
  6. Tell your collaborators. If one of them is still infected, they can reinfect you.

The full playbook is in OpenSourceMalware's developer guide. Their indicator list lives in the PolinRider repo and keeps changing, so use that instead of anything copied into a blog post (including this one).

Habits I picked up after this

  • Open unfamiliar repos in VS Code Restricted Mode until I have looked through them
  • Block force pushes and require signed commits on repos that matter
  • Use SSH keys instead of cached HTTPS credentials for git
  • Read the PR timeline for force push lines
  • Keep tokens short lived, and revoke old OAuth apps and deploy integrations
  • Remember that npm ci --ignore-scripts does not help when npm run dev loads a poisoned config

If you have old repos you have not opened in months, check those first. That is where mine were.

I am building a CLI for this

After doing all of this by hand across 53 repos, one thing was obvious: nobody is going to scroll right on every project they own.

So I am building a small CLI you run before you open or run a project. It will flag config files with hidden whitespace, oversized configs, auto running VS Code tasks, a bloated npm cli.js and known markers, then print a short report. The goal is a quick yes or no on a repo you just cloned, especially one from a stranger.

It is coming soon. If you want to know when it drops, follow me on GitHub at github.com/prodhosh. A star or a follow helps a lot.

The same thing is happening to AI skills

This is not only about npm anymore. AI agents now install and run "skills", which are folders with a SKILL.md file and sometimes scripts, and they run with your permissions. It is the same trust problem as npm, with fewer people checking.

The numbers are not small. A 2026 study of 42,447 agent skills, cited in NVIDIA's SkillSpector README, found that 26.1% had at least one vulnerability and 5.2% showed likely malicious intent. In February 2026, a campaign called ClawHavoc hit OpenClaw's ClawHub marketplace. Koi Security found 341 malicious skills out of 2,857, and later counts went past 1,100. A skill does not even need code to be dangerous. Plain English instructions can be the attack. Wiz also reported that the ChainDrop npm worm tried to persist through Claude Code hooks and VS Code tasks.json.

Luckily, NVIDIA released an open source tool for this called SkillSpector. You point it at a skill before you install it:

uv tool install git+https://github.com/NVIDIA/skillspector.git

skillspector scan ./some-skill/ --no-llm
skillspector scan https://github.com/user/some-skill

It scans a folder, zip, file, URL or Git repo against 69 patterns in 17 categories (prompt injection, data exfiltration, curl | bash, obfuscated code, credential access), gives a risk score from 0 to 100, and exits with code 1 when the score is above 50, so you can use it as a gate in CI. It never runs the skill.

One rule caught my eye: it flags large whitespace padding used to hide instructions. That is the same trick PolinRider uses in config files.

Two honest limits. It is not a sandbox, so it will not save you if you install something risky anyway. And its optional LLM pass sends file contents to whichever provider you configured, so use --no-llm for private skills. My habit now: read the SKILL.md, look inside any scripts folder, then scan it. Same as I would do for a package I had never heard of.

And all of this happened in the same 48 hours

I was writing this during a week that made the point better than I could. Security is not a side topic anymore.

Four security stories from Oct 21 to 22, 2026

PHP. Two flaws called PHP2Shell (CVE-2026-109618 and CVE-2026-109619), both rated 9.2. An unauthenticated attacker can run code on a web server. They affect PHP 5.4 through 8.5.11. Fixes landed on Oct 21 in 8.5.12, 8.4.27, 8.3.36 and 8.2.35, and older versions may not get one. Amazon saw an exploit attempt on Oct 17, days before the public disclosure. W3Techs says PHP runs on about 70% of the websites whose server side language is known, so this one is big. Researchers also said AI models helped turn these old bugs into working exploits much faster than before.

Node.js. On Oct 21, ten CVEs were fixed, four rated high. The worst is a bug in the bundled Brotli decoder that allows remote code execution. Update to 22.23.4, 24.22.1 or 26.12.1. Node 20 gets no fix.

GitLab. CVE-2026-110264, CVSS 10.0, unauthenticated code execution on self managed servers. The bug goes back to GitLab 7.0. If you patched on Oct 12, that patch was not enough and you need the new release. GitLab.com is already patched.

SBI. The bank confirmed 3,897 customer records with personal data inside a dark web dump of about 3.75 lakh rows. The alert came on Oct 16, the disclosure on Oct 21, and the source is still unknown.

The AI side is a month old, but it fits. In late September, OpenAI held back a model called GPT-6.1 Astra. Its head of safety systems said it did not meet the bar on staying within the scope it was authorized for and on honestly reporting what it did. NPR also reported that OpenAI paused training of its most advanced models until it had more safeguards.

An agent that does more than you authorized is the same problem as a skill that does more than it says, or a config file that does more than it shows. We are handing software a lot of access right now. It is worth checking what we hand over.

Do this today

Open your projects folder and run the five checks above. Look at your old repos first. Then turn off automatic tasks in VS Code, and block force pushes on anything that matters. It takes ten minutes, and I wish I had done it before the Vercel emails.

If you find something, I would like to hear about it. And if you want the CLI when it is ready, follow me at github.com/prodhosh.

Sources

PolinRider

AI skills

The same 48 hours