Veil 推出本地優先的 AI 文字脱敏工具
Veil: a local-first redaction gate for everything you paste into an AI
作者推出 Veil,一款置於剪貼簿與 AI 聊天框之間的本地優先脱敏工具,會在文字送出前找出憑證及上下文敏感資訊,並以穩定佔位符取代。工具提供 CLI、本機網頁介面及 Node 函式庫,結合 18 條確定性規則、熵值掃描和 Gemma 建議,未能逐字匹配原文的模型引述會被丟棄。
Last week I pasted a production database URL into a chat window.
Nothing happened. That is exactly the problem. The paste box sits one keystroke away from the stack trace, the config file, the support ticket with a customer's name in it. Every AI chat UI warns you not to paste secrets, and then puts the box right there.
I built Veil to sit between the clipboard and the chat box. It runs on your machine, finds the credentials and the quieter contextual leaks, and replaces them with stable placeholders before anything is sent.
What I built
Veil is a local-first redaction gate with three surfaces:
- a CLI (
veil scan,veil redact,veil doctor) - a local web UI on
127.0.0.1:4317 - a library you can call from Node (
scan(text, { client, useModel }))
It catches AWS keys, GitHub/Slack/Stripe/Google/OpenAI tokens, JWTs, PEM blocks, database URLs with inline passwords, card numbers (Luhn-checked), emails, phone numbers, private IPs, and internal hostnames. Then an open-weight Gemma model reads for the things regex cannot see: a customer's name, an internal codename, the sentence that says the document is under NDA.
Everything is MIT licensed, has zero runtime dependencies, and the model is swappable through VEIL_MODEL.
The demo
Here is a fake support ticket with a fake production config in it:
Customer: Priya Raman at Northwind Traders.
Contact: priya.raman@northwind-traders.com / +1 415 555 0132
Internal project: HALCYON-7
Staging host: billing-api.corp.internal
AWS_ACCESS_KEY_ID=AKIA...
DATABASE_URL=postgres://veil_app:hunter2isbad@db-prod-01.corp.internal:5432/billing
This ticket is under NDA with Northwind Traders.
And here is what comes out the other side:
Customer: [PERSON_1] at [CUSTOMER_1].
Contact: [EMAIL_1] / [PHONE_1]
Internal project: [PROJECT_1]
Staging host: [HOST_1]
AWS_ACCESS_KEY_ID=[AWS_KEY_1]
DATABASE_URL=[DATABASE_URL_1]
[CONFIDENTIAL_1]. Do not forward it outside the
migration channel.
The redacted version still reads like a document. You can tell that two lines mention the same value, because the same literal always gets the same placeholder. You cannot tell what the value was.
Run it yourself:
git clone https://github.com/shreya-oeo/veil
cd veil
npm test
npm run fixtures
node src/cli.js scan fixtures/support-ticket.md # rules only, ~3 ms
node src/cli.js redact fixtures/support-ticket.md --model --model-name gemma4:cloud
The CLI is pipe-friendly, so it also works as a gate:
git diff | veil redact --model > safe.diff
kubectl logs api-0 | veil scan --json --fail-on high
How it works
The architecture follows one rule: a redaction tool must be correct, not creative.
flowchart LR
A[Input] --> B[18 deterministic rules]
A --> C[Entropy scan]
A --> D[Gemma via Ollama]
B --> E[Merge spans]
C --> E
D -->|quote must exist verbatim| E
E --> F[Stable placeholders]Deterministic rules run first. A regex finds AKIA... with certainty. A small model does not. The model should never be asked to do a job that string matching already does perfectly.
Entropy catches the shape you did not write a rule for. A 32-character token with 5.1 bits of entropy per character, sitting next to SECRET=, is a credential even when no vendor prefix matches. Git SHAs, SHA-256 digests, and base64 images are allowlisted so the scan does not cry wolf.
The model runs second, and only suggests. It returns quotes. Every quote is checked character-for-character against the source:
const index = text.indexOf(quote);
if (index === -1) {
dropped += 1;
continue;
}
If the model paraphrases, fixes a typo, or invents a plausible-looking secret, the span is discarded and counted. The UI shows the count. On this fixture the model returned four quotes and all four survived, but the number exists precisely because it will not always be zero.
Overlapping spans merge before anything is replaced. An AWS secret on an env line trips both a regex and the entropy scan; a JWT inside a URL trips two more. Replacing spans one at a time shifts every offset after the first, so Veil merges them first and keeps the most severe label. Model quotes that merely repeat a precise rule match are dropped, so an email stays [EMAIL_1] instead of becoming a vaguer [REDACTED_1].
The part I got wrong first
My first version let the model's findings override the rules. The result was worse, not better: Gemma would return "Person name" for an email address, the merge would pick the higher severity, and the output would turn a precise [EMAIL_1] into a vague [REDACTED_1].
The fix was structural, not prompt-level. Rules own the categories they can prove. The model only gets to add spans the rules cannot see. That is a small change in the merge logic and a large change in how much I trust the output.
Why open innovation matters here
A redaction tool that uploads your text to a closed API to ask whether the text is sensitive has already lost the argument. The whole point is to keep the data on the machine that already has it.
That constraint is what made the design better:
-
The trust boundary is inspectable. You can read all 18 rules. You can point
OLLAMA_HOSTat a box you control. There is no account, no telemetry, and no per-token bill for a tool that should run on every paste. - The model is a component, not the product. Gemma is doing one narrow job: copy the fragment that names a person or a private project. Swapping it for a smaller or larger model changes recall and latency, not correctness, because the verification step does not trust either one blindly.
- Open weights make the cost structure sane. A privacy check that costs money per scan is a privacy check people turn off. This one is free after the download.
- The rules are the community's. Every detector in the repo is a pattern someone else already learned the hard way. MIT licensing means the next person can add the token format their employer uses without waiting for a vendor.
The closed alternative is not just more expensive. It is structurally unable to make the promise Veil makes: your text did not leave this laptop.
What it does not do
I would rather be explicit about this than have someone trust it too much.
- Text only. No screenshots or OCR yet. That is the obvious next step and a different failure surface.
- Small models miss context. The local 1B–4B range is good at names and codenames, not at reading a whole contract. Veil truncates the model pass at 6,000 characters and says so.
- Novel formats need new rules. Entropy is a fallback, not a substitute for a rule someone has not written yet.
- It cannot unsend a paste. It only gates the next one.
- Passing the scan is not legal advice. If you are under NDA, read the agreement.
The tool also reports residual risk it cannot fix: local filesystem paths that reveal a username, health or identity terms, and documents marked confidential all produce a warning even when nothing matched.
Notes on the build
- 18 deterministic rules, a Shannon-entropy pass, and a verbatim-verification layer.
- 34 tests on
node --test, including the two failures that matter: an invented quote and a quote that differs by one space. - Zero runtime dependencies. Node's built-in
http,fs, andfetchcover the whole surface. The UI is one HTML file with no CDN, no font request, and no analytics. - The demo runs Gemma through Ollama's cloud tag because my connection would have taken an hour to pull a local tag. The code path is identical: set
VEIL_MODEL=gemma3:4b(or1b) and the same pipeline runs fully offline. The UI labels cloud-routed models instead of pretending they are local. - The fixtures are generated at runtime from fragments, because committing files full of credential-shaped strings is a good way to get your repo blocked by GitHub's push protection. I know, because that is what happened on the first push.
What's next
OCR for screenshots, a pre-commit hook, and an editor extension that redacts on paste. The core is small enough that each of those is a surface, not a rewrite.
Code: https://github.com/shreya-oeo/veil
Run it: git clone, npm test, node src/cli.js serve
Built for the Hacktoberfest Open-Source AI Challenge. Entering Best Use of Gemma — the model pass is what turns a regex filter into something that catches a customer's name and an NDA sentence, not just a key.
來源:DEV Community · dev.to
