npm called my firewall malware. So does my firewall.
The registry rejects redstamp because its detection corpus is, byte for byte, a file full of attack strings. It isn't exactly wrong, and the obvious way past it is a technique lifted straight from the corpus I ship.
I can't publish redstamp to npm. Not “haven't got around to it,” the registry rejects the upload, deterministically, every time. I asked for a review and it was declined.
The reason is the interesting part, and it isn't npm being careless.
redstamp is a firewall for AI agent tool calls. Before an agent runs a shell command or calls an MCP tool, it classifies the action and blocks the catastrophic ones. To do that it carries a dictionary of around a hundred and fifty attack patterns, reverse shells, cloud-metadata SSRF, secret-exfil clauses, prompt-injection phrasings, encoded-payload shapes. That dictionary is the product. It is also, byte for byte, a file full of attack strings.
npm's content scanner reads it and sees malware. It is not exactly wrong.
What the failure looked like
The signature was unusually clean, which is what made it diagnosable.
Authentication succeeds. The OIDC trusted-publisher exchange succeeds and returns a token. The provenance statement gets signed and published to Sigstore's transparency log. Then the tarball PUT comes back 403 Forbidden, forbidden by your security policy, with no reason line.
Everything that could plausibly be misconfigured had already worked before the rejection landed. So it wasn't auth, wasn't the trusted-publisher binding, wasn't the package name, wasn't 2FA.
The decisive test was cheap. I published a benign canary to the same package name, a tiny tarball with a licence and a stub readme, no code, under a throwaway tag so nothing moved. It went through the identical pipeline on the first try.
Same account, same scope, same workflow, same credentials. Empty tarball accepted, full tarball rejected. The block is keyed on content. Three sibling packages with lighter signature density publish without complaint, which fits, it isn't the project, it's the density of attack strings in one file.
The part I find genuinely funny
redstamp does this to itself, though when I sat down to write this section I made the same mistake npm’s scanner did. I assumed. Then I measured, and the story got smaller and considerably more interesting.
The tool runs on my own machines as a Claude Code hook, screening every command my agents try to run. So I fed it the shapes I was about to accuse it of over-blocking.
What actually reproduces: writing about a blocked command gets blocked. This section nearly didn’t survive itself, a note recording that the firewall had refused a curl-into-interpreter one-liner is refused in turn, because the prose quotes the shape. The rule cannot tell a sentence describing a command from the command.
What I had wrong: the incident that prompted this section was a curl … | python -c I ran to check my own RSS feed after publishing. I filed it as a false positive. It isn’t one. Remote bytes were going into an interpreter process, and nothing in that command tells the classifier my script parsed the input rather than executing it, it scores identically to curl … | python with no script at all, where the remote bytes are the program. Downloading to a file first, which is what I did instead, is allowed. That is the rule working, not failing.
And two war stories I would have sworn to did not survive contact with the classifier. Searching the policy for its own rule text: allowed. A commit message quoting a live payload: allowed. Both had hardened in my notes as things that had happened, and neither reproduces.
So the false-positive class here is real but narrow, prose that quotes a command shape, and most of the folklore around it turned out to be mine. Which is its own lesson. I write a tool for deciding what is dangerous, and I still reached for an anecdote where I should have reached for a measurement.
Antivirus vendors have lived here for thirty years. A definitions file is indistinguishable from a malware sample by content alone; what separates them is provenance and intent, and neither of those is in the bytes. Every security tool that ships its detections as data inherits this the moment it touches a registry that scans uploads.
What I'm not going to do about it
There is an obvious workaround. Encode the signatures. Split them across files. Assemble the patterns at runtime from fragments. Any of it would sail past a content scan.
I'm not doing that, and the reason isn't rule-following.
The first problem is that it is detection evasion. I'd be taking a technique straight out of the corpus I ship, obfuscate the payload until the scanner stops recognising it, and using it against a scanner that is correctly doing its job. If I'm willing to do that the moment it's inconvenient for me, I have no standing to sell a tool that catches other people at it.
The second problem is worse. The reason you can trust a security tool is that you can read it. Every pattern redstamp blocks on sits in plain source where you can check it, disagree with it, or find the gap in it. Obfuscating those signatures to satisfy a registry would trade the single property that makes the tool auditable for a distribution convenience. That's a bad trade at any price.
So the answer to “why isn't it on npm” is: because the fix would make it worse.
What I did instead
Releases ship as Sigstore-signed tarballs on GitHub, with provenance you verify before you install:
gh release download --repo askalf/redstamp --pattern 'redstamp.tgz*'
gh attestation verify redstamp.tgz --repo askalf/redstamp --bundle redstamp.tgz.sigstore.json
npm i -g ./redstamp.tgz
I'd argue that's a stronger supply-chain story than the registry install it replaces. npm i hands you a tarball because a registry said so. This hands you a tarball you cryptographically verified came from the repository it claims to, built by the workflow it claims to be built by, and it fails loudly if it didn't.
The npm name still exists. It holds a deprecated pointer package that throws on import and tells you where the real thing lives. The worst outcome here was never “not on npm,” it was a stale registry page reading as abandoned while the project shipped weekly.
The part that generalises
If you maintain a security tool that ships signatures, a WAF ruleset, a secret-scanner corpus, a detection library, you will eventually meet a scanner that cannot distinguish your rules from the thing they describe. When you do, the question isn't how to get past it. It's whether the way past costs you the property your users are actually buying.
Provenance is the way out, not obfuscation. Signed artifacts and verifiable build origin answer “is this what the author published” without requiring anyone to guess what the bytes mean. That problem is solved. Content inspection of a detection corpus isn't, and I don't think it can be.
redstamp is MIT, zero-dependency, and blocks 165 of 165 malicious samples with zero false positives on a 291-sample corpus that anyone can re-run, alongside six other firewalls scored on the same neutral host.
You just can't npm install it. Ask npm's scanner why and it'll tell you it found something dangerous in there. It did. That's the product.
We build and run software on AI infrastructure that shifts under it: agents, release pipelines, the guardrails and automation that keep them shipping without a human in the loop. If you're putting agents near anything that matters, that's the kind of thing we're good at holding steady.
Start a conversation →