This project exists for research purposes: to make incident post-mortems safe to feed into AI systems (LLMs, embeddings, fine-tuning datasets, retrieval pipelines) without leaking the sensitive information they typically contain.
Incident reports mix valuable operational signal — what broke, how it was diagnosed, what fixed it — with personal and confidential data: names, emails, phone numbers, IPs, customer identifiers, internal hostnames, secrets. The pipeline takes raw incident JSON/JSONL and produces a redacted (or pseudonymized) copy plus an audit trail describing what was changed and why, so the post-mortem narrative stays useful for analysis while the sensitive fields are removed. Sample data uses the Rootly export shape; other shapes can be adapted by adjusting the field-extraction logic.
This is not a compliance tool. It is intended to support research and experimentation on incident data — please review outputs before sharing them externally.
Each incident flows through seven stages:
- Policy load — read the JSON policy that defines which categories are PII and which action to take (redact, pseudonymize, keep).
- Deterministic extraction — find obvious PII with regex, Presidio, and spaCy (emails, phones, SSNs, IPs, names).
- Contextual detection — the current finder uses enhanced rules and simulated LLM analysis to suggest additional candidates. The configured OpenAI finder model is not called by this stage yet.
- LLM verification — a "judge" model (Anthropic
claude-sonnet-4-6by default) re-checks each candidate against the policy. - Arbitration & redaction — conflicting decisions are resolved, then the text is rewritten with redactions or pseudonyms.
- Quality validation — pattern-based scan of the redacted text for residual PII and schema integrity issues; produces
quality_metrics. - LLM final review — the judge model checks the redacted text for remaining identifiers, including contextual re-identification. Long text is reviewed in overlapping windows so the tail is covered too. This stage uses one call per window, runs last, and skips in simulation mode or when no API key is set. An unexpected skip is flagged for human review. Disable explicitly with
--skip-final-review.
Simulation mode (--llm-simulation) still runs the rule-based finder and simulated judge, while skipping real API calls and the final review. Models are configurable in config/llm_models.json.
Each incident is processed with separate detector, judge, arbitration, and validation state. The parallel path keeps a long incident intact so identifiers cannot be split by fixed character boundaries. Validation issues and incomplete final reviews are included in the human-review decision.
The pipeline targets the categories defined in config/policies/default_policy.json. Out of the box, that includes:
- Personal identifiers — full names, email addresses, phone numbers, postal addresses
- Government/financial IDs — SSNs, credit card numbers, bank account numbers
- Network identifiers — IP addresses, MAC addresses, hostnames that map to individuals
- Account data — usernames, customer IDs, employee IDs
- Credentials — API keys, tokens, and other secrets that surface in incident notes
Each category has a configurable action — REDACT replaces the value with a placeholder (e.g. [REDACTED_EMAIL]), PSEUDONYMIZE swaps in a stable fake value so downstream analysis still works. Edit the policy JSON to add categories or change actions.
Some strings the detectors flag as PII are actually operationally useful and shouldn't be redacted — region codes (us-east-1), internal hostnames (kafka-broker-3.us-east-1), service identifiers, well-known infra names. The allowlist is a user-controlled override applied during arbitration.
The list lives in config/allowlist.json and has two fields:
literals— case-insensitive exact-string match. Use for things likeus-east-1,eu-central-1.regex_patterns— Pythonre.searchsemantics. Use for parameterized names like^kafka-broker-\d+(\.[a-z0-9-]+)*$.
When a detected entity matches any allowlisted pattern, the arbitration step overrides its action to RETAIN and records the matched pattern in the audit trail so it shows up in arbitration.json and the per-incident report.
CLI control:
- Default: the bundled
config/allowlist.jsonis loaded automatically. --allowlist path/to/custom.json— use a different file (e.g. per-environment).--no-allowlist— disable entirely; every detection goes through normal redaction.
Warning: the allowlist overrides redaction. Never put anything that could be PII (names, emails, SSNs, customer IDs) in it. The default file ships with public AWS region codes and a regex for kafka-broker-* / redis-cache-* as starter examples; trim or extend it for your environment.
Automated redaction is not infallible, so each incident is scored against a confidence threshold (default 0.7, override with --confidence-threshold). Any incident whose overall_quality_score (or sub-metric: precision, recall, F1) falls below that threshold is flagged for human review.
Three signals can flip the flag:
- Quality score below threshold — the validator's
overall_quality_score(or any ofprecision/recall/f1_score) falls below the threshold. - LLM final review verdict — stage 7 returns
is_clean: falsewith a list of suspicious snippets. - Validation or review coverage — residual PII, schema violations, high-severity issues, or an unexpected final-review failure also require review.
Any of these signals sets needs_review: true.
Where the flag shows up:
- Per-incident report (
incident_<id>_detailed_report.json) — top-levelhuman_reviewblock withneeds_review,quality_score, the failing metrics, the LLM verdict, and areasonline. The full LLM final-review payload (issues + reasoning) is in the siblingfinal_reviewblock. - Overall summary (
overall_summary.json) —human_review.incidents_needing_review_count, list of incident IDs, and a separatefinal_review_summaryshowing how many incidents the LLM flagged. - Console output —
👀 Needs Human Review:and🔎 LLM Final Review:lines.
A reviewer should diff the original vs processed text for those incidents, focus on entities the LLM flagged, and either accept the output, edit it, or tighten the policy and re-run.
- English only. Presidio uses an English spaCy model; non-English incidents will have lower recall.
- Schema-tuned. Field extraction is tuned to the included sample shape (Rootly export). Other shapes need an adapter in
src/data_collection/. - LLM-dependent quality. The judge and final review depend on the configured model, prompt, and policy. Simulation mode does not provide an independent LLM check for contextual PII.
- Finder is simulated. Stage 3 currently uses enhanced rules and simulated span analysis even when real APIs are enabled; the configured finder model is not invoked.
- Quality metrics are heuristics. The reported precision and recall are estimated from pipeline decisions and residual checks, without labeled ground truth.
- Pseudonyms are deterministic. The same input produces the same pseudonym across runs. These values are not anonymization and may be guessable for common names; treat them as sensitive.
- Audit files contain originals.
processing_results.json, detailed reports, and stage audits include unredacted source text. Keep the output directory private and share only reviewed processed text. - No guarantee of zero residual PII. The quality validator flags issues but cannot certify a redacted output is clean — always review high-sensitivity outputs manually.
- API cost scales with volume when using real LLM calls; benchmark with
--llm-simulationfirst.
git clone https://github.com/Rootly-AI-Labs/incident-data-cleaner.git
cd incident-data-cleaner
pip install -r requirements.txt
pip install -e .
python -m spacy download en_core_web_lg# Process a JSONL file of incidents (no API calls)
python process_incidents.py data/test_samples/rootly_samples.jsonl --llm-simulation
# Real LLM calls — requires API keys in env
export OPENAI_API_KEY=...
export ANTHROPIC_API_KEY=...
python process_incidents.py data/test_samples/rootly_samples.jsonl --max-concurrent 5Run python process_incidents.py --help for all flags. Results land in output/<file>_processing_<timestamp>/.
- Redaction policy:
config/policies/default_policy.jsondefines categories, sensitivity levels, and actions. Pass a custom one with--policy path/to/policy.json. - LLM models:
config/llm_models.jsonselects the configured finder/judge models. The current finder stage is simulated; the judge and final reviewer useclaude-sonnet-4-6(Anthropic) by default. The judge key comes fromANTHROPIC_API_KEY. - Confidence threshold:
--confidence-threshold 0.7(default) controls when an incident is flagged for human review.
A SQLite store for tracking processed incidents is available via db_cli.py (load, process, list, get, stats). See DATABASE_MVP_README.md.
make test-allMIT — see LICENSE.