llm-input-hardening
The input boundary: what text LLMs and agents are allowed to read. Part of the Forge
*-hardeningfamily, alongsideagent-api-hardening(capability boundary) andsafegit(recovery boundary).
Deterministic Unicode-aware input hardening for LLM applications.
llm-input-hardening sits between untrusted text and your prompt or template
layer. It bounds Unicode normalization, removes invisible or control characters
that destabilize prompts, emits structured reports for logging and enforcement,
and detects mixed- and whole-script confusables.
It is not prompt injection prevention. It addresses text-integrity problems, not model intent.
This README is the main entry point and quickstart. Deeper references — the full
report schema, policy details, threat model, integrations, and evaluation
methodology — live under docs/.
The hardening contract defines the guarantees, deliberate policy limits, and regression strategy across stages.
Install
pip install llm-input-hardening
Optional extras:
pip install "llm-input-hardening[security]"for theconfusable_homoglyphsbackendpip install "llm-input-hardening[ftfy]"for pre-sanitize text repair
Determinism note: output is reproducible for pinned code and dependency
versions. use_ftfy=True inspects the original input before running
ftfy.fix_text(normalization=None) and sanitizing the repaired text. Rust owns
bounded normalization, including when repair is enabled. Reports retain pre-repair
security evidence; ftfy version changes can still change output and reports.
Import from llm_input_hardening.
Quickstart
from llm_input_hardening import sanitize
clean_text, report = sanitize(user_text, policy="balanced_chat")
Compact demo/report output:
from llm_input_hardening import compact_report, sanitize
clean_text, report = sanitize(user_text, policy="balanced_chat")
summary = compact_report(report, sanitized_text=clean_text)
Recursive JSON sanitization:
from llm_input_hardening import sanitize_json
clean_payload, report = sanitize_json(payload, policy="balanced_chat")
Enforcement-oriented usage:
from llm_input_hardening import sanitize_and_decide
clean_text, report, decision = sanitize_and_decide(
user_text,
sanitize_policy="strict_exec",
confusables_backend="confusable_homoglyphs",
)
Execution fields need their own schema after normalization. For example, require an unchanged identifier from a known set:
clean_text, report, decision = sanitize_and_decide(
tool_name,
sanitize_policy="strict_exec",
reject_on_change=True,
validator=lambda value: value in {"search", "summarize"},
)
if decision["action"] != "allow":
raise ValueError("tool name rejected")
validator always sees the returned text; validation errors propagate. Paths
need application-specific root/allowlist checks, and code still needs parsing
and sandboxing. strict_exec by itself does not authorize execution.
If your application joins untrusted fragments, inspect the final assembly:
from llm_input_hardening import sanitize_assembled
clean_text, report, decision = sanitize_assembled(
retrieved_fragments, separator="\n\n", sanitize_policy="balanced_chat",
)
if decision["action"] != "allow":
raise ValueError("assembled content requires review")
The helper joins raw fragments before inspection, with budgets for total characters and part count. Individually allowed fields do not authorize a later concatenation. Keep trusted message roles and instructions outside this helper.
The text API accepts at most 1,000,000 input characters by default (configurable
with max_input_chars). Requested spans are limited to 10,000 events. JSON
traversal also bounds nodes, total text, and report size; exceeding a limit
raises ValueError instead of returning a partially processed result.
For ASGI applications, use ASGIPromptSanitizerMiddleware with explicit protected
routes and selectors such as messages[*].content and
messages[*].content[*].text. With enforce=True, uninspected requests and
quarantine/reject decisions are blocked by default. See
integration configuration.
Policies
| Policy | Normalization | Default-Ignorables | Whitespace tidy | Typical use |
|---|---|---|---|---|
preserve |
NFC | preserve | no | low-friction logging and observability |
balanced_chat |
NFC | preserve | yes | general chat and prompt input |
strict_exec |
NFKC | strip | yes | tool arguments and execution-adjacent paths |
code_mode |
NFC | strip | no | code snippets and syntax-sensitive text |
Backward-compatible aliases are still accepted: balanced, strict, and code.
Scope
Use this library for:
- user text before prompt interpolation
- retrieved text before it reaches the model
- tool and function argument strings
- JSON payloads with untrusted string keys or values
Do not use it as a substitute for:
- system prompt design
- tool sandboxing and allowlists
- output validation
- human review for high-risk actions
How It Fits With Guardrails And Red-Teams
llm-input-hardening is a text-integrity layer. It is designed to run before,
inside, or alongside larger guardrail systems.
| Tool family | What it is good at | How this library fits |
|---|---|---|
| NVIDIA NeMo Guardrails | Orchestrating input, output, retrieval, dialog, and tool rails; running content safety, PII, jailbreak, and model/policy checks. | Use this first in an input, retrieval, or tool-input rail when you want deterministic Unicode cleanup and structured text-integrity reports before semantic checks. |
| Promptfoo red teaming | Generating and evaluating adversarial test cases against your LLM app. | Use promptfoo to test the app. Use this library as one defensive layer, then measure overlap with promptfoo static obfuscation strategies. |
| Model-based prompt-injection detectors | Semantic classification of jailbreak or prompt-injection intent. | Complementary. This library does not infer intent; it makes suspicious text properties visible and policy-decidable. |
NeMo Guardrails exposes input/retrieval/tool rails, including jailbreak and safety flows. Promptfoo exposes red-team strategy transforms such as Base64, Hex, Homoglyph, and Emoji Smuggling. This package covers the deterministic text integrity part of that space; it should not be described as a replacement for either tool.
Useful references:
- NeMo Guardrails rails configuration
- NeMo Guardrails library integrations
- Promptfoo red-team configuration
- Promptfoo multi-input red teaming
NeMo Guardrails Mapping
NeMo Guardrails is the orchestration layer. llm-input-hardening is a
deterministic text preflight layer you can call before a NeMo flow, from a
custom rail, or before handing text to a downstream model/API rail.
| NeMo area | Example checks or integrations | What this library adds |
|---|---|---|
| Input rails | self check input, content safety, jailbreak heuristics/model, LlamaGuard, PII detection/masking | Normalizes/removes Unicode ambiguity before the rail sees text; emits report fields you can log or route on. |
| Retrieval rails | sensitive-data checks, retrieved-document policies | Sanitizes untrusted retrieved text before prompt assembly and records which sources carried obfuscation signals. |
| Tool input rails | action/tool argument validation | Applies strict_exec to execution-adjacent strings before tool calls. |
| Third-party guardrail integrations | GuardrailsAI, Prompt Security, Pangea AI Guard, Cisco AI Defense, and similar providers | Complements semantic/API checks with local deterministic cleanup and low-latency text-integrity telemetry. |
| Jailbreak detection heuristics/model | perplexity-style heuristics or model/NIM-based jailbreak detection | Different layer. This library does not decide semantic jailbreak intent; it removes and flags obfuscation that can distort prompt/log/tool text. |
Promptfoo Static Obfuscation Strategy Mapping
This is not a certification claim or a claim that this package passes promptfoo's full red-team suite. Treat it as a starting map for overlap with promptfoo static obfuscation strategies.
| Promptfoo strategy | This library behavior | Notes |
|---|---|---|
| Homoglyph | Flags mixed-script and whole-script confusables, including all-Cyrillic words that fold to ASCII-Latin skeletons. | Execution policies reject. Chat quarantines isolated/Latin-context or all-confusable text; native-prose candidates remain observable without automatic quarantine. The optional backend adds broader data. |
| Emoji Smuggling | strict_exec strips default-ignorable characters such as emoji variation selectors and ZWJ chains. balanced_chat preserves normal emoji behavior but still reports default-ignorable signals. |
Use strict_exec for tool args and execution-adjacent strings; use balanced_chat for normal chat UX. |
| Base64 | Flags base64-shaped blobs. | It does not decode the blob or classify decoded intent. |
| Hex / alt encodings | Flags contiguous hex, promptfoo-style space-separated byte hex, ascii85/base85, and long binary-byte strings. | It does not decode the payload or classify decoded intent. |
| Indirect prompt injection | Helps clean and report Unicode/control-character obfuscation in retrieved text before prompt assembly. | It does not decide whether visible natural-language instructions are malicious. |
| Multi-input red teaming | Sanitizes JSON string keys and values with sanitize_json() and rejects sanitized-key collisions. |
It does not reason about cross-field authorization or role confusion. |
See examples/promptfoo-redteam-overlap/ for a small overlap harness that
imports promptfoo's built-in static strategy transforms and classifies which
generated payloads this package catches. The checked smoke fixture currently
catches base64, hex, homoglyph, and emoji.
Homoglyphs And Invisible Text, Briefly
Some attacks do not rely on clever prose. They rely on text that humans and machines see differently.
| Pattern | What happens | Example shape | What this library does |
|---|---|---|---|
| Bidi controls | Text can render in a different visual order than its logical order. | abc\u202Edef |
Removes bidi controls and reports bidi_control. |
| Zero-width characters | Text can contain hidden separators or payload markers. | pay\u200bload |
Removes known junk invisibles; strict policies strip broader default-ignorables. |
| Homoglyphs | Characters from different scripts can look like familiar Latin text. | раypal using Cyrillic р and а |
Flags mixed-script confusables; enforcement can reject. |
| Whole-script homoglyphs | Every letter can be replaced while the word remains single-script. | all-Cyrillic раураӏ |
Flags whole_script_confusable; stores the ASCII-Latin skeleton. |
| Styled forms | Compatibility letters or digits can look like ordinary ASCII. | mathematical bold, circled, small-caps, fullwidth 100 |
Flags confusable_styled; strict_exec may also fold forms via NFKC. |
| Normalization amplification | One compatibility character can expand to many normalized characters. | repeated U+FDFA |
Streams normalization into an 8x/minimum-128 cap and rejects if truncated. |
| Emoji variation selectors / joiners | Emoji can carry invisible formatting characters or joiner chains. | heart variation selector, family ZWJ sequence | balanced_chat preserves normal emoji UX; strict_exec strips default-ignorables. |
| Encoding-shaped blobs | Payloads can be hidden in encoded-looking strings. | long base64, hex, ascii85, binary-byte strings | Flags the shape; does not decode or classify intent. |
The mental model:
- Normalize text by policy.
- Remove characters that make prompt/log/tool text ambiguous.
- Flag suspicious shapes that should affect logging or enforcement.
- Keep semantic safety checks separate.
CLI
llm-input-hardening sanitize --text "abcdef"
llm-input-hardening inspect --text "abcdef"
llm-input-hardening inspect --compact --text "abcdef"
llm-input-hardening decide --text "abcdef"
llm-input-hardening decide --compact --text "abcdef"
Development
uv sync --all-extras
uv run maturin develop --release
uv run python -m pytest -q
Benchmarks And Bakeoff
The repo has two different evaluation paths.
bench/run_benchmarks.pyis a timing harness. It measures sanitizer latency and throughput on fixed synthetic inputs.bench/run_bakeoff.pyis an evaluation harness. It runs labeled corpus cases, checks expected detections and postconditions, and computes precision, recall, benign-change rate, and stability signals.
Use the Make targets below:
make bench
make bench-plots
make bakeoff-smoke
make bakeoff-smoke-assert
make bakeoff-full
make bakeoff-full-assert
make bakeoff-diff
make bakeoff-diff-assert
What they mean:
bakeoff-smokeis the fast local check. It evaluates onlyllm-input-hardeningand writes results tobench/results/bakeoff_smoke_latest.json.bakeoff-fullis the heavyweight comparison run. It installsllm-guardandconfusable-homoglyphs, then writes comparable results tobench/results/bakeoff_latest.json.bakeoff-diffcompares the current full run to a reviewed schema-4 baseline (bench/results/bakeoff_baseline_schema4.json, orBAKEOFF_BASELINE=...). Establish that baseline after reviewing a full run; historical schema-2 results are intentionally not comparable. See baseline migration.
Generated reports stay under bench/results/.
Repo Notes
CONTRIBUTING.mdcovers contribution mechanics.SECURITY.mdcovers reporting and threat assumptions.SCOPE.mdexplains what the project is and is not trying to solve.
Metadata
Release files for llm-input-hardening 2.0.0
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_input_hardening-2.0.0.tar.gz | 68.4 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| llm_input_hardening-2.0.0-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| llm_input_hardening-2.0.0-cp310-abi3-musllinux_1_2_x86_64.whl | CPython 3.10 | abi3 | Linux musl 1.2+ x86-64 | Details |
| llm_input_hardening-2.0.0-cp310-abi3-musllinux_1_2_aarch64.whl | CPython 3.10 | abi3 | Linux musl 1.2+ ARM64 | Details |
| llm_input_hardening-2.0.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| llm_input_hardening-2.0.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| llm_input_hardening-2.0.0-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| llm_input_hardening-2.0.0-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 3.5 MB
Release files / llm_input_hardening-2.0.0.tar.gz
| Download URL | llm_input_hardening-2.0.0.tar.gz |
|---|---|
| Size | 68.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
28409d7695ad21014b649e4d79f74e629db6a85ca0d8d3f7a192633dbd9aba19
|
|
BLAKE2b-256 checksum How to use checksums |
abc3932371f5fdc6f69fe13c307a2950aa59bfe6d3ef723ab079bdfc89c2d898
|
| 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 Sep 6, 2026.
Transparency logRelease files / llm_input_hardening-2.0.0-cp310-abi3-win_amd64.whl
| Download URL | llm_input_hardening-2.0.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 326.1 kB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
8c6839a7b09c805109a9ff6a018c8f8da0ae6524b4cd7ede1945f6f063fbd8fe
|
|
BLAKE2b-256 checksum How to use checksums |
bc0ca224a9f212b84c05dfcf1d1a655cac7fb01693d60c626a738181f081d6dd
|
| 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 Sep 6, 2026.
Transparency logRelease files / llm_input_hardening-2.0.0-cp310-abi3-musllinux_1_2_x86_64.whl
| Download URL | llm_input_hardening-2.0.0-cp310-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 678.8 kB |
| Tags | CPython 3.10 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
bd39d8756a647c6e83f90b0dfb7ee91f5a5ace1097d096cabc4c1d117777f036
|
|
BLAKE2b-256 checksum How to use checksums |
ff855a5bc23e72a3e9c8a3902c5be6490bea8a7cc080df67f91738d996617522
|
| 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 Sep 6, 2026.
Transparency logRelease files / llm_input_hardening-2.0.0-cp310-abi3-musllinux_1_2_aarch64.whl
| Download URL | llm_input_hardening-2.0.0-cp310-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 642.1 kB |
| Tags | CPython 3.10 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
1589d78d41419fd1c1daa6d3bcc86710915b5f477aeb85608dda4d17eb46a81d
|
|
BLAKE2b-256 checksum How to use checksums |
f898f261c4d7a06d8feca7d90cfbcdf56ce8a63721a8a537bfeef62ad98dd2be
|
| 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 Sep 6, 2026.
Transparency logRelease files / llm_input_hardening-2.0.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | llm_input_hardening-2.0.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 477.3 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
71c997ef79d205d20f9f07f8c5d052d42be995cf1b424aab3536c993dbf7f899
|
|
BLAKE2b-256 checksum How to use checksums |
4479b5d7c56b7c882f163db49d0bcdaae445333beac91575f66759bb299d044f
|
| 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 Sep 6, 2026.
Transparency logRelease files / llm_input_hardening-2.0.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | llm_input_hardening-2.0.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 466.0 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
04eaf97d691702912348b34171cc6a92770000c7ded84a06c1d15d417c547c05
|
|
BLAKE2b-256 checksum How to use checksums |
ba44677101ae52b2986d963d57ec29b9a7e57bcddee1d33afd6d7c7f0f2c9729
|
| 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 Sep 6, 2026.
Transparency logRelease files / llm_input_hardening-2.0.0-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | llm_input_hardening-2.0.0-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 428.3 kB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
aef829633d190c91cdeb35aa8ff9e20e4bd784c204dee87ed4522361f069869c
|
|
BLAKE2b-256 checksum How to use checksums |
3ad9f0a0b751c28291c92d60318f895f6e3190cbe82e14336cb5f1c1fc519764
|
| 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 Sep 6, 2026.
Transparency logRelease files / llm_input_hardening-2.0.0-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | llm_input_hardening-2.0.0-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 434.2 kB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
b22e89f6b4e12ace2c57bb1d90c0733faf9ff45b4cce36fa21d645fffa865dc4
|
|
BLAKE2b-256 checksum How to use checksums |
2df1bd3d9dc7d2d7a2f5e3b330035cd42396dd55fc322982c5acd4bfb20b1d4e
|
| 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 Sep 6, 2026.
Transparency log