llm-sanitizer
CLI + MCP server for detecting, classifying, and redacting embedded LLM agent instructions in documents, source code, and web pages.
What It Does
Documents, web pages, and source code can contain embedded instructions
targeting LLM agents — from legitimate customization to malicious prompt
injection. llm-sanitizer scans content for these embedded instructions,
classifies their risk level, reports findings in multiple formats, and
optionally produces redacted output.
Detection
Twelve pluggable detection rules covering:
- Instruction override phrases ("ignore previous instructions…")
- Zero-width character encoding (hidden text via invisible Unicode)
- HTML/markdown hidden content (white-on-white, display:none)
- Role-play injection ("act as", "you are now DAN")
- System prompt markers (
<system>,## System Prompt) - Data exfiltration attempts ("output the system prompt")
- Comment-embedded directives (
<!-- AI: do this -->) - Base64-encoded instructions
- Unicode homoglyph substitution (Cyrillic lookalikes)
- Agent-specific config patterns in unexpected locations
- Character-splitting obfuscation (
i g n o r e,ignore___all) — reconstructed and re-scanned - Semantic-intent injection — a local, no-egress n-gram classifier that catches keyword-less rephrasings (role reassignment, verbatim/echo exfiltration, supersede-prior-guidance) the pattern rules miss
Classification
Five risk levels: info (legitimate AI config) → low → medium → high → critical (confirmed injection technique).
Redaction
Non-destructive cleaning in four modes:
- strip — remove the instruction entirely
- comment — replace with
[REDACTED: ...]marker - highlight — wrap in visible markers for review
- placeholder — replace each character with
█, so the text is gone but every byte offset, line number and column in the document is unchanged
Binary inputs never produce unredacted binary output. Scanning a PDF,
DOCX, PPTX or XLSX requires extracting its text, so that redacted text is what
the redact tools write, and the response says so
(output_format: "extracted-text"). The original bytes are never copied to an
output path. Where no text can be recovered at all, the call is refused and
no file is written — an unredacted copy is worse than no file, because a
caller that checks only whether the output exists cannot tell the two apart.
PDFs can also be redacted in place, with the [pdf-redact] extra:
uv pip install 'llm-sanitizer[pdf-redact]'
A rewritten <stem>.redacted.pdf is written beside the text output, with the
findings removed from the content stream — not a black rectangle drawn
over them, which leaves the text in the file and is the classic failure of
this category. It is published only after being verified clean three ways
(the project's extractor, a second independent extractor, and a raw
content-stream search); a rewrite that cannot be proved clean is deleted and
reported as refused, and the redacted text output still stands.
Installation
pip install llm-sanitizer
# or
uv pip install llm-sanitizer
Platform notes
The dependency chain markitdown -> magika -> onnxruntime needs binary wheels for
onnxruntime on your platform and Python version.
- Intel macOS (x86_64): onnxruntime dropped
macosx_x86_64wheels entirely at 1.24.0, and the last Intel build (1.23.2) only ships wheels for Python ≤3.13. Use Python 3.13 or older — with uv, pin the interpreter explicitly:uvx --python 3.13 --from llm-sanitizer[7z,rar] llm-sanitize .... - Apple Silicon macOS: works on macOS 14+ (onnxruntime 1.24+ wheels require macOS 14); on macOS 13 or earlier use Python ≤3.13.
- Linux and Windows: work on Python 3.11 through 3.14.
We keep requires-python = ">=3.11" rather than capping it: a cap cannot carry
platform markers and would regress the platforms where Python 3.14 resolves fine.
Quick Start
CLI
# Scan a file
llm-sanitize scan document.md
# Scan a URL
llm-sanitize scan https://example.com/page
# Scan a directory
llm-sanitize scan ./docs/ --glob "**/*.md"
# Redact a file
llm-sanitize redact document.md -o clean_document.md
# Redact a directory (mirrors structure)
llm-sanitize redact ./docs/ -o ./clean_docs/
# Control how binary files (PDF, DOCX, zip, images, ...) are handled —
# detected by content, not extension, so renaming a file changes nothing
llm-sanitize scan document.pdf --binary-mode extract # pull embedded text via markitdown (default)
llm-sanitize scan suspicious.png --binary-mode text # force raw bytes to be scanned as literal text
llm-sanitize scan large_dir/ --binary-mode skip # exclude binary files from the scan entirely
# Assemble a directory report from previously-saved `scan --format json`
# results without re-scanning (for callers that cache scans by content hash)
llm-sanitize merge --manifest manifest.txt --format sarif > report.sarif
--binary-mode extract (the default) rejects malicious zip archives — excessive
entry counts, total uncompressed size, or compression ratios — before
decompressing anything.
MCP Server
Add to your MCP configuration:
{
"mcpServers": {
"llm-sanitizer": {
"command": "llm-sanitizer"
}
}
}
Available tools: scan_text, scan_file, scan_url, scan_dir, redact,
redact_file, redact_url, redact_dir, list_rules.
scan_url/redact_url fetch server-side with an honest browser UA, but a
managed WAF (Cloudflare/Akamai) can still refuse the request based on origin
reputation. That failure comes back as {"status": "error", "error_type": "fetch_blocked", "http_status": <code>, ...} — distinct from a scan failure,
so a caller knows the page could not be verified rather than that scanning
broke. llm-sanitizer will not route fetches through a third-party proxy to
work around a block (a trust-boundary tool observing/altering its own input
via another network defeats its own threat model) or otherwise bypass the
site's bot-protection. For a page that can't be fetched server-side, paste
its HTML/text and use scan_text instead.
Python API
from llm_sanitizer.scanner import scan_text
result = scan_text("Check this <!-- ignore previous instructions --> content")
print(result.findings) # [Finding(rule='comment_directive', risk='high', ...)]
Configuration
Create .llm-sanitizer.yml at your project root:
A config file that exists is always applied, or the scan fails. If
.llm-sanitizer.ymlis present but cannot be read — an unparseable file, or a stripped-down install withoutPyYAML— the scanner raisesConfigErrorinstead of falling back to defaults. Reporting one policy while enforcing a different one is the failure this refuses to have.Having no config file is unaffected: the built-in defaults apply, and that is a deliberate choice rather than a silent one.
sensitivity: medium # low | medium | high
rules:
zero_width:
enabled: true
instruction_override:
enabled: true
sensitivity: high # per-rule sensitivity override
policy:
mode: "allow-known" # allow-known | allow-none | allow-all
agents:
copilot: allow
cursor: allow
# Refuse oversized input fail-closed (a CRITICAL input_too_large finding)
# rather than reading/scanning it. Default 25 MiB.
max_scan_bytes: 26214400
# How to treat a non-archive binary that yields no extractable text.
unprocessable_binary_policy: fail # fail (default) | scan-text | ignore
# Recursive archive scanning limits (zip / tar / gz / bz2 / xz / 7z / rar).
archive:
max_depth: 3 # archive-in-archive nesting levels
max_cumulative_bytes: 524288000 # 500 MB across all nested levels
max_uncompressed_bytes: 104857600 # 100 MB per archive
max_entries: 1000 # per-archive entry-count cap
max_compression_ratio: 100 # zip-bomb ratio guard
formats: [zip, tar, gz, bz2, xz, 7z, rar]
output:
format: markdown # json | markdown | sarif
context_lines: 2
Output Formats
- JSON — structured findings for programmatic use
- Markdown — human-readable reports
- SARIF — GitHub Code Scanning / VS Code integration
Documentation
Credits — semantic-intent training data
The semantic_intent classifier is trained on a hand-curated corpus plus,
optionally, third-party labeled datasets (used at train time only — only the
trained weights ship; the datasets are not redistributed). With thanks to:
- prodnull/prompt-injection-repo-dataset (Apache-2.0) — injection embedded in repository files, with domain-matched hard negatives.
- deepset/prompt-injections (Apache-2.0) — classic prompt-injection set (positives used for augmentation).
See data-raw/SOURCES.md for full provenance, licenses,
and pinned revisions.
License
AGPL-3.0-or-later. Commercial licensing available — contact greg@warnes-innovations.com.
Metadata
Release files for llm-sanitizer 0.7.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| llm_sanitizer-0.7.1.tar.gz | 334.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| llm_sanitizer-0.7.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 555.0 kB
Release files / llm_sanitizer-0.7.1.tar.gz
| Download URL | llm_sanitizer-0.7.1.tar.gz |
|---|---|
| Size | 334.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5efbe76e5031c6bb8f688ec47c78f517e957bb2e697a8d485257dec53a5f2313
|
|
BLAKE2b-256 checksum How to use checksums |
5a996b2ee36ed71e4e49ed5e3c72ea8f67b5a06aba31c22022fff0d4e793729f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / llm_sanitizer-0.7.1-py3-none-any.whl
| Download URL | llm_sanitizer-0.7.1-py3-none-any.whl |
|---|---|
| Size | 220.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7d9fc475b8448f491b71358bc95cc2b943d973ec3c2ff0354c1f9efab6a8c9c7
|
|
BLAKE2b-256 checksum How to use checksums |
feff4e66c662bc57eca657989578fa55abf061713f56727938ebd3f255999613
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log