DEV Community

Cover image for A fake client sent me a GitHub repo. Running it cost me two days and every password I had.
Vinicius Pereira
Vinicius Pereira

Posted on

A fake client sent me a GitHub repo. Running it cost me two days and every password I had.

This week a client on a freelancer platform sent me a repo and asked me to run it. I ran it. Two days later I had rotated every credential I own and wiped my machine.

I want to walk through exactly how it works, because it is not one clever trick, it is a whole family of them, and they are all aimed at us specifically. The people building these know that a developer will clone a repo and type npm run dev without thinking, the same way you would open a door for someone holding coffee. That reflex is the exploit.

How the main one works

The setup looks like a real job. A "client" posts something senior and well paid: a senior AI engineer role, a big fixed budget. They message you fast and warm. Then they share a GitHub repo. In my case it was an AI companion game, and it was genuinely well architected, clean separation, sensible structure, the kind of code that lowers your guard on purpose. They ask you to run it locally and send a screenshot proving it works, before any contract exists.

The malware was in postcss.config.js. Not in an obvious place, in a config file nobody reads, sitting on the same line as the real config behind a long run of spaces so it scrolls off the screen. It was obfuscated with fromCharCode so even if you looked you would see noise, not a URL. It fires the moment you run the dev server.

When it fires, it beacons out to a command-and-control server, pulls down a second stage, and goes straight for the browser. On a Mac it tries to read the "Chrome Safe Storage" key out of the keychain, which is the key that decrypts every password and cookie Chrome has saved. One keychain prompt, and if you approve it, the whole vault leaves the machine. To make the C2 hard to block, the address was not hardcoded, it was resolved through a dead-drop on the Ethereum blockchain, so a takedown or a blocklist does nothing.

This family has names if you want to read more: Contagious Interview, BeaverTail, InvisibleFerret. It is run at scale and it targets developers through job offers, on purpose, because our machines hold SSH keys, cloud credentials, and source access.

The part that actually scared me came that night. I found that it had not just run and exited. It had rewritten main.js inside VS Code, cli.js inside npm, and index.js inside Discord, three Electron apps whose code is plain editable JavaScript on disk. The npm one went from 407 bytes to 1.7 MB. Every time I opened any of those apps, or ran any npm command, it respawned the password stealer and a clipboard monitor. A reboot does nothing against that. "No LaunchAgent, no cron" is not the same as "no persistence."

The other flavors

The repo trick is the loud one. There are quieter versions arriving the same week, and I have watched every one of these land in my own feed in the last few days.

The malicious attachment. The job post has a .zip or .rar attached, labeled something like "requirements" or "project brief." Inside, a couple of harmless PNGs as decoy and one .vbs or .bat file. That script downloads the real payload from a hacked WordPress site and runs it. On a Mac a .vbs will not execute, but the file still travels, and if you also use Windows it will fire there.

The compromised real account. You check the client and it looks fine. Payment verified, phone verified, a five-star review, spending history, an account from years ago. Then it sends you a repo or an attachment with malware in it. Old, legitimate accounts get taken over and used as bait precisely because they pass the naive filter. Judge the message and the attachment, never the profile badge.

The off-platform pull. A warm reply to your proposal asking for your email so they can send the "official interview invitation," and often a tip to write your email with spaces so the platform's filter does not catch it. No real client ever coaches you to defeat the contact filter. That instruction alone is the whole answer.

The pay-to-test scam. A tester or QA role that, once you accept, needs you to fund a crypto wallet or a subscription "for payment testing, reimbursed separately." Crypto is irreversible and the reimbursement never comes. Real payment testing uses sandbox keys, never your own money.

Why the usual signals fail you here

The instinct is to trust the platform's checkmarks. Payment verified, top rated, high spend. Those tell you the account can pay and has a history. They tell you nothing about whether the file in front of you is safe, and they are exactly what a hijacked account still shows. The verification protects you against not getting paid. It does not protect you against running code.

The payment model itself kills the excuse. These platforms already hold the client's money and release it only when they approve your work, so a nervous client never needs to watch your machine run their project first. When someone insists you run their code and screenshot it before there is any contract at all, that is not caution and it is not a technical test. There is no legitimate reason for it. It is the attack.

The rules I follow now, no exceptions

  1. Never run a stranger's code. Not a repo, not a script, not a notebook, not a "quick test task." Reading it is fine. Running it is the line, and it does not move for a good story or a big budget.

  2. Read third-party code by view only. Read it raw over HTTP, no clone, no npm install, no pip install, no docker compose. And read the boring files first, because that is where it hides: config files like postcss, webpack, vite, the scripts block in package.json (watch postinstall and prepare), the Dockerfile, git hooks. Malware lives in the file you skip.

  3. Treat attachments as text. Never double-click. A .docx or .xlsx is a zip, so unzip it and read the XML. A .zip or .rar, list the contents before extracting, and any .vbs, .bat, .js, .lnk, .scr, .hta, or executable inside means you stop there. Never go fetch the second stage to "see what it is." That just hands the attacker your IP and runs live malware.

  4. A funded contract does not make their code safe to run. This is the bypass a scammer will reach for next, so be clear about it: a paid contract changes who is liable for the work, it does not change what the code does to your machine. If you ever genuinely have to run something unknown, it goes in a throwaway VM with no network and no access to your real files, and even a clean run there proves nothing, because some of this only fires on the right OS and some detects a VM. Reading the code is the only real check. Everything else is a second layer.

  5. Stop saving passwords in the browser. That single keychain prompt is what turns one mistake into fourteen stolen logins. Use a password manager with its own master password, turn on 2FA everywhere, add passkeys where you can.

  6. If it fires, assume the worst and wipe. Persistence hides in Electron apps and in npm, not only in the places a checklist looks. A reboot is not cleanup. If you cannot prove the machine is clean, you cannot trust it. I could not, so I formatted.

The uncomfortable part

The platforms are not catching these fast enough. I reported the job that got me, and the first reply said there was nothing wrong with it. The post came down a day later, from a different team, which tells you the front line and the safety team are not the same speed. You cannot outsource this to the marketplace. You are the last check.

The good news is that the defense is cheap. Skipping a scam costs you one lead you were never going to close anyway. Running one costs you your machine and days of your life. I know which trade I want now.

Be careful out there. If a client ever needs you to run their code before they will hire you, they are not hiring you.

Vinicius Pereira
vinimabreu.dev ยท github.com/vinimabreu

Top comments (0)