TL;DR — A CLI that scans your installed npm packages for malicious ones. Four fast rules pick the suspects, then a Gemma 3 4B model running on your own laptop reads each one and says what it's actually doing. Nothing leaves your machine. MIT, 125 tests,
github.com/Ackberry/exorcist.
If you're not a developer
When you install almost any modern app or website's code, it quietly pulls in hundreds of small packages written by strangers. Nobody reads them. In September 2025, attackers phished one maintainer and pushed malicious code into packages downloaded two billion times a week — it sat there rewriting people's crypto wallet addresses.
Exorcist is a tool that reads those packages before they bite, and asks an AI running on your own computer — not a company's server — whether anything looks wrong.
If you are a developer
Four rules, each one file in src/rules/:
| Ghost | Catches |
|---|---|
| Poltergeist | install hooks, child_process, network, process.env, .npmrc, eval/Function, base64, \xNN-escaped names |
| Doppelganger | typosquats — lodasg wearing lodash's face |
| Phantom | a direct dependency nothing imports, whose install hooks still run |
| Revenant | a release after 365+ days of silence, or under a new maintainer |
Rules are the cheap filter — regex and lockfile arithmetic, never executed. Only flagged packages go to the model. The rule total is a floor: Gemma can raise a score, never lower one. Each package comes back pin, swap, or remove.
git clone https://github.com/Ackberry/exorcist && cd exorcist
npm ci && npm run build
npm install --prefix demo/victim
node dist/cli.js demo/victim --no-oracle
No Ollama needed for that. ollama pull gemma3:4b and drop the flag to add verdicts.
--json pipes the report; --fail-on 75 exits 2 so CI can gate on it.
Three things worth knowing
It's noisy, and I'd rather say so. On its own 346 dependencies it flags 100 — mostly revenant findings at pin, which means "shipped again after a long quiet" and is both suspicious and completely normal. One is a real false positive: trough read as typosquat of through. Treat the score as a starting point for a person, not a gate.
I nearly improved the wrong layer. I was about to write a careful prompt to catch obfuscated code. I measured first: four packages doing the identical thing scored 100, 100, 100, and zero — the last one hid every name behind \xNN escapes and called Function("return this")(). A package with no findings never becomes a suspect, so the model would never have seen it. The fix belonged in the rules, not the prompt.
Local isn't a preference here. A scanner reads untrusted code by definition. Pipe that to a cloud model and you've built an injection target and shipped your dependency tree off the machine in one move.
Sentry tracing (opt-in, SENTRY_DSN) showed the shape of it: resolve, rules and score take about eleven milliseconds for five packages. Everything else is the model. That's the argument for rules-as-a-filter, measured instead of assumed.
Built on Gemma 3 4B via Ollama, orchestrated as a Mastra workflow, traced with Sentry. The limitations are written down in docs/STATUS.md, including that it only reads source and never runs it, so a payload that only appears at runtime is outside what it can see.
I started this for the Hacktoberfest Open-Source AI Challenge and missed the deadline by a few hours. Halloween got the better of me. Posting it anyway, because the thing works and the ghosts in node_modules do not care what day it is.


Top comments (0)