When Malware Jailbreaks Your Scanner: Fail Open vs Fail Closed
On June 8, 2026, six bioinformatics packages on PyPI — ensmallen, embiggen, pyphetools, gpsea, phenopacket-store-toolkit, and ppkt2synergy — shipped malicious versions within about 60 seconds of each other. Endor Labs quarantined all six inside 30 minutes. The payload was a credential-stealing worm that harvested AWS keys, GitHub PATs, Vault tokens, SSH keys, and even the API keys of AI coding assistants from ~/.config.
That part is ordinary supply-chain malware. The interesting part is what the attacker put above the payload.
Weaponizing the scanner's safety training
The file opened with a ~99-line comment block written to look like a classified briefing demanding weaponized biological and nuclear device instructions. The real loader sat on line 101. JFrog Security Research described the structure plainly: a guardrail-triggering prompt at the top, then the obfuscated dropper:
try { eval(reconstruct(/* obfuscated blob */)); } catch (e) {}
The goal was not to convince an LLM-based malware scanner that the file is clean. It was subtler: make the scanner refuse to answer. When a safety-tuned model reads a file demanding CBRN content, its own alignment training fires. It declines. And in a lot of scanning pipelines, "the model declined" is treated the same as "nothing to report." The malware passes.
That is the whole chain:
Malicious package → prompt injection → LLM refusal → scanner fails open → malware ships.
The results were inconsistent but real. Per JFrog's testing, Claude refused in every mode tested and the Gemini API refused, while the Gemini chatbot, ChatGPT, DeepSeek, and a local Qwen all correctly flagged the file. So the technique specifically beats the most safety-tuned configurations — the ones a security team is most likely to trust. As JFrog put it, the model underneath can often read the file and name exactly what is wrong; if the guardrail trips first, none of that reaches you.
A parallel npm variant, documented by Socket, stacked a second trick on top: token flooding. It repeated "You're absolutely right!" across tens of thousands of lines to inflate the file past 3.5 million tokens, blowing out the scanner's context window before it ever reached the payload. Different mechanism, same objective — starve the analysis, not fool it.
Zscaler ThreatLabz and StepSecurity tracked the same wave as an evolution of the long-running Shai-Hulud / "Hades" campaign, noting the injection block was positioned to either force a false-clean verdict or trigger a safety refusal — either outcome lets the package through.
Three things this changes
The researchers' takeaways are worth stating directly, because they generalize well beyond this one worm:
- Malware now carries prompt injection as an anti-analysis layer. The scanner is part of the attack surface.
- Source code is untrusted input. If an LLM reads a file to judge it, that file can talk back.
- Safety guardrails can sabotage legitimate security analysis. Alignment tuning that refuses "dangerous" content will also refuse to analyze content that merely looks dangerous.
The structural bug
The failure isn't that the model is dumb. It's that a single model is being asked to both judge untrusted input and obey its own safety rules on that same input. The attacker only has to trip rule two to defeat job one. Any architecture where the thing reading the input is also the thing that can be talked out of reading it will fail open.
Why deterministic, upstream screening doesn't fail open
The attack's premise is that the scanner is an LLM, so you weaponize its safety training. Remove that premise and the technique has nothing to grip.
That means screening untrusted content deterministically and upstream of the model, rather than asking a safety-tuned LLM to be the judge. This is the whole design of AgentGuards: it screens input before it reaches a model — regex plus a PromptGuard classifier — in under 50 ms, and it fails closed.
I ran the actual technique against it. Two cases, both real results — and I'll be exact about scope.
Case A — guarding the agent that reads the untrusted file. When an agent fetches a web page or reads a file, that content goes through check_input with use_case="web_fetch" first. Against the injection header, the decision was block — jailbreak, critical severity, matched on the "unrestricted mode" pattern. It works because a regex and a classifier can't be argued into a refusal and don't care about CBRN framing. Interception happens before the model, so the model's safety layer — the thing the attack needs to fire — is never reached. (While drafting this post, the same guardrail even blocked my research fetches of the source articles for containing the string jailbreak. Annoying, and exactly the fail-closed behavior the attack can't exploit.)
Case B — AgentGuards' own code_scan as the scanner. This path is semgrep plus gitleaks — deterministic SAST and secret detection, no LLM in the loop. The injection header did nothing to it. There is no model to refuse and nothing to jailbreak, so it is immune to the anti-analysis trick by construction.
Here's the honest caveat, because overclaiming would be its own kind of slop: in my test, code_scan returned allow / no findings on the synthetic obfuscated eval(reconstruct(...)) dropper. AgentGuards is a guardrail and input-screening layer, not an antivirus, and semgrep's ruleset didn't match that particular obfuscated loader. So this is not a claim that AgentGuards detects the malware. The narrow, correct point is this: a deterministic scanner cannot be made to fail open by a jailbreak, while an LLM scanner can.
▶ Watch: Your AI Agent Can Be Tricked Into Curling Any Webpage — Here's the Fix — the same upstream web-content screening from Case A, shown live.
The takeaway for anyone building AI security tooling
If your malware scanner, code reviewer, or triage bot is a bare LLM prompt, treat every file it reads as adversarial input, because attackers now do. The defense isn't a better safety prompt — the attack targets the safety prompt. Put a deterministic guardrail in front of the model so untrusted input is screened before it can touch the model's own behavior. You can try the exact check from Case A in the prompt-injection checker or wire it into your own agent via the REST API.
Fail closed, not open. When you're not sure whether a file is safe, the correct default is to block — not to let a polite refusal wave it through.