This project welcomes contributions from both humans and LLM coding assistants. This document is written to be useful to both.
llm_context_shield is a Rust CLI tool that scans text for LLM context injection threats. The scanner design is intentionally modular: each threat category is an isolated file implementing a single Scanner trait. Adding new detection capability should not require touching existing code.
- Create
src/scanners/<name>.rsfollowing the pattern of any existing scanner (e.g.prompt_injection.rs). - Implement the
Scannertrait:pub struct MyScanner { inner: RegexScanner } impl Scanner for MyScanner { fn name(&self) -> &'static str { "my_scanner" } fn scan(&self, input: &str) -> Vec<Finding> { self.inner.scan(input) } }
- Add a
Categoryvariant to theCategoryenum insrc/scanner.rsand update itsDisplayimpl. - Register it in
src/scanners/mod.rs::build(). - Add unit tests in
#[cfg(test)] mod testsat the bottom of your new file. Each test should:- Use a real malicious payload as input, not a synthetic always-true string.
- Assert on both presence of findings and their severity.
- Include at least one
clean_text_no_findingstest to guard against false positives.
Pattern files are in src/scanners/*.rs. Each scanner holds a Vec<(Regex, Severity, &'static str)>. To add a new pattern:
- Add a
(Regex::new(r"...").unwrap(), Severity::X, "description")tuple. - Note: the
regexcrate does not support look-ahead or look-behind. Use alternatives (e.g. match a broader pattern and accept some false positives, or split into two patterns). - Run
cargo testand verify your new pattern is covered by a unit test.
| Severity | Use when |
|---|---|
Critical |
Unambiguous, high-confidence attack with direct impact (e.g. explicit instruction override, ChatML token injection) |
High |
Strong signal, low false-positive risk (e.g. identity reassignment, system prompt extraction) |
Medium |
Moderate signal or context-dependent (e.g. authority keyword directives, URL-encoded markdown images) |
Low |
Weak signal, potentially noisy — use sparingly |
cargo check # fast syntax/type check
cargo test # run all unit + integration tests
cargo clippy # lint — zero warnings expected
cargo fmt # formatAll PRs must pass cargo test and cargo clippy with zero warnings.
src/
main.rs Entry point: parse CLI, read input, run scanners, report
cli.rs Clap arg definitions
input.rs Read from stdin/file, normalize (BOM, line endings)
scanner.rs Scanner trait, Finding/Severity/Category types, RegexScanner helper
report.rs Output formatting (json / text / quiet)
scanners/
mod.rs Registry: build() -> Vec<Box<dyn Scanner>>
*.rs One file per threat category
tests/
integration.rs Full CLI pipeline tests using assert_cmd
Key design decisions:
- Scanners are trait objects (
Box<dyn Scanner>). The allocation overhead is negligible for a CLI tool. - Regex patterns are compiled once in
new()and reused across scans. - Severity filtering happens at report time, not scan time — all findings are always collected.
- Structured output goes to stdout; diagnostic text goes to stderr. This preserves stdout for piping.
- Read
src/scanner.rsfirst — it defines all shared types. - The
RegexScannerhelper insrc/scanner.rseliminates boilerplate; use it for regex-based scanners. - Do not use look-ahead (
(?=...)) or look-behind ((?<=...)) — theregexcrate does not support them. - Prefer broader patterns with clearly documented scope over narrow patterns that miss variants.
- Keep each scanner file self-contained: patterns, struct,
Scannerimpl, and#[cfg(test)]all in one file. - After any change:
cargo test && cargo clippymust both pass clean before the work is done.