A local-first redaction layer between developers and LLMs. Paste code, configs, logs or a
whole repository, and RedactAI replaces the credentials and sensitive data in it with typed
placeholders before anything is sent to an LLM, then puts the real values back into the reply.
STRIPE_SECRET_KEY = stays; the key after it doesn't.
The hard part isn't finding secrets; it's not breaking the code around them. A redactor that
blanks a connection string, a YAML key or a port number leaves the model unable to answer the
question it was asked. RedactAI removes only the sensitive part of each finding (the password
inside a URL, the token after Bearer), so the output stays valid code with every name, host
and line of logic intact.
▶ Open the workbench: paste a file or upload a folder, pick a redaction level, press Redact. The hosted engine runs the code lane and every pattern tier; the NER models and the local LLM need a GPU and run locally.
A line from a real service, and the finding the engine reports for it (developer profile,
trimmed):
DB_URL = "postgresql://svc_reports:[email protected]:5432/reports"
# becomes
DB_URL = "postgresql://svc_reports:[PASSWORD_1]@db.internal:5432/reports"
{
"entity_type": "PASSWORD",
"severity": "CRITICAL",
"action": "PSEUDONYMIZE",
"placeholder": "[PASSWORD_1]",
"detectors": [{
"detector_id": "embedded.url_userinfo",
"confidence": 0.95,
"evidence": "embedded credential (url_userinfo)"
}],
"replacement": "[PASSWORD_1]"
}
// db.internal is found too (INTERNAL_HOSTNAME, score 0.85) and kept on purpose:
// the developer profile keeps hosts so questions about the code stay answerable.
Every finding is attributable: it names the detector that fired, the evidence, the policy decision and why. The same secret gets the same placeholder in every file of a session, and an encrypted per-session vault restores real values in the model's reply, so a suggested patch applies as written.
redactai redact settings.py # sanitized file on stdout, risk on stderr
redactai redact-tree ./my-service --out ./redacted # a whole repo, mirrored, + findings.json
redactai scan app.log # what would be redacted, and why
redactai serve --open # workbench + OpenAI-compatible proxy on :8420
Point any OpenAI client at http://127.0.0.1:8420/v1 and every prompt is redacted before it
leaves, streaming included.
Secrets in code (model-free tier, developer profile). Measured on corpora written by
authors who could not see the detector: 52 files across 14 languages and 26 infra/tooling
formats, 235 planted secrets, split into dev and holdout. False positives are measured on
~800 KB of real open-source code with nothing planted in it.
| Before | Dev | Holdout | |
|---|---|---|---|
| Planted secrets redacted | 92.3% | 98.2% | 98.5% (94.0% on the blind first run) |
| Files left unparseable by redaction | 6 / 14 | 0 / 14 | 0 / 4 |
| Names, hosts, digests & references kept | 80.0% | 98.2% | 95.8% |
| False positives, real OSS code | 0.48 / 1k chars | 0.028 / 1k | — |
Sensitive data in prose: 2,000-case acceptance gate (13.1 h, 0 errors), five of five rows pass.
| Metric | Combined | Required | |
|---|---|---|---|
Recall on CRITICAL types | 1.0000 hand-labelled / 0.9650 synthetic | ≥ 0.99 on labelled gold | PASS |
Recall on HIGH types | 0.9715 (0.9497 under the old rule) | ≥ 0.95 | PASS |
| Type-agnostic recall | 0.9684 | ≥ 0.93 | PASS |
| False positives on negative cases | 1.82 / 1k chars | ≤ 2 | PASS |
| Semantic fidelity | 0.8604 | ≥ 0.85 | PASS |
Honest caveats. The holdout was inspected after its first run to find failure patterns,
so only the 94.0% figure is blind; both are shown. Most of the HIGH recall movement is a
scoring amendment (D-37 in ARCHITECTURE.md), not a detector fix. The
prose gate predates the code lane and has not been re-run since. The remaining dev miss is a
password written inside an English sentence, which is the local model's job.
flowchart LR
subgraph A["A · Detect (high recall)"]
A0[S0 · normalize + segment<br/>code · config · prose] --> A1[S1 · deterministic<br/>code lane · 196 vendor rules · validators]
A0 --> A2[S2 · NER ensemble<br/>GLiNER2 · GLiNER · spaCy]
A0 --> A3[S3 · local LLM<br/>quasi-IDs · confidential data]
end
subgraph B["B · Decide (precision)"]
B1[S4 · merge + propagate<br/>+ coreference] --> B2[S5 · adjudicate<br/>19 rule vetoes · local judge]
B2 --> B3[S6 · policy<br/>profile · level · regime packs]
end
subgraph C["C · Transform + guard"]
C1[S7 · transform<br/>AES-GCM vault] --> C2[S8 · verify + risk score]
C2 --> C3[S9 · egress guard<br/>hard tripwire]
end
A1 --> B1
A2 --> B1
A3 --> B1
B3 --> C1
C3 --> D[["LLM provider"]]
D --> E[["S10 · rehydrate reply + audit"]]
The accuracy machinery, which is most of the engineering:
process.env.X ?? "…", SQL
INSERT columns, Postman {key, value} records. Comments and code inside strings are read too.DB_PASSWORD_FILE=/run/secrets/db and
GITHUB_TOKEN: ${GITHUB_TOKEN} stay; a random value under a meaningless name goes. Randomness
is measured (entropy, class switching, letter-bigram spellability), not listed.Bearer, only the value of --requirepass.ssh deploy@bastion is not an email, and a symbol the file declares is a name.developer / balanced / strict / permissive profiles, a redaction
level (which data categories to act on), and GDPR, HIPAA, PCI-DSS, CCPA and corporate packs
that can only tighten, never loosen.Full design rationale in ARCHITECTURE.md; day-by-day decisions in
CHANGELOG.md; the specs everything was built from in plans/.
| Layer | Choice | Why |
|---|---|---|
| Code lane | Hand-written multi-dialect lexer + binding analysis | No parser dependency; tolerant of partial, pasted code |
| Deterministic | Regex + validators (Luhn, IBAN, libphonenumber, python-stdnum) | Checksums turn a pattern match into a proof |
| NER | GLiNER2 (fastino/gliner2-privacy-filter-PII-multi), GLiNER, spaCy en_core_web_trf | Ensemble for recall on names and addresses in prose |
| LLM | Qwen3.5 9B / 4B via Ollama, schema-constrained, temperature=0 | Local only; context-dependent calls without egress |
| ML runtime | PyTorch + CUDA 12.8, VRAM-sequential loading | Fits the full stack on an 8 GB card |
| API | FastAPI + Uvicorn, OpenAI-compatible proxy | Drop-in for any OpenAI client, streaming rehydration |
| Crypto | cryptography: AES-GCM vault, HMAC-SHA256 audit chain | Values recoverable per session, never stored in logs |
| Frontend | Next.js 16 static export, TypeScript | No server, no secrets, no CDN |
| Deploy | Vercel (site + torch-free Python function), Docker | The pattern tiers run anywhere; the models run locally |
| Quality | pytest (1,843), strict mypy, ruff, Playwright e2e, GitHub Actions | Hermetic suite; a test fails if detection opens a socket |
~20k lines of Python across the pipeline, detectors, API and CLI; ~5k of TypeScript in the workbench.
Requires Python 3.12.
git clone https://github.com/Jaden-Varkey/redact-ai.git && cd redact-ai
./scripts/setup.sh # setup.ps1 on Windows
./scripts/pull_models.sh # GLiNER + spaCy + Qwen
python scripts/doctor.py # checks GPU, models and providers
redactai serve --open # workbench + proxy on http://127.0.0.1:8420
No GPU? The code lane and every pattern tier run anywhere:
REDACTAI_CONFIG_FILE=config/demo.yaml redactai serve
# or: docker build -f deploy/demo/Dockerfile -t redactai . && docker run -p 8420:8420 redactai
Choosing a redaction level (the workbench picker, or categories on the API):
| Level | Redacts |
|---|---|
| Secrets only | API keys, passwords, tokens, private keys, connection strings |
| Secrets + personal data | also emails, phone numbers, addresses, card and ID numbers |
| Thorough (default) | also health, network, sensitive traits and company data; hosts stay readable |
| Maximum | everything, including internal hosts, on weaker evidence |
Nothing leaves the machine unless you send it: detection is local, log scrubbing cannot be turned off, and file names are segmentation hints that are never stored.
"We keep keys in .env" covers one file. The rest of what gets pasted into an assistant:
| You paste | What's in it |
|---|---|
| A file for "explain this" / review | hardcoded fallbacks getenv("KEY", "sk_live_…"), test fixtures, commented-out rotated keys |
| Logs and stack traces | Authorization: Bearer …, session cookies, tokens in URLs, customer emails and IPs |
| CI, Docker, Kubernetes, Helm, Terraform | run: steps with --password, base64 Secrets, terraform.tfstate plaintext attributes |
| API collections (Postman, Insomnia, HAR) | bearer tokens, client secrets, cookies as header records |
| SQL dumps, seed data, notebooks | user rows, password hashes, credentials in cell output |
| A PR diff or a whole service | the old key the diff removes, the same key in three files |
A secret written in plain English ("the bootstrap value is bluebird-rotates-77") needs the local model. English/US locale for sensitive data in prose. Images, PDFs and audio are out of scope. RedactAI reduces risk; it does not guarantee that nothing sensitive ever reaches a third party.
uv sync --extra dev
uv run pytest -m "not gpu and not ollama and not network" # hermetic suite, ~2 min
uv run pytest -m gpu # needs CUDA
uv run ruff check . && uv run mypy src/redactai
cd web-demo && npm test && npm run e2e # unit tests + browser e2e
Heavy ML dependencies are imported lazily, so the hermetic suite runs without models, a GPU or
Ollama, and an autouse fixture fails any test that tries to reach the network. CI runs lint,
formatting, strict mypy, the hermetic suite and a dependency audit on every push to main.
src/redactai/
├── stages/ s0 normalize · s1 deterministic · s2 NER · s3 LLM · s4 merge · s5 adjudicate
│ s6 policy · s7 transform · s8 verify · s9 egress guard
├── detectors/ code lexer · code judge · secret values · embedded · structured · validators
├── pipeline/ engine: single documents, file sets, batches
├── api/ FastAPI routes · OpenAI-compatible proxy · schemas
├── vault/ AES-GCM session vault · rehydration
├── audit/ HMAC-chained audit log
├── providers/ downstream LLM adapters
└── cli/ redact · redact-tree · scan · serve
web-demo/ Next.js workbench (static export)
A local-first redaction layer between developers and LLMs. Paste code, configs, logs or a
whole repository, and RedactAI replaces the credentials and sensitive data in it with typed
placeholders before anything is sent to an LLM, then puts the real values back into the reply.
STRIPE_SECRET_KEY = stays; the key after it doesn't.
The hard part isn't finding secrets; it's not breaking the code around them. A redactor that
blanks a connection string, a YAML key or a port number leaves the model unable to answer the
question it was asked. RedactAI removes only the sensitive part of each finding (the password
inside a URL, the token after Bearer), so the output stays valid code with every name, host
and line of logic intact.
▶ Open the workbench: paste a file or upload a folder, pick a redaction level, press Redact. The hosted engine runs the code lane and every pattern tier; the NER models and the local LLM need a GPU and run locally.
A line from a real service, and the finding the engine reports for it (developer profile,
trimmed):
DB_URL = "postgresql://svc_reports:[email protected]:5432/reports"
# becomes
DB_URL = "postgresql://svc_reports:[PASSWORD_1]@db.internal:5432/reports"
{
"entity_type": "PASSWORD",
"severity": "CRITICAL",
"action": "PSEUDONYMIZE",
"placeholder": "[PASSWORD_1]",
"detectors": [{
"detector_id": "embedded.url_userinfo",
"confidence": 0.95,
"evidence": "embedded credential (url_userinfo)"
}],
"replacement": "[PASSWORD_1]"
}
// db.internal is found too (INTERNAL_HOSTNAME, score 0.85) and kept on purpose:
// the developer profile keeps hosts so questions about the code stay answerable.
Every finding is attributable: it names the detector that fired, the evidence, the policy decision and why. The same secret gets the same placeholder in every file of a session, and an encrypted per-session vault restores real values in the model's reply, so a suggested patch applies as written.
redactai redact settings.py # sanitized file on stdout, risk on stderr
redactai redact-tree ./my-service --out ./redacted # a whole repo, mirrored, + findings.json
redactai scan app.log # what would be redacted, and why
redactai serve --open # workbench + OpenAI-compatible proxy on :8420
Point any OpenAI client at http://127.0.0.1:8420/v1 and every prompt is redacted before it
leaves, streaming included.
Secrets in code (model-free tier, developer profile). Measured on corpora written by
authors who could not see the detector: 52 files across 14 languages and 26 infra/tooling
formats, 235 planted secrets, split into dev and holdout. False positives are measured on
~800 KB of real open-source code with nothing planted in it.
| Before | Dev | Holdout | |
|---|---|---|---|
| Planted secrets redacted | 92.3% | 98.2% | 98.5% (94.0% on the blind first run) |
| Files left unparseable by redaction | 6 / 14 | 0 / 14 | 0 / 4 |
| Names, hosts, digests & references kept | 80.0% | 98.2% | 95.8% |
| False positives, real OSS code | 0.48 / 1k chars | 0.028 / 1k | — |
Sensitive data in prose: 2,000-case acceptance gate (13.1 h, 0 errors), five of five rows pass.
| Metric | Combined | Required | |
|---|---|---|---|
Recall on CRITICAL types | 1.0000 hand-labelled / 0.9650 synthetic | ≥ 0.99 on labelled gold | PASS |
Recall on HIGH types | 0.9715 (0.9497 under the old rule) | ≥ 0.95 | PASS |
| Type-agnostic recall | 0.9684 | ≥ 0.93 | PASS |
| False positives on negative cases | 1.82 / 1k chars | ≤ 2 | PASS |
| Semantic fidelity | 0.8604 | ≥ 0.85 | PASS |
Honest caveats. The holdout was inspected after its first run to find failure patterns,
so only the 94.0% figure is blind; both are shown. Most of the HIGH recall movement is a
scoring amendment (D-37 in ARCHITECTURE.md), not a detector fix. The
prose gate predates the code lane and has not been re-run since. The remaining dev miss is a
password written inside an English sentence, which is the local model's job.
flowchart LR
subgraph A["A · Detect (high recall)"]
A0[S0 · normalize + segment<br/>code · config · prose] --> A1[S1 · deterministic<br/>code lane · 196 vendor rules · validators]
A0 --> A2[S2 · NER ensemble<br/>GLiNER2 · GLiNER · spaCy]
A0 --> A3[S3 · local LLM<br/>quasi-IDs · confidential data]
end
subgraph B["B · Decide (precision)"]
B1[S4 · merge + propagate<br/>+ coreference] --> B2[S5 · adjudicate<br/>19 rule vetoes · local judge]
B2 --> B3[S6 · policy<br/>profile · level · regime packs]
end
subgraph C["C · Transform + guard"]
C1[S7 · transform<br/>AES-GCM vault] --> C2[S8 · verify + risk score]
C2 --> C3[S9 · egress guard<br/>hard tripwire]
end
A1 --> B1
A2 --> B1
A3 --> B1
B3 --> C1
C3 --> D[["LLM provider"]]
D --> E[["S10 · rehydrate reply + audit"]]
The accuracy machinery, which is most of the engineering:
process.env.X ?? "…", SQL
INSERT columns, Postman {key, value} records. Comments and code inside strings are read too.DB_PASSWORD_FILE=/run/secrets/db and
GITHUB_TOKEN: ${GITHUB_TOKEN} stay; a random value under a meaningless name goes. Randomness
is measured (entropy, class switching, letter-bigram spellability), not listed.Bearer, only the value of --requirepass.ssh deploy@bastion is not an email, and a symbol the file declares is a name.developer / balanced / strict / permissive profiles, a redaction
level (which data categories to act on), and GDPR, HIPAA, PCI-DSS, CCPA and corporate packs
that can only tighten, never loosen.Full design rationale in ARCHITECTURE.md; day-by-day decisions in
CHANGELOG.md; the specs everything was built from in plans/.
| Layer | Choice | Why |
|---|---|---|
| Code lane | Hand-written multi-dialect lexer + binding analysis | No parser dependency; tolerant of partial, pasted code |
| Deterministic | Regex + validators (Luhn, IBAN, libphonenumber, python-stdnum) | Checksums turn a pattern match into a proof |
| NER | GLiNER2 (fastino/gliner2-privacy-filter-PII-multi), GLiNER, spaCy en_core_web_trf | Ensemble for recall on names and addresses in prose |
| LLM | Qwen3.5 9B / 4B via Ollama, schema-constrained, temperature=0 | Local only; context-dependent calls without egress |
| ML runtime | PyTorch + CUDA 12.8, VRAM-sequential loading | Fits the full stack on an 8 GB card |
| API | FastAPI + Uvicorn, OpenAI-compatible proxy | Drop-in for any OpenAI client, streaming rehydration |
| Crypto | cryptography: AES-GCM vault, HMAC-SHA256 audit chain | Values recoverable per session, never stored in logs |
| Frontend | Next.js 16 static export, TypeScript | No server, no secrets, no CDN |
| Deploy | Vercel (site + torch-free Python function), Docker | The pattern tiers run anywhere; the models run locally |
| Quality | pytest (1,843), strict mypy, ruff, Playwright e2e, GitHub Actions | Hermetic suite; a test fails if detection opens a socket |
~20k lines of Python across the pipeline, detectors, API and CLI; ~5k of TypeScript in the workbench.
Requires Python 3.12.
git clone https://github.com/Jaden-Varkey/redact-ai.git && cd redact-ai
./scripts/setup.sh # setup.ps1 on Windows
./scripts/pull_models.sh # GLiNER + spaCy + Qwen
python scripts/doctor.py # checks GPU, models and providers
redactai serve --open # workbench + proxy on http://127.0.0.1:8420
No GPU? The code lane and every pattern tier run anywhere:
REDACTAI_CONFIG_FILE=config/demo.yaml redactai serve
# or: docker build -f deploy/demo/Dockerfile -t redactai . && docker run -p 8420:8420 redactai
Choosing a redaction level (the workbench picker, or categories on the API):
| Level | Redacts |
|---|---|
| Secrets only | API keys, passwords, tokens, private keys, connection strings |
| Secrets + personal data | also emails, phone numbers, addresses, card and ID numbers |
| Thorough (default) | also health, network, sensitive traits and company data; hosts stay readable |
| Maximum | everything, including internal hosts, on weaker evidence |
Nothing leaves the machine unless you send it: detection is local, log scrubbing cannot be turned off, and file names are segmentation hints that are never stored.
"We keep keys in .env" covers one file. The rest of what gets pasted into an assistant:
| You paste | What's in it |
|---|---|
| A file for "explain this" / review | hardcoded fallbacks getenv("KEY", "sk_live_…"), test fixtures, commented-out rotated keys |
| Logs and stack traces | Authorization: Bearer …, session cookies, tokens in URLs, customer emails and IPs |
| CI, Docker, Kubernetes, Helm, Terraform | run: steps with --password, base64 Secrets, terraform.tfstate plaintext attributes |
| API collections (Postman, Insomnia, HAR) | bearer tokens, client secrets, cookies as header records |
| SQL dumps, seed data, notebooks | user rows, password hashes, credentials in cell output |
| A PR diff or a whole service | the old key the diff removes, the same key in three files |
A secret written in plain English ("the bootstrap value is bluebird-rotates-77") needs the local model. English/US locale for sensitive data in prose. Images, PDFs and audio are out of scope. RedactAI reduces risk; it does not guarantee that nothing sensitive ever reaches a third party.
uv sync --extra dev
uv run pytest -m "not gpu and not ollama and not network" # hermetic suite, ~2 min
uv run pytest -m gpu # needs CUDA
uv run ruff check . && uv run mypy src/redactai
cd web-demo && npm test && npm run e2e # unit tests + browser e2e
Heavy ML dependencies are imported lazily, so the hermetic suite runs without models, a GPU or
Ollama, and an autouse fixture fails any test that tries to reach the network. CI runs lint,
formatting, strict mypy, the hermetic suite and a dependency audit on every push to main.
src/redactai/
├── stages/ s0 normalize · s1 deterministic · s2 NER · s3 LLM · s4 merge · s5 adjudicate
│ s6 policy · s7 transform · s8 verify · s9 egress guard
├── detectors/ code lexer · code judge · secret values · embedded · structured · validators
├── pipeline/ engine: single documents, file sets, batches
├── api/ FastAPI routes · OpenAI-compatible proxy · schemas
├── vault/ AES-GCM session vault · rehydration
├── audit/ HMAC-chained audit log
├── providers/ downstream LLM adapters
└── cli/ redact · redact-tree · scan · serve
web-demo/ Next.js workbench (static export)