Jaden-Varkey/redact-ai

Python

1

46 commits

updated Sep 24, 2026

See the code

README

RedactAI

CI Python Tests Types License Live

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.


What it produces

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.


Results

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.

BeforeDevHoldout
Planted secrets redacted92.3%98.2%98.5% (94.0% on the blind first run)
Files left unparseable by redaction6 / 140 / 140 / 4
Names, hosts, digests & references kept80.0%98.2%95.8%
False positives, real OSS code0.48 / 1k chars0.028 / 1k—

Sensitive data in prose: 2,000-case acceptance gate (13.1 h, 0 errors), five of five rows pass.

MetricCombinedRequired
Recall on CRITICAL types1.0000 hand-labelled / 0.9650 synthetic≥ 0.99 on labelled goldPASS
Recall on HIGH types0.9715 (0.9497 under the old rule)≥ 0.95PASS
Type-agnostic recall0.9684≥ 0.93PASS
False positives on negative cases1.82 / 1k chars≤ 2PASS
Semantic fidelity0.8604≥ 0.85PASS

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.


How it works

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:

  • Code lane. A hand-written lexer for ~20 language families finds every literal and what the program binds it to: assignments, object keys, kwargs, process.env.X ?? "…", SQL INSERT columns, Postman {key, value} records. Comments and code inside strings are read too.
  • Name and value judged together. 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.
  • Surgical scope. Only the password inside a connection string, only the token after Bearer, only the value of --requirepass.
  • Vetoes that protect the file. A YAML key is never redacted, a port is not a ZIP code, ssh deploy@bastion is not an email, and a symbol the file declares is a name.
  • Local judge for the undecidable band. A 32-char hex under a neutral name is a key or a checksum; a local model reads the line. It refuses any non-local client.
  • Policy as data. 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.
  • Egress guard. The last check before dispatch verifies that no vault value, or any fragment of one, is present in the outgoing payload.

Full design rationale in ARCHITECTURE.md; day-by-day decisions in CHANGELOG.md; the specs everything was built from in plans/.


Tech stack

LayerChoiceWhy
Code laneHand-written multi-dialect lexer + binding analysisNo parser dependency; tolerant of partial, pasted code
DeterministicRegex + validators (Luhn, IBAN, libphonenumber, python-stdnum)Checksums turn a pattern match into a proof
NERGLiNER2 (fastino/gliner2-privacy-filter-PII-multi), GLiNER, spaCy en_core_web_trfEnsemble for recall on names and addresses in prose
LLMQwen3.5 9B / 4B via Ollama, schema-constrained, temperature=0Local only; context-dependent calls without egress
ML runtimePyTorch + CUDA 12.8, VRAM-sequential loadingFits the full stack on an 8 GB card
APIFastAPI + Uvicorn, OpenAI-compatible proxyDrop-in for any OpenAI client, streaming rehydration
Cryptocryptography: AES-GCM vault, HMAC-SHA256 audit chainValues recoverable per session, never stored in logs
FrontendNext.js 16 static export, TypeScriptNo server, no secrets, no CDN
DeployVercel (site + torch-free Python function), DockerThe pattern tiers run anywhere; the models run locally
Qualitypytest (1,843), strict mypy, ruff, Playwright e2e, GitHub ActionsHermetic 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.


Quickstart

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):

LevelRedacts
Secrets onlyAPI keys, passwords, tokens, private keys, connection strings
Secrets + personal dataalso emails, phone numbers, addresses, card and ID numbers
Thorough (default)also health, network, sensitive traits and company data; hosts stay readable
Maximumeverything, 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.


Where secrets actually live

"We keep keys in .env" covers one file. The rest of what gets pasted into an assistant:

You pasteWhat's in it
A file for "explain this" / reviewhardcoded fallbacks getenv("KEY", "sk_live_…"), test fixtures, commented-out rotated keys
Logs and stack tracesAuthorization: Bearer …, session cookies, tokens in URLs, customer emails and IPs
CI, Docker, Kubernetes, Helm, Terraformrun: 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, notebooksuser rows, password hashes, credentials in cell output
A PR diff or a whole servicethe 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.


Development

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)

