Screen tool and web output for prompt injection before your agent reads it
Put a check between untrusted tool results and the model: Llama Prompt Guard 2 for injection attempts, LlamaFirewall's scanners for hidden text and generated code, and Llama Guard for content safety. Includes a tested screening wrapper and the limits of each layer.
Prerequisites
- Python 3.10 or later (llamafirewall requires >=3.10)
- A Hugging Face account that has been granted access to the gated meta-llama/Llama-Prompt-Guard-2-22M or -86M repository, for the classifier
- An agent loop where you can intercept tool results before they go back to the model
Why screen tool output
An agent that browses, reads email or opens files puts text written by strangers into its own context. The Prompt Guard 2 model card describes the risk: prompt injections "manipulate untrusted third-party and user data in the context window to make a model execute unintended instructions." This guide adds one check at that boundary. Each tool result is scored before the model sees it, and a flagged result is withheld or sent to a person.
That check is one layer. Classifiers miss attacks, so keep the other layers too: give tools as little power as the task needs, ask a person before anything irreversible, and test the whole agent with AgentDojo.
Which tool does what
| Tool | What it checks | Runs where | Use it on tool output for |
|---|---|---|---|
| Llama Prompt Guard 2 22M / 86M | Text that "explicitly attempts to override prior instructions" (binary benign/malicious) | Locally, CPU or GPU | The main injection check |
| LlamaFirewall 1.0.3 | Wraps Prompt Guard 2 plus hidden-ASCII, regex, CodeShield and AlignmentCheck scanners | Locally, except AlignmentCheck (a hosted LLM by default) | Hidden Unicode text, known strings, unsafe generated code, auditing the agent's next action |
| Llama Guard 4 12B | Content safety: MLCommons hazard categories S1 to S13, plus S14 Code Interpreter Abuse | Locally, one GPU | Harmful content in inputs or outputs, not injection |
| HOL Guard | Agent actions and, per its README, "prompts and tool results" for prompt injection and sensitive content | Hooks in the agent harness | Coding agents where you can't edit the loop |
Llama Guard is a content-safety classifier, not an injection detector. Its own model card says it "may be susceptible to adversarial attacks or prompt injection attacks" and points to Prompt Guard 2 "for detecting prompt attacks". Use it if you also need to filter harmful content. The rest of this guide is about injection.
What Prompt Guard 2 detects, and what it misses
From the model card:
- Scope. A text is labelled malicious "if the prompt explicitly attempts to override prior instructions". Version 2 has no separate injection label. Meta found the broader "unintentional instruction-following" objective "too broad to be useful". A page that quietly says "the user also wants a copy sent to this address" may not be explicit enough to be flagged.
- 512 tokens. "Both Llama Prompt Guard 2 models support a 512-token context window. For longer inputs, split prompts into segments and scan them in parallel to ensure violations are detected."
- Two sizes. 86M is built on mDeBERTa-base and is multilingual. 22M is built on DeBERTa-xsmall and "reduces latency and compute costs by 75%, with minimal performance trade-offs". Both were evaluated in English, French, German, Hindi, Italian, Portuguese, Spanish and Thai.
- Known limits. The card says the models are vulnerable to adaptive attacks and that fine-tuning on your own prompts "improves accuracy and reduces false positives".
Meta's reported numbers:
| Model | AUC (English) | Recall at 1% FPR (English) | AUC (multilingual) | Latency, A100, 512 tokens | AgentDojo APR at 3% utility reduction |
|---|---|---|---|---|---|
| Prompt Guard 2 86M | 0.998 | 97.5% | 0.995 | 92.4 ms | 81.2% |
| Prompt Guard 2 22M | 0.995 | 88.7% | 0.942 | 19.3 ms | 78.4% |
| Prompt Guard 1 | 0.987 | 21.2% | 0.983 | 92.4 ms | 67.6% |
None of these figures is 100%, and they are Meta's own measurements on its own test sets. Plan for misses.
The weights are gated. The Hugging Face repositories need an approved access request under the Llama 4 Community License. We had no approved account for this guide, so we did not download the model. Everything below was run, but the classifier was a stand-in. The last section, How we tested, has the details.
Get the model onto the machine once
Request access on the model page. After approval, download the files to a fixed directory, from a machine where you are logged in:
hf auth login
hf download meta-llama/Llama-Prompt-Guard-2-22M --local-dir ./models/Llama-Prompt-Guard-2-22M
Your agent then loads from that directory and needs no Hugging Face token or network access at run time.
A screener for tool output
This module loads the classifier with local_files_only=True, so it can't download anything or prompt for a login. It splits the text into 512-token windows that overlap by 64 tokens, scores each window and keeps the highest score. The cookbook's inference utilities also take the maximum over chunks. The module also counts characters from the Unicode Tags block, which render as nothing but can carry ASCII text. Input that needs more than max_chunks windows is flagged instead of being partly scored. The result holds numbers and flags only, never the text.
"""Screen untrusted tool or web output with Llama Prompt Guard 2 before an agent reads it.
Loads the model from local files only: it never downloads and never asks for a
Hugging Face login. Results never include the screened text.
"""
import unicodedata
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
TAG_CHARS = range(0xE0000, 0xE0080) # Unicode Tags block: invisible "hidden ASCII"
class Screener:
def __init__(self, model_path, threshold=0.9, window=512, overlap=64, max_chunks=64):
self.tok = AutoTokenizer.from_pretrained(model_path, local_files_only=True)
self.model = AutoModelForSequenceClassification.from_pretrained(
model_path, local_files_only=True).eval()
labels = {v.upper(): int(k) for k, v in self.model.config.id2label.items()}
if "MALICIOUS" not in labels:
raise ValueError(f"no MALICIOUS label in {self.model.config.id2label}")
self.bad = labels["MALICIOUS"]
self.threshold = threshold
self.window = window
self.body = window - len(self.tok("")["input_ids"]) # leave room for [CLS]/[SEP]
self.step = self.body - overlap
self.max_chunks = max_chunks
def chunks(self, text):
ids = self.tok(text, add_special_tokens=False)["input_ids"]
starts = range(0, max(len(ids) - self.body, 0) + self.step, self.step)
return [ids[s:s + self.body] for s in starts] or [[]]
@torch.no_grad()
def score(self, text, batch=16):
best = 0.0
parts = self.chunks(text)
for i in range(0, len(parts), batch):
texts = [self.tok.decode(p) for p in parts[i:i + batch]]
enc = self.tok(texts, truncation=True, max_length=self.window,
padding=True, return_tensors="pt")
probs = torch.softmax(self.model(**enc).logits, dim=-1)[:, self.bad]
best = max(best, probs.max().item())
return best, len(parts)
def screen(self, text):
hidden = sum(ord(c) in TAG_CHARS for c in text)
fmt = sum(unicodedata.category(c) == "Cf" for c in text) - hidden
n = len(self.chunks(text))
if n > self.max_chunks: # fail closed rather than score part of it
return {"flagged": True, "score": None, "chunks": n, "too_long": True,
"tag_chars": hidden, "other_format_chars": fmt}
score, n = self.score(text)
flagged = score >= self.threshold or hidden > 0
return {"flagged": flagged, "score": round(score, 4), "chunks": n,
"too_long": False, "tag_chars": hidden, "other_format_chars": fmt}
The 0.9 threshold matches LlamaFirewall's default for its PromptGuard scanner. Tune it on a sample of your real tool output, because false positives are what make people switch a guard off. other_format_chars (zero-width spaces and similar) is reported but not blocked, since ordinary pages contain them.
Wrap every tool that returns outside text:
import logging
from screen_tool_output import Screener
log = logging.getLogger("screen")
screener = Screener("./models/Llama-Prompt-Guard-2-22M")
def guarded(tool_name, output):
verdict = screener.screen(output)
log.info("screened %s: %s", tool_name, verdict) # the verdict holds no tool text
if verdict["flagged"]:
return f"[{tool_name} output withheld: flagged as possible prompt injection, score {verdict['score']}]"
return output
Return the placeholder to the model so it knows the call happened. Keep the original in a quarantine store that a person can review. Don't put it in the same log as the verdict.
LlamaFirewall: what works on tool output
LlamaFirewall assigns scanners per message role. ToolMessage has the role Role.TOOL. With no configuration, tool messages get CodeShield and PromptGuard. When several scanners run, any BLOCK wins.
This configuration ran on CPU with no gated model and no API key:
from llamafirewall import LlamaFirewall, Role, ScanDecision, ScannerType, ToolMessage
fw = LlamaFirewall({Role.TOOL: [ScannerType.HIDDEN_ASCII, ScannerType.REGEX]})
result = fw.scan(ToolMessage(content=tool_output))
if result.decision != ScanDecision.ALLOW:
... # withhold or ask a person
What we saw with llamafirewall 1.0.3:
- Text hidden in tag characters was blocked, and the reason contained the decoded hidden text.
- "IGNORE PREVIOUS INSTRUCTIONS" was blocked by the default regex. "Disregard what you were told earlier" passed. The default injection pattern is just
ignore previous instructions|ignore all instructions. - The default regex set also blocks email addresses, phone numbers, card numbers and US social security numbers. That blocked a tool result that only contained a contact address. On tool output you will usually want your own patterns.
- CodeShield blocked a snippet using MD5 for passwords (CWE-327). That makes sense for code an agent is about to run or commit. See also scanning agent-written code with Semgrep.
- A role with no scanners configured returns ALLOW with the reason "default". If you configure only
Role.USER, tool output passes unchecked.
Gotchas with ScannerType.PROMPT_GUARD in 1.0.3, the latest release on PyPI (May 2025):
- Imports fail with current huggingface_hub. On a fresh install today, the scanner raised
ImportError: cannot import name 'HfFolder' from 'huggingface_hub'with huggingface_hub 2.4.0. The repository's main branch has the fix, but it isn't released. Pin"huggingface_hub<1.0"(we used 0.36.2, with transformers 4.57.6) or use the screener above. - It always uses the 86M model (
meta-llama/Llama-Prompt-Guard-2-86M), and with no saved token it calls Hugging Face's interactivelogin(). In a non-interactive process that ended inEOFError. - It only scores the first 512 tokens. The text is tokenized with
truncation=True, max_length=512. An injection further down a long page is never seen. Split long output yourself, as in the screener, before you hand it to the scanner. - Its BLOCK reason includes the full text (
Full text: "..."). Don't sendresult.reasonto shared logs.
AlignmentCheck is different. Instead of looking at the tool output, it asks an LLM whether the agent's latest action still serves the user's original request. That can catch injections a classifier misses. In 1.0.3 the firewall builds it with fixed defaults: meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8 at https://api.together.xyz/v1, with the key in TOGETHER_API_KEY. It sends the whole trace, user request included, to that endpoint. Check that this is acceptable for your data before you turn it on. We did not run AlignmentCheck.
Put it together
- Mark which tools return outside content: web fetch, search, email, files, other agents and MCP servers you don't run yourself.
- Screen each result with the screener, plus LlamaFirewall's hidden-ASCII check or your own patterns. Withhold flagged results and keep them for review.
- Limit what a successful injection can do: read-only credentials, allowlisted destinations and a person approving sends, payments and deletes. The guardrail frameworks comparison covers policy layers that check tool calls.
- For coding agents, HOL Guard works through the agent's own hooks and MCP proxies. Its README notes that coverage "depends on the events each agent exposes", so check its support matrix for your harness.
- Measure. Run AgentDojo with and without the screener, and add attacks from the red-teaming tools. A guard you haven't measured is a guess.
How we tested
Run on 2026-11-03 on CPU (Python 3.13.5, torch 2.15.0+cpu, llamafirewall 1.0.3, codeshield 1.0.1).
- The Prompt Guard 2 repositories are gated (
"gated": "manual"on the Hugging Face API), and an unauthenticated download ofconfig.jsonreturned 401. We had no approved account, so we did not measure detection quality. The figures above are Meta's. - The screener was run with transformers 4.57.6 and 5.22.1, against a stand-in model: the ungated
microsoft/deberta-v3-xsmallcheckpoint with an untrained two-label head. The tests checked these code paths: - real tokenization and forward passes
- every token of a 2,000-token input covered by five windows of at most 512 tokens
- an injection placed after token 2,000 caught by a stub head, which first-512-token truncation missed
- tag characters flagged
- oversized input failing closed
- an uncached gated model id raising
OSErrorat once, with no download or prompt - the wrapper withholding a flagged result without echoing it
- LlamaFirewall's HIDDEN_ASCII, REGEX and CODE_SHIELD scanners were run as shown. PROMPT_GUARD was run only far enough to see the import error and the login prompt.
Directory entries in this guide
- Llama Prompt Guard 2: Meta's small open-weight classifiers (86M and 22M) that flag prompt injection and jailbreak attempts in user input and untrusted content.
- LlamaFirewall: Meta's open-source guardrail framework for agents that chains scanners for prompt injection, goal hijacking and insecure generated code.
- Llama Guard: Meta's open-weight safety classifiers that label LLM prompts and responses as safe or unsafe against the MLCommons hazard taxonomy.
- HOL Guard: Local-first security layer that reviews AI coding agents' shell commands, file access, package installs and MCP tool calls before they run.
- AgentDojo: ETH Zurich benchmark environment for prompt-injection attacks and defenses on tool-using LLM agents, with suites like workspace, banking, travel and slack.
Related
- Test a tool-using agent for prompt injection with AgentDojo (Guide)
- Runtime guardrails for agents: Guardrails AI, NeMo Guardrails, Invariant and HOL Guard (Comparison)
- Open-source red-teaming tools for LLM apps and agents (Comparison)
- Run agent-generated code safely in a sandbox (Guide)
- Scan agent-written code for vulnerabilities with the Semgrep MCP server (Guide)
Sources
- Llama Prompt Guard 2 86M model card, accessed
- Llama Prompt Guard 2 22M model card, accessed
- Llama Prompt Guard 2 22M on Hugging Face (gated), accessed
- llama-cookbook: Prompt Guard inference utilities, accessed
- LlamaFirewall README, accessed
- llamafirewall on PyPI, accessed
- LlamaFirewall source: firewall and default scanners, accessed
- LlamaFirewall source: Prompt Guard utilities, accessed
- LlamaFirewall source: Prompt Guard scanner, accessed
- LlamaFirewall source: regex scanner, accessed
- LlamaFirewall source: AlignmentCheck scanner, accessed
- LlamaFirewall source: custom LLM check scanner (AlignmentCheck defaults), accessed
- Llama Guard 4 12B model card, accessed
- HOL Guard README, accessed
- Unicode Character Database, Blocks.txt (Tags: E0000..E007F), accessed