On 4 and 5 November, three "clients" sent me malware. None of it ran, because I read every package before I open it. This is what they looked like.
- A zip of requirements. A job post from an account with years of real hiring history, which had just posted five unrelated jobs in the same minute: a hijacked account. The post demanded that applicants confirm they had seen the attachment. Inside the zip were two screenshots and
Project Requirements and Discussion.vbs. Double-click it on Windows and it downloads a decoy text file and a batch file into%TEMP%, then runs the batch file. - A repository of documents. Another "client" asked for a call, then sent a zip of a git repository of Markdown documents, a whitepaper and a design doc for a token project. The guide in it asked me to run
git checkout whitepaperto read each section. Two hooks in.git/hooks,post-checkoutandpre-commit, ran a download piped into a shell in the background:curl | bashon macOS,wget | bashon Linux,curl | cmd.exeon Windows. Reading the Markdown does nothing. The checkout runs it. The marketplace's own attachment scan marked the zip clean. - The same zip again. The next day the
.vbsoperation came back from another hijacked account, as a $7,000 job for an AI developer, withRequirements and Project Info.vbsnext to two clean screenshots.
I read all three as data, without running or opening anything in them, so nothing in them ever executed. That is the whole trick: none of it works unless you run it, and a developer's reflex is to run it. You get a repository, you clone it and run it, the same way you hold a door for someone carrying coffee. That reflex is the exploit, and the people losing the most to it are not freelancers.
This is not a freelancer problem
The same move has emptied some of the best-defended companies in crypto.
- Ronin, 2022, about $625 million. According to reporting by The Block, a senior engineer at Sky Mavis, the studio behind Axie Infinity, was approached on LinkedIn with a fake job offer and opened a PDF of the "offer" that carried spyware. That foothold was the way into the bridge. (Engadget)
- Bybit, February 2025, about $1.5 billion. Safe's investigation with Mandiant traced it to one developer's Mac, infected after the developer downloaded a Docker project, a stock-investment simulator, through social engineering. Session tokens from that laptop got the attackers into Safe's cloud, where they changed the wallet interface Bybit's signers used. (The Hacker News)
- Binance, 29 October 2026, $6.34 billion. Binance traced the theft to an employee who was contacted through a job platform, invited to fake interviews and given a coding assignment. The malware replaced the
pyenvexecutable in the Homebrew directory, and the endpoint protection on the machine did not catch it. (BleepingComputer) - Contagious Interview, the campaign Palo Alto Networks Unit 42 documented in late 2023, does this at scale. Fake recruiters share a project on GitHub or Bitbucket whose dependencies include malicious npm packages. The first stage, BeaverTail, is a stealer that fetches a Python backdoor called InvisibleFerret. (The Hacker News)
None of these people were careless. They worked at companies with security teams and endpoint protection. They did what every developer does with a repository: they ran it.
Why you verify before you run
Every one of those attacks needs one thing from you, and it is execution. Execution is cheaper to give away than it looks.
-
npm installruns install scripts.npm run devloads every config file the toolchain reads, and--ignore-scriptsdoes nothing for a script you ask for by name. -
git checkoutruns hooks, and a git config can name the program git starts as your pager or your editor. - Opening a folder in an editor can start a task (
"runOn": "folderOpen"), and opening it in a devcontainer runs its lifecycle commands. - Double-clicking
Project Requirements and Budget.vbsruns it.
Antivirus is not the backstop people think it is. Binance's did not see the new malware. A sandbox helps, and anything a stranger sends runs in a disposable container on my side, but a clean sandbox proves less than it seems. Malware written for macOS simply fails on Linux, and modern loaders check for a virtual machine and sit still. When a sandbox catches something, that is real evidence. When it catches nothing, it has not proved much.
So the first line of defense is reading the thing before any of it executes. The problem is that nobody reads a whole repository by hand at 11 p.m. with a client waiting. So I wrote down what has to be checked before anything runs: install hooks, git hooks, every config that runs on load, editor tasks, obfuscation, network calls, anything that touches the keychain or ~/.ssh. Then I turned that list into a tool.
gifthorse
gifthorse reads a file, folder or archive without running any of it and answers in one word: BLOCK, REVIEW or NO FINDINGS, with the file, the line, and the hosts it would have contacted, defanged and ready for an abuse report. On a zip shaped like the first one above:
$ gifthorse --iocs brief.zip brief.zip: BLOCK Do not open or run anything from this package. [block] GH101 Executable or script inside a package at brief.zip!/Project Requirements and Budget.vbs A program file (.vbs) is inside a document package. evidence: Project Requirements and Budget.vbs trigger: when you double-click the file or open it on Windows next: do not open it; ask the sender for a PDF or plain text [block] GH201 Download, write and run chain in a Windows script at brief.zip!/Project Requirements and Budget.vbs:1 The script downloads a file, saves it, then runs it. evidence: download line 1, write line 4, run line 6 trigger: when you double-click the script on Windows next: report it; use the --iocs block for the hosts it would contact inspected 3 of 3 files 2 findings: 2 block, 0 review, 0 info Indicators (defanged, for an abuse report): url hxxp[://]stage[.]cdn[.]example[.]invalid/payload Every finding says, in plain words, the everyday action that would set it off and what to do instead. The person holding the file is usually not a security engineer, so the output is written for someone who is not.
What it looks at: program files and disk images where paperwork is expected; names that hide their real extension; git hooks and git config keys that run a command; the download, write and run chain in Windows scripts; build configs that run on load and carry hidden code; install scripts and curl | sh install lines; editor tasks, devcontainer commands and agent configs that start a process; code pushed off-screen, decoded strings run as code, invisible characters, and a blockchain lookup used as a channel; and access to saved passwords and keys. Its test fixtures rebuild these shapes synthetically in memory, among them the .vbs dropper, a git hook that pipes a download into a shell, and a postcss.config.js with a payload pushed off-screen, and each one comes back BLOCK.
It never executes or extracts anything from the target. Archives (zip, tar and its gzip, bzip2 and xz forms, single compressed files, and RAR5 member names) are read in memory under caps. It is Python 3.11 or newer, standard library only, and the test suite runs with sockets disabled.
NO FINDINGS is not a clearance
The hardest part of building it was not the rules. It was deciding what the quiet answer means.
The first version had shortcuts that felt reasonable. It followed links inside the package to read their targets. It followed git include directives. It treated a folder with a .git or a src directory as a code project, and a code project earns some slack, since an installer under tests/ is normal there. Three skeptical reviews later, each of those shortcuts had turned out to be a way for a file to pass without being judged. A link can point anywhere. An include can pull in a config the tool never sees. A src folder costs nothing to add to a zip of "requirements".
I removed them instead of patching them. The rule now is deny by default: anything the tool did not read, did not follow, or could not judge by its type is a REVIEW. A link is a REVIEW whatever it points at. Only a manifest whose content confirms it makes a folder a project. NO FINDINGS means nothing was found. It never means safe, and the README says so in the verdict table.
That costs some noise, so I measured it. On 218 popular npm and PyPI packages, the published files come back 169 NO FINDINGS, 49 REVIEW and 0 BLOCK. Every REVIEW is listed with its rule and path: mostly install scripts that run code on npm install, plus a few links and git submodule pointers that are no longer followed.
Verifying the verifier
The day I published it, the first CI run on Windows came back red. gifthorse had scanned its own repository and called every Python file a BLOCK.
The cause was one character. Git on Windows checks files out with CRLF line endings, and the pattern that confirms a pyproject.toml ended in [ \t]*$. The \r sat between the section header and the end of the line, so the manifest did not confirm, the folder stopped being a project, and every .py file in a folder full of docs looked like a script dropped into paperwork. On macOS and Linux the suite was green. On the platform where people double-click attachments, it was wrong.
The fix was two lines: read marker files with their line endings normalized. Then came a test that fails without the fix, and a rerun of the benchmark, which did not change. What I keep from it is that the failure was loud. A tool that fails closed shouts BLOCK when it is confused. One that failed open would have said NO FINDINGS on every Windows checkout, and nobody would have noticed.
What I do now
- I read before I run. The tool does the first pass in seconds, and then I read every line it points at.
- Nothing a stranger sends runs on my machine before a contract exists. When something must run, it runs in a throwaway container with no network and no credentials.
- A client who insists that I run their project and send a screenshot before we sign is not testing my skills. That request is the scam.
- A clean scan means the first read found nothing obvious. It does not clear the package.
The repo is at github.com/vinimabreu/gifthorse, MIT licensed:
pipx install "git+https://github.com/vinimabreu/gifthorse@9be244fecb79eca25e296e6540db5777de6313fa" gifthorse ./the-thing-they-sent If you find a package that should be a BLOCK and is not, report it privately through the repository's security page. Those reports will make it better than any rule I can think of alone.