Jaden-Varkey/redact-ai

Python

1

46 commits

updated Sep 24, 2026

See the code

README

RedactAI

CI Python Tests Types License Live

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.


What it produces

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.


Results

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.

BeforeDevHoldout
Planted secrets redacted92.3%98.2%98.5% (94.0% on the blind first run)
Files left unparseable by redaction6 / 140 / 140 / 4
Names, hosts, digests & references kept80.0%98.2%95.8%
False positives, real OSS code0.48 / 1k chars0.028 / 1k—

Sensitive data in prose: 2,000-case acceptance gate (13.1 h, 0 errors), five of five rows pass.

MetricCombinedRequired
Recall on CRITICAL types1.0000 hand-labelled / 0.9650 synthetic≥ 0.99 on labelled goldPASS
Recall on HIGH types0.9715 (0.9497 under the old rule)≥ 0.95PASS
Type-agnostic recall0.9684≥ 0.93PASS
False positives on negative cases1.82 / 1k chars≤ 2PASS
Semantic fidelity0.8604≥ 0.85PASS

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.


How it works

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:

  • Code lane. A hand-written lexer for ~20 language families finds every literal and what the program binds it to: assignments, object keys, kwargs, process.env.X ?? "…", SQL INSERT columns, Postman {key, value} records. Comments and code inside strings are read too.
  • Name and value judged together. 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.
  • Surgical scope. Only the password inside a connection string, only the token after Bearer, only the value of --requirepass.
  • Vetoes that protect the file. A YAML key is never redacted, a port is not a ZIP code, ssh deploy@bastion is not an email, and a symbol the file declares is a name.
  • Local judge for the undecidable band. A 32-char hex under a neutral name is a key or a checksum; a local model reads the line. It refuses any non-local client.
  • Policy as data. 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.
  • Egress guard. The last check before dispatch verifies that no vault value, or any fragment of one, is present in the outgoing payload.

Full design rationale in ARCHITECTURE.md; day-by-day decisions in CHANGELOG.md; the specs everything was built from in plans/.


Tech stack

LayerChoiceWhy
Code laneHand-written multi-dialect lexer + binding analysisNo parser dependency; tolerant of partial, pasted code
DeterministicRegex + validators (Luhn, IBAN, libphonenumber, python-stdnum)Checksums turn a pattern match into a proof
NERGLiNER2 (fastino/gliner2-privacy-filter-PII-multi), GLiNER, spaCy en_core_web_trfEnsemble for recall on names and addresses in prose
LLMQwen3.5 9B / 4B via Ollama, schema-constrained, temperature=0Local only; context-dependent calls without egress
ML runtimePyTorch + CUDA 12.8, VRAM-sequential loadingFits the full stack on an 8 GB card
APIFastAPI + Uvicorn, OpenAI-compatible proxyDrop-in for any OpenAI client, streaming rehydration
Cryptocryptography: AES-GCM vault, HMAC-SHA256 audit chainValues recoverable per session, never stored in logs
FrontendNext.js 16 static export, TypeScriptNo server, no secrets, no CDN
DeployVercel (site + torch-free Python function), DockerThe pattern tiers run anywhere; the models run locally
Qualitypytest (1,843), strict mypy, ruff, Playwright e2e, GitHub ActionsHermetic 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.


Quickstart

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):

LevelRedacts
Secrets onlyAPI keys, passwords, tokens, private keys, connection strings
Secrets + personal dataalso emails, phone numbers, addresses, card and ID numbers
Thorough (default)also health, network, sensitive traits and company data; hosts stay readable
Maximumeverything, 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.


Where secrets actually live

"We keep keys in .env" covers one file. The rest of what gets pasted into an assistant:

You pasteWhat's in it
A file for "explain this" / reviewhardcoded fallbacks getenv("KEY", "sk_live_…"), test fixtures, commented-out rotated keys
Logs and stack tracesAuthorization: Bearer …, session cookies, tokens in URLs, customer emails and IPs
CI, Docker, Kubernetes, Helm, Terraformrun: 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, notebooksuser rows, password hashes, credentials in cell output
A PR diff or a whole servicethe 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.


Development

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)