DEV Community

Cover image for I Built the Thing My Last Article Said Didn't Exist Yet
Cor E
Cor E

Posted on

I Built the Thing My Last Article Said Didn't Exist Yet

The other day I wrote about the npm worm that learned to trust your AI agent — a keyv-adjacent supply-chain attack that didn't bother with credential exfiltration. It planted hooks into Claude Code and VS Code, timed to fire on SessionStart/folderOpen, and let the editor's own trust in your workspace do the detonating. I ended that piece on a question I didn't have an answer to: if workspace trust and dependency trust are both single, coarse-grained decisions, and agent tooling keeps stacking new automatic triggers on top of that trust, at what point does "trusting a workspace" stop meaning anything specific at all?

I still don't have a full answer. But I ran into the concrete, boring version of the problem this week, and it was annoying enough that I fixed the one narrow slice of it I actually could.

The slice: I couldn't tell my own hooks from a planted one

I'd added a PreToolUse hook to Claude Code a while back — nothing exotic, it checks package installs against a scanner before they run. While testing an unrelated change, a tool result came back with what looked like a genuine security annotation: a package had supposedly failed a registry check and I should warn myself and help remove it.

Except no package had been installed. The command that supposedly triggered it was a syntax check — ast.parse and bash -n, nothing else. I flagged it as bogus in the moment and moved on, but a few days later, re-reading my own notes on it, I'd filed it away as "a parsing bug in my hook, fix later" — which is a much less alarming story than the one that turned out to be true. I went back and replayed the exact command that supposedly triggered it through my hook's actual code. Zero targets. It never even ran. Something else had put a fake security warning into my own tool output, and I'd half-talked myself into a boring explanation for it instead of an alarming one.

That's the part that stuck with me. Not the worm technique specifically — the fact that I, sitting right there, watching my own hook fire, could not immediately and confidently tell the difference between "my code did this" and "something else did this and is pretending to be my code." If I can't tell, a routine dependency review absolutely can't. And "a bad package plants a hook config three directories deep in a transitive dependency" is exactly the technique from the worm article — nobody expects executable config to be hiding in node_modules.

The fix: sign what's yours, so "not signed" means something

There's no built-in way to distinguish "a hook I intentionally installed", or asked Claude to help me build and isntall, from "a hook a bad package just planted." So I built one — claude-hookscanner, MIT-simple in concept even if the install script ended up longer than I expected:

  • HMAC-sign every hook you author. A shared secret, generated once, keyed via HMAC-SHA256 into a # sentinel-hmac: <hex> comment on the script itself. An attacker who can drop files onto disk — which is the actual capability a malicious package has — has no path to that key. Missing or invalid signature reliably means "not something I signed."
  • Heuristics for everything you can't sign — you can't HMAC a third-party package's own files. Any .claude config found inside node_modules/ gets flagged; hook commands matching high-risk patterns (curl | bash, eval, base64 -d, ...) get flagged; hook scripts resolving outside the expected hooks directory get flagged.
  • A content-hash ack-list for things you've actually looked at and judged benign, so you're not re-reviewing the same harmless dependency debris every scan.
  • A PostToolUse nudge that reminds the agent to sign a hook the moment it writes one, instead of leaving it unsigned and indistinguishable from a planted one until the next scan happens to run.

The part I'm not going to oversell

The article that started this was, if anything, about resisting the urge to overclaim. So: this tool is aimed at opportunistic, drive-by supply-chain worms — generic payloads that hardcode a short list of well-known paths across every victim they land on, because that's what keeps a mass-distributed payload cheap. Against that, even the install script's random-not-fixed key location is a real, if modest, improvement — the whole reason I bothered randomizing it instead of using a normal predictable path.

It is not protection against a targeted attacker who's read this article and knows to go looking. A lone 64-character hex file sitting alone in your hooks directory is identifiable by shape and location no matter what you name it or where you hide it — and a fully root-compromised host defeats the whole scheme regardless, since root can read the key too. I'd rather say that plainly than have someone find out the hard way that "raises the bar against generic attacks" and "unbreakable" are not the same sentence.

Try it

git clone https://github.com/c0ri/claude-hookscanner.git
cd claude-hookscanner
./install.sh
Enter fullscreen mode Exit fullscreen mode

Note: This only works on Linux atm and Linux on WSL for Windows, but if there's interest maybe I can help port it to mac and true Windows.

It walks you through key placement, wires the reminder hook into ~/.claude/settings.json (backing it up first), and runs a scan immediately so you're not taking it on faith. --uninstall is in there too, and it doesn't require you to have remembered where the key ended up.

I still don't know at what point "trusting a workspace" stops meaning anything specific. But at least now, when a hook on my machine tells me something failed a security check, I can ask it to prove it's actually mine — and that's a smaller, more honest claim than "solved," which is probably the right size for it.

— Cor, Skyblue Soft

Top comments (0)