# Sether > Sether provides local privacy tools built on free MIT PII-redaction libraries by Raeven Company LTD. It detects personal data (emails, phone numbers, payment cards, IBANs, names, API keys and other secrets) in text, replaces each value with a random token or a realistic decoy before the text reaches an LLM, and restores the original values in the model's reply. Processing runs locally in your process, browser or editor. Pricing in one line: full npm and PyPI libraries stay free under MIT, including decoy and restore, with no Sether usage quota. Official tools offer Free scan, mask, token replacement and all restore of known mappings. New licensed releases add optional Sether Pro: realistic decoys and custom watchlists, plus custom rules in Shield, bundled for one person across Shield and the editor. The Pro pilot is $5 for one month or $48 for one year, manual renewal with no automatic charge. Install a compatible build before explicitly starting the seven-day, no-card trial. Expiry returns to Free and does not lock existing known-mapping restore. Current store builds may retain legacy free features. Hosted Gateway is separate early access and not generally available. ## Products - [Sether Shield](https://sether.ai/shield): Local prompt scanning, masking, token replacement and known-mapping restore. New official releases add optional Pro decoys, custom watchlists and rules. Current store builds may retain legacy free features. Status: Live on the Chrome Web Store. Price: Free + optional Pro. - [Sether for VS Code](https://sether.ai/vscode): Local findings, masks, token replacement and restore for AI coding workflows. New official releases add Pro decoys, custom watchlists in the same user bundle as Shield. The editor source remains MIT licensed. Status: Live on the VS Code Marketplace. Price: Free + optional Pro. Install: `code --install-extension GodfreyLebo.sether`. - [Sether Hooks](https://sether.ai/hooks): Stops a prompt that carries personal data or a secret before Claude Code, Codex or Gemini CLI sends it to the model, and shows a clean version to send instead. Runs locally with no network calls. Status: Coming soon. Price: Free · MIT. - [Sether for TypeScript](https://sether.ai/docs): Streaming redact and restore between your app and any LLM. Chunk-boundary safe, SSE aware, with fetch, Express, OpenAI and Anthropic wrappers. New in 0.8.0: JSON-safe restore for tool-call arguments. Status: Live on npm. Price: Free · MIT. Install: `npm install @raeven-co/sether`. - [Sether for Python](https://sether.ai/docs/python): The same engine and token format as the npm package, with sync and async streaming plus ASGI, WSGI, httpx, OpenAI and Anthropic helpers. New in 0.4.0: JSON-safe restore and decoy mode. Status: Live on PyPI. Price: Free · MIT. Install: `pip install sether`. - [sether-ner](https://sether.ai/docs#ner): Local named-entity recognition for names, organisations and locations in plain sentences. Runs a small model in your process, no API calls. Status: Live on npm. Price: Free · MIT. Install: `npm install @raeven-co/sether-ner @huggingface/transformers`. - [Sether for n8n](https://github.com/raeven-co/n8n-nodes-sether): Redact, Restore and Validate Tool Call nodes. Keep customer data tokenised through an AI workflow and block tool calls that try to use values they were never given. Status: Beta · install from GitHub. Price: Free · MIT. - [Sether Gateway Guard](https://github.com/raeven-co/sether-gateway-guard): An OpenAI-compatible hop you run yourself. Tokenises every message, restores replies and gates tool calls with a default-deny policy file. Status: Beta · self-host from GitHub. Price: Free · MIT. - [Sether Gateway (hosted)](https://sether.ai/pricing): A proposed managed redaction and restore endpoint for teams that prefer hosted infrastructure. Separate from the finished-tool Pro bundle; not generally available. Status: Early access. Price: Separate early access. ## Key facts - npm package: @raeven-co/sether, version 0.8.0, 252 tests, 224 detection patterns all checked for ReDoS, 1 runtime dependency. - PyPI package: sether, version 0.4.0. Same engine and token format as the npm package. - Developer-library license: full MIT. Source: https://github.com/raeven-co/sether. The editor source also remains MIT; official-tool Pro licensing does not change these grants. - Token format: , for example . Each match gets a random token, and only your vault can map it back to the original. - Decoy mode replaces supported values with realistic stand-ins, using test ranges where available. Restore requires the original mapping. Decoy mode is included in the free MIT libraries and is a Pro feature in new licensed official tools. - Privacy: prompt, reply, file and mapping processing stays local in the tools. New licensed clients contact Sether for account and license metadata, not prompts, mappings or watchlist contents. Demo use and account creation do not start a trial. - Certifications: none today. Sether does not hold a SOC 2 report. - Limitations: detection covers supported types, not every private detail; names in plain sentences need sether-ner; Shield runs in desktop Chrome only; restore needs the matching mapping. Full list: https://sether.ai/limitations - Founder: Godfrey Lebo (https://sether.ai/about). Company: Raeven Company LTD, Abuja, Nigeria. ## Pages - [Product overview](https://sether.ai): What Sether is, how a prompt is redacted and restored, every product, pricing and FAQ - [Sether Shield for Chrome](https://sether.ai/shield): Local browser privacy tools: Free essentials, optional Pro in new releases; current store builds may retain legacy free features - [Sether for VS Code](https://sether.ai/vscode): Editor extension that flags secrets and copies scrubbed snippets for AI assistants, live on the VS Code Marketplace - [Pricing](https://sether.ai/pricing): Full MIT libraries stay free; official tools have Free essentials and optional Pro, with a seven-day opt-in pilot trial and manual renewal - [TypeScript docs](https://sether.ai/docs): Install, quickstart, detectors, decoy mode, streaming, middlewares and audit events for @raeven-co/sether - [Python docs](https://sether.ai/docs/python): pip install sether: sync and async streaming, ASGI, WSGI, httpx, OpenAI and Anthropic helpers - [Blog](https://sether.ai/blog): Guides and comparisons on PII redaction for LLM applications - [Sether Hooks (coming soon)](https://sether.ai/hooks): Local prompt hook that stops prompts with personal data or secrets before Claude Code, Codex or Gemini CLI sends them - [News](https://sether.ai/news): Research, analysis and release announcements, labelled by type - [Roadmap](https://sether.ai/roadmap): What shipped recently, what is in testing, what comes next, and what is not planned, with reasons - [Limitations](https://sether.ai/limitations): What Sether does not catch or promise today, by product - [About and team](https://sether.ai/about): Who builds Sether, what we believe, and how to partner with us - [Editorial policy](https://sether.ai/editorial-policy): How articles are labelled, checked, disclosed and corrected - [Live sandbox](https://sether.ai/sandbox): Browser demo of redaction, no signup, nothing leaves the device - [Server playground](https://sether.ai/playground): Round trip through the real npm package on our server - [Demo decks](https://sether.ai/demos): Interactive slide decks on GDPR, redaction and streaming - [Privacy](https://sether.ai/privacy): How this site handles personal data - [Terms](https://sether.ai/terms): Terms of use ## Roadmap (last reviewed 2026-10-25) - Shipped recently: Shield 0.5.3: fixes for ChatGPT's new composer; JSON-safe restore in TypeScript 0.8 and Python 0.4; Sether for VS Code on the Marketplace; sether-ner, early release; n8n node and Gateway Guard, in beta. - Building now: Sether Hooks for Claude Code, Codex and Gemini CLI; Pro pilot builds of Shield and VS Code; African national ID numbers. - Next: Vault adapters for Redis and Postgres; Published benchmarks against Microsoft Presidio; Unicode-aware email detection; sether-ner from early release to stable. - Later: Hosted gateway: stored audit trail, SIEM export and compliance reports. - Not planned, and why: Intercepting your network traffic; "Zero-knowledge" claims; Guessing traits from a name; Rewriting your prompt with another AI; A phone app, for now. - Details: https://sether.ai/roadmap ## Articles - [A quote in a name could add a bcc to an AI tool call. Here is the fix in Sether 0.8.0.](https://sether.ai/blog/sether-0-8-0-json-safe-restore): Sether 0.8.0 fixes JSON restore injection paths. See the affected scope, JSON-aware APIs, strict mode, runnable examples and upgrade limits. Markdown: https://sether.ai/blog/sether-0-8-0-json-safe-restore.md - [A quote in a name could add a bcc to an AI tool call. Here is the fix in Sether for Python 0.4.0.](https://sether.ai/blog/sether-python-0-4-0-json-safe-restore): Sether Python 0.4.0 adds JSON-safe restore and strict token handling. Review the fix, runnable examples and structured-stream upgrade limits. Markdown: https://sether.ai/blog/sether-python-0-4-0-json-safe-restore.md - [PII redaction for LLM applications: the complete guide (2026)](https://sether.ai/blog/pii-redaction-for-llm-apps-complete-guide): Design an LLM redaction flow: detection, local mappings, reply restore, streaming limits and practical acceptance checks for your application. Markdown: https://sether.ai/blog/pii-redaction-for-llm-apps-complete-guide.md - [How to redact PII in JSON and structured data before sending it to an LLM](https://sether.ai/blog/redact-pii-in-json-before-llm): Redact supported personal values in JSON, verify syntax and coverage, then restore replies safely with JSON-aware APIs in TypeScript and Python. Markdown: https://sether.ai/blog/redact-pii-in-json-before-llm.md - [Tokenize PII before AI document generation: keep real customer data out of the model](https://sether.ai/blog/tokenize-pii-before-ai-document-generation): Separate AI drafting from sensitive field insertion. Use typed replacements, validate the draft and restore known mappings at document rendering. Markdown: https://sether.ai/blog/tokenize-pii-before-ai-document-generation.md - [How to redact PII before sending prompts to OpenAI (TypeScript)](https://sether.ai/blog/redact-pii-before-openai-typescript): Integrate Sether in a TypeScript LLM request path. Test detection, vault sharing, streaming boundaries and JSON-aware reply restoration. Markdown: https://sether.ai/blog/redact-pii-before-openai-typescript.md - [How to redact PII from LLM prompts in Python](https://sether.ai/blog/redact-pii-llm-prompts-python): Build a Python redact-and-restore flow with Sether. Check vault sharing, supported wrappers, streaming behavior and JSON-safe restoration. Markdown: https://sether.ai/blog/redact-pii-llm-prompts-python.md - [Is it GDPR-compliant to send customer data to OpenAI?](https://sether.ai/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai): An engineering guide to reviewing LLM data flows: GDPR scope, provider roles, retention, minimisation and evidence for qualified privacy advice. Markdown: https://sether.ai/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai.md - [Microsoft Presidio alternatives for Node.js and TypeScript (2026)](https://sether.ai/blog/lightweight-presidio-alternatives-typescript): Compare Presidio and Sether integration choices: detection fixtures, reversible operations, streaming, state and deployment boundaries. Markdown: https://sether.ai/blog/lightweight-presidio-alternatives-typescript.md - [Reversible PII redaction: how redact-then-restore works](https://sether.ai/blog/reversible-pii-redaction-redact-restore): How typed tokens and a retained vault support reply restoration, with practical checks for expired mappings, JSON output and tool authorization. Markdown: https://sether.ai/blog/reversible-pii-redaction-redact-restore.md - [How to stop pasting sensitive data into ChatGPT (without banning it)](https://sether.ai/blog/stop-pasting-sensitive-data-into-chatgpt): Review sensitive drafts with Sether Shield: local text processing, Free essentials, optional Pro workflows and the limits of browser-side controls. Markdown: https://sether.ai/blog/stop-pasting-sensitive-data-into-chatgpt.md - [The chunk-boundary bug: why streaming PII redaction is hard](https://sether.ai/blog/streaming-llm-redaction-chunk-boundary-bug): Why per-chunk scanning misses split values, how bounded buffering helps and which encoding, transport and long-input cases to test. Markdown: https://sether.ai/blog/streaming-llm-redaction-chunk-boundary-bug.md - [Regex vs NER for PII detection: when patterns aren't enough](https://sether.ai/blog/regex-vs-ner-pii-detection): Evaluate patterns, labels, known terms and NER for PII detection. Build representative fixtures and review local model deployment boundaries. Markdown: https://sether.ai/blog/regex-vs-ner-pii-detection.md - [How I took my npm package's supply-chain score from 78 to zero alerts](https://sether.ai/blog/npm-supply-chain-zero-alerts-socket): A June 2026 Sether hardening account: dependency provenance, scanner limits, SDK wrapper types and practical checks for the release you deploy. Markdown: https://sether.ai/blog/npm-supply-chain-zero-alerts-socket.md ## Optional - Full article text follows below. - [Sitemap](https://sether.ai/sitemap.xml) - [RSS feed](https://sether.ai/blog/feed.xml) Not product documentation: /admin, /account, /api, /cards, /clips, /pitch, /pay. --- Source: https://sether.ai/blog/sether-0-8-0-json-safe-restore # A quote in a name could add a bcc to an AI tool call. Here is the fix in Sether 0.8.0. > Sether 0.8.0 on npm fixes a bug where restoring tokens into serialized JSON as plain text put original values back unescaped, so a value containing a quote could add keys to LLM tool-call arguments. The new restoreJson and restoreJsonValue helpers restore each string value on its own and re-escape it, strict mode refuses tokens it cannot resolve, and Sether's own SSE, fetch, Express and SDK-wrapper paths now restore JSON value by value. Published 2026-10-17, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - Restoring tokens into serialized JSON as plain text put original values back unescaped. An original holding a `"` could add keys, for example a `bcc` in an email tool call. - `@raeven-co/sether` 0.8.0 adds `restoreJson` and `restoreJsonValue`, which restore each string value on its own and let the serializer escape it. Object keys are never restored. - Strict mode (`strict: true`) throws `UnresolvedTokenError` instead of silently leaving invented, expired or altered tokens in place. - Sether's own SSE, fetch, Express and SDK-wrapper restore paths now restore JSON value by value, and the Express middleware no longer lets a client-sent `"__proto__"` key become the body's prototype. - `restoreSync` behaves as before. If you restore JSON yourself, switch to `restoreJson` after upgrading. On 14 October I was building an n8n node for Sether. Its job is to check a model's proposed tool call before any real value goes back into it. So I was looking hard at the moment the real values go back in. That is where I found the bug. Sether put originals back into JSON as plain text. If an original value held a quote, it could rewrite the JSON around it. Nobody reported this to me. I found it in my own library, and it was in Sether's own middleware too, not only in code that called it. It is fixed in `@raeven-co/sether` 0.8.0 and in the Python package, `sether` 0.4.0. The advisory is here: [the Sether security advisories on GitHub](https://github.com/raeven-co/sether/security/advisories). This post covers what Sether is, what went wrong, what changed, and how to upgrade. ## Sether in one picture ![Your app and Sether sit inside your process with the vault. The prompt "Email alice@example.com" leaves as "Email ". The LLM provider only sees tokens. The reply "Sent to " comes back and Sether restores it to "Sent to alice@example.com".](https://sether.ai/blog/img/sether-release-one-picture.png) Sether is an open-source library that sits between your app and any LLM. Before a prompt leaves your process, it swaps personal data for tokens shaped like ``. By default it catches emails, phone numbers written with a country code, card numbers, US Social Security numbers, IBANs and IP addresses. Opt-in packs add secrets such as API keys, and labelled identity fields such as `Name:`. For detected values, the edited request contains tokens instead of those originals; unmatched text and identifying context can remain. Recognised reply tokens restore only while their mapping is available. The default vault stays in process, and the core package does not phone home. Keep the full data flow in view, including your own logs and stores. ```bash npm install @raeven-co/sether ``` ## The bug: restoring into JSON as text Restoring sounds like the easy half. Find each token in the reply, look it up in the vault, and put the original in its place. For a chat reply in plain prose, that is exactly right. But models don't only write prose. They write tool calls, and tool-call arguments are serialized JSON: ```json {"to":"","subject":"Hello "} ``` Now say the original behind `` is a name someone typed into your app: ```text Alice", "bcc": "attacker@example.com ``` Plain text substitution drops that straight into the document. The first quote ends the `subject` string early. The rest of the name becomes JSON structure. Here it is end to end. This runs as-is against 0.8.0: ```ts import { MemoryVault, restoreJson, restoreSync } from '@raeven-co/sether'; const EMAIL = ''; const NAME = ''; // The vault maps tokens back to originals. This name was typed by someone // who controls their own name field. const vault = new MemoryVault(); vault.set(EMAIL, 'bob@example.com'); vault.set(NAME, 'Alice", "bcc": "attacker@example.com'); // Tool-call arguments as the model wrote them: valid JSON, tokens only. const args = JSON.stringify({ to: EMAIL, subject: `Hello ${NAME}` }); // Text substitution: the original lands in the document unescaped. const asText = restoreSync(args, { vault }); console.log(asText); console.log(Object.keys(JSON.parse(asText))); // Value by value (new in 0.8.0): the original stays inside its string. const asJson = restoreJson(args, { vault }); console.log(asJson); console.log(Object.keys(JSON.parse(asJson))); ``` Output: ```text {"to":"bob@example.com","subject":"Hello Alice", "bcc": "attacker@example.com"} [ 'to', 'subject', 'bcc' ] {"to":"bob@example.com","subject":"Hello Alice\", \"bcc\": \"attacker@example.com"} [ 'to', 'subject' ] ``` The model wrote two keys. Text restore produced three. A `send_email` tool reading those arguments now has a `bcc` recipient nobody asked for. `restoreJson` is new in 0.8.0. It parses the document, restores each string value on its own, and lets `JSON.stringify` escape it. The same name stays inside `subject`, as data. ![The bug versus the fix: the same tool-call JSON restored as text gains an extra bcc key; restored value by value it keeps two keys and the quotes are escaped.](https://sether.ai/blog/img/sether-0-8-0-bug-vs-fix.png) ### Who could actually trigger it I want to be precise here, because "PII library had an injection bug" can sound bigger or smaller than it is. - **The original has to carry a `"` to add keys, or end in a `\` to break the document.** And an attacker has to be able to influence it, for example their own name. - **The built-in detector paths checked in the affected releases did not capture a `"`.** The demonstrated key-addition case therefore uses a custom or externally populated mapping. - **Two built-in detectors can capture a trailing backslash:** `labeledPasswordDetector` (in the opt-in secrets pack) and `addressDetector` (in the opt-in identity pack). That breaks the document. - **Custom detectors, vaults filled from other systems, and direct `vault.set` calls can carry any character.** There, full key injection is possible. The backslash case, with a detector that ships in the package and no custom code: ```ts import { MemoryVault, TOKEN_RE, redactSync, restoreJson, restoreSync, secretsDetectors, } from '@raeven-co/sether'; // A built-in detector, no custom code: the labeled-password detector // captures the trailing backslash as part of the password. const vault = new MemoryVault(); const prompt = redactSync('password: hunter2\\', { detectors: secretsDetectors, vault }); const token = prompt.match(TOKEN_RE)?.[0] ?? ''; const args = JSON.stringify({ note: token, to: 'bob@example.com' }); const asText = restoreSync(args, { vault }); console.log(asText); try { JSON.parse(asText); } catch (err) { console.log((err as Error).name); } console.log(restoreJson(args, { vault })); ``` Output: ```text {"note":"hunter2\","to":"bob@example.com"} SyntaxError {"note":"hunter2\\","to":"bob@example.com"} ``` ## It wasn't only in code that called Sether Sether's own restore paths did the same thing. I ran the same tool-call arguments through the 0.7.0 package tarball and through 0.8.0, and parsed what came out: | Restore path | 0.7.0: keys after parsing | 0.8.0: keys after parsing | | --- | --- | --- | | `createSSERestoreStream`, JSON `data:` payload | `to`, `subject`, `bcc` | `to`, `subject` | | `wrapFetch`, `application/json` body | `to`, `subject`, `bcc` | `to`, `subject` | | `createExpressMiddleware`, `res.send(string)` | `to`, `subject`, `bcc` | `to`, `subject` | | `wrapOpenAI`, JSON-mode reply | `to`, `subject`, `bcc` | `to`, `subject` | The same review turned up a second, separate problem in the Express middleware. It rebuilt `req.body` with plain assignment. A client could send this: ```json {"__proto__": {"isAdmin": true}} ``` In 0.7.0, after the middleware ran, `req.body.isAdmin` was `true`, and `Object.keys(req.body)` listed only the other keys. Validation that walks own keys could miss it. This has been there since the middleware shipped. In 0.8.0 `req.body.isAdmin` is `undefined`, and `"__proto__"` stays an ordinary key, exactly as `JSON.parse` produced it. ## What changed in 0.8.0 ### New: `restoreJson` and `restoreJsonValue` - `restoreJson(json, { vault })` takes serialized JSON and returns serialized JSON. Each string value is restored and re-escaped. - `restoreJsonValue(value, { vault })` does the same for a value you already parsed. It returns a new value and keeps a `"__proto__"` key as data. - Object keys are never restored. - Invalid JSON throws `SyntaxError`. There is no fallback to text replacement. - Tokens whose angle brackets a serializer escaped as `<` / `>` are still restored. An Anthropic `tool_use` block arrives with its `input` already parsed, so that's `restoreJsonValue`: ```ts import { MemoryVault, restoreJsonValue } from '@raeven-co/sether'; const EMAIL = ''; const NAME = ''; const vault = new MemoryVault(); vault.set(EMAIL, 'bob@example.com'); vault.set(NAME, 'Alice", "bcc": "attacker@example.com'); // An Anthropic tool_use block: its `input` arrives already parsed. const block = { type: 'tool_use', id: 'toolu_01', name: 'send_email', input: { to: EMAIL, subject: `Hello ${NAME}` }, }; const input = restoreJsonValue(block.input, { vault, strict: true }); console.log(input); ``` Output: ```text { to: 'bob@example.com', subject: 'Hello Alice", "bcc": "attacker@example.com' } ``` ### New: strict mode By default, a token Sether can't resolve passes through untouched. That's quiet in the wrong way. A model can invent a token, copy one with the wrong case, or put one in a key. `strict: true` throws `UnresolvedTokenError` instead. Its `tokens` property lists every token that would survive. It never includes restored originals. `restoreSync`, `restoreJson` and `restoreJsonValue` all accept it. ```ts import { MemoryVault, UnresolvedTokenError, restoreJson } from '@raeven-co/sether'; const EMAIL = ''; const vault = new MemoryVault(); vault.set(EMAIL, 'bob@example.com'); // The model copied one real token, invented one, and changed the case of one. const args = JSON.stringify({ to: EMAIL, cc: '', bcc: EMAIL.toLowerCase(), }); // Default: anything that can't be restored passes through silently. console.log(restoreJson(args, { vault })); // Strict: throw instead, and name every token that would survive. try { restoreJson(args, { vault, strict: true }); } catch (err) { if (err instanceof UnresolvedTokenError) console.log(err.tokens); } ``` Output: ```text {"to":"bob@example.com","cc":"","bcc":""} [ '', '' ] ``` Strict mode is not authorization. A model can still put a real token in the wrong field, like the customer's own address in `bcc`. Validate tool arguments before acting on them. Also new: `TOKEN_RE` (the token pattern) and the `JsonValue` type are exported, so integrations don't need to copy the pattern. ### Fixed: Sether's own restore paths restore JSON value by value ![Which restore paths are JSON-safe in @raeven-co/sether 0.8.0 and sether 0.4.0.](https://sether.ai/blog/img/sether-release-json-safe-paths.png) These paths now change only the string values that hold a token, and re-encode just those. Every other byte stays as it was: formatting, key order, escapes, and integers beyond `Number.MAX_SAFE_INTEGER`. Keys are never restored. - **`createSSERestoreStream`:** a `data:` payload that parses as JSON is restored value by value. `[DONE]` and plain-text payloads are still restored as text. In a text payload, a line break inside an original used to end the line, so the rest could inject `event:` or `id:` fields. Now the original continues on a new `data: ` line. - **`wrapFetch`:** an `event-stream` body is handled per `data:` payload, as above. A body that parses as JSON is restored value by value. A JSON-typed body that doesn't parse as a whole (NDJSON, JSON Lines) is restored line by line, and lines that aren't valid JSON stay tokenized. Other text is restored as text. - **`createExpressMiddleware`:** a string passed to `res.send()` that parses as JSON is restored value by value. That includes the string Express's own `res.json()` hands to `res.send()`. - **`wrapOpenAI` / `wrapAnthropic`:** when the request asks for JSON output (`response_format` `json_object` or `json_schema`; Anthropic `output_config.format`, or the deprecated `output_format`, with `json_schema`), the reply is restored value by value. If it isn't valid JSON, it stays tokenized. ### New, opt-in: restore tool-call arguments in the SDK wrappers Tool-call arguments still stay tokenized by default. The tests pin that. If you want real values in them, set `restoreToolArguments: true`. Non-streaming `tool_calls[].function.arguments` (OpenAI) and `tool_use.input` (Anthropic) are then restored value by value. ```ts import OpenAI from 'openai'; import { Sether, wrapOpenAI } from '@raeven-co/sether'; const sether = new Sether(); const openai = wrapOpenAI(new OpenAI(), { detectors: sether.detectors, vault: sether.vault, restoreToolArguments: true, // new in 0.8.0, default false }); const reply = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [{ role: 'user', content: 'Send the invoice to alice@example.com' }], tools: [ { type: 'function', function: { name: 'send_email', parameters: { type: 'object', properties: { to: { type: 'string' }, subject: { type: 'string' } }, required: ['to', 'subject'], }, }, }, ], }); for (const call of reply.choices[0]?.message.tool_calls ?? []) { if (call.type !== 'function') continue; // The model only saw . These arguments now carry real data: // validate them before your tool acts on them. console.log(call.function.name, JSON.parse(call.function.arguments)); } ``` When I ran this, a stand-in for the model received `Send the invoice to ` and answered with a `send_email` call holding that token. The script printed: ```text send_email { to: 'alice@example.com', subject: 'Your invoice' } ``` Restored arguments carry real PII into your tool code. Validate them before you act. ### Fixed: the Express middleware no longer rewrites binary bodies `res.send(buffer)` used to send `{"0":..,"1":..}` instead of the bytes, and a `Date` inside a JSON body became `{}`. Only arrays and plain objects are walked now. Buffers, typed arrays, Dates and other non-plain objects pass through unchanged. The `"__proto__"` fix above is in the same middleware. ## Behaviour changes to check before you upgrade - **`restoreSync`, `createRestoreStream` and `sether.restore()` behave exactly as before.** Upgrading does not fix your own code if it restores JSON with them. You have to switch to `restoreJson` or `restoreJsonValue`. - On Sether's own paths, a token sitting in an object key is no longer restored. - JSON-mode content that isn't valid JSON, for example cut off by a token limit, now stays tokenized. - NDJSON lines that aren't valid JSON now stay tokenized. - Tokens escaped as `<…>` are now restored. Rehearse the upgrade with a fictional original containing a quote and backslash, an unknown token, a truncated JSON reply and an otherwise valid token in an unauthorized destination field. Assert both syntax and allowed fields after restoration. Use a stubbed tool: a successful parse or strict restore must never be treated as permission to send an email or change a record. These restore APIs and the alias engine remain part of the full MIT libraries. Optional Pro workflows in the official tools are separate; an SDK upgrade does not add a Sether usage quota or require a Pro account. See [pricing](/pricing) for that product distinction. ## How to upgrade 1. Install 0.8.0: ```bash npm install @raeven-co/sether@^0.8.0 ``` 1. Find the places where you restore text yourself: ```bash grep -rnE "restoreSync|createRestoreStream|\.restore\(\)|AliasVault" src/ ``` 1. For each one that can receive JSON (tool-call arguments, JSON-mode replies, JSON response bodies), switch to `restoreJson` for a string or `restoreJsonValue` for a parsed value. Add `strict: true` if a surviving token should be an error. 2. If you restore a JSON-mode response streamed over SSE, collect the tokenized `delta.content` fragments first, then call `restoreJson` on the joined text. See the limits below. 3. If you want real values in tool-call arguments from `wrapOpenAI` / `wrapAnthropic`, set `restoreToolArguments: true`, and validate the arguments before acting. 4. If you use `createExpressMiddleware`, upgrading is the fix for `"__proto__"`. 0.8.0 also carries 0.7.1, a docs-only patch that was tagged but never reached npm. ## What 0.8.0 does not fix - **JSON-mode output streamed over SSE.** Each SSE chunk stays valid JSON. But the `delta.content` fragments join into a second JSON document that the stream never sees whole. An original can land in that document unescaped. Don't SSE-restore JSON-mode streams: collect the fragments and call `restoreJson`. - **Text restore is still text restore, by design.** `restoreSync`, `createRestoreStream` and `sether.restore()` are plain substitution. So is `AliasVault.restore()`: on JSON, apply it to each parsed string value, never to the serialized document. - **Big integers in `restoreJson`.** It re-serializes through `JSON.parse`, so integers above 2^53 lose precision. Sether's middleware paths keep exact bytes. If it matters in your own code, parse with your own reviver and call `restoreJsonValue`. - **One level at a time.** A string value that itself holds serialized JSON is restored as plain text. Parse and restore it separately. - **A JSON payload split across several `data:` lines** is restored as text. Each `data:` line is handled on its own. - **Strict mode only catches token-shaped text** in the `` form. It is not authorization. ## Disclosure - Found by me on 14 October 2026, while building `n8n-nodes-sether`. It was not reported by anyone else. - Affected: `@raeven-co/sether` 0.7.0 and earlier. Fixed in 0.8.0. - The Express `"__proto__"` issue affects every release that ships `createExpressMiddleware`. Fixed in 0.8.0. - Python: `sether` 0.2.0 and earlier on PyPI, fixed in 0.4.0. The Python post is here: [Sether for Python 0.4.0: JSON-safe restore](/blog/sether-python-0-4-0-json-safe-restore). - Advisory: [the Sether security advisories on GitHub](https://github.com/raeven-co/sether/security/advisories) - To report a security issue, follow [SECURITY.md](https://github.com/raeven-co/sether/blob/main/SECURITY.md). Please don't open a public issue. 0.8.0 ships with 252 passing tests (201 before this release), a ReDoS scan of 224 regex patterns with 0 flagged unsafe, and one runtime dependency, `libphonenumber-js`. ## Links - npm: [www.npmjs.com/package/@raeven-co/sether](https://www.npmjs.com/package/@raeven-co/sether) - GitHub: [github.com/raeven-co/sether](https://github.com/raeven-co/sether) - Changelog: [github.com/raeven-co/sether/blob/main/CHANGELOG.md](https://github.com/raeven-co/sether/blob/main/CHANGELOG.md) - Try it in the browser: [the live sandbox](/sandbox) ## Frequently asked questions ### Which versions are affected? `@raeven-co/sether` 0.7.0 and earlier on npm, fixed in 0.8.0, and `sether` 0.2.0 and earlier on PyPI, fixed in 0.4.0. The original value has to contain a quote to add keys, or end in a backslash to break the document, and an attacker has to be able to influence it. The affected built-in paths checked here did not capture a quote; custom detectors, externally populated vaults and direct `vault.set` calls can. ### Does upgrading fix my code automatically? Only where you rely on Sether's own restore paths: the SSE stream, `wrapFetch`, the Express middleware and the SDK wrappers. `restoreSync` and `sether.restore()` are unchanged, so code that restores serialized JSON with them must switch to `restoreJson` or `restoreJsonValue`. ### What does strict mode catch? Token-shaped text that would survive restore: well-formed tokens the vault cannot resolve (invented, expired or from another vault), case-altered copies and tokens in object keys. The error lists tokens only, never originals. It is not authorization, so validate a tool call before you act on it. ### How do I upgrade? Run `npm install @raeven-co/sether@^0.8.0`. Python users run `pip install --upgrade "sether>=0.4.0"`. --- Source: https://sether.ai/blog/sether-python-0-4-0-json-safe-restore # A quote in a name could add a bcc to an AI tool call. Here is the fix in Sether for Python 0.4.0. > sether 0.4.0 on PyPI fixes a bug where restoring tokens into serialized JSON as plain text put original values back unescaped, so a value containing a quote could add keys to LLM tool-call arguments. New restore_json and restore_json_value helpers restore each string value on its own, strict mode raises on tokens it cannot resolve, and the SSE, httpx, ASGI, WSGI and SDK-wrapper paths now restore JSON value by value. It also brings the 0.3.0 features to PyPI for the first time. Published 2026-10-17, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - Restoring tokens into serialized JSON as plain text put original values back unescaped. An original holding a `"` could add keys, for example a `bcc` in an email tool call. - `sether` 0.4.0 adds `restore_json`, `restore_json_value` and `Sether.restore_json`, which restore each string value on its own and let `json.dumps` escape it. Object keys are never restored. - Strict mode (`strict=True`) raises `UnresolvedTokenError` instead of silently leaving invented, expired or altered tokens in place. - The SSE, httpx, ASGI, WSGI and SDK-wrapper restore paths now restore JSON value by value, and streamed JSON-mode deltas stay tokenized. - It is also the first PyPI release of the 0.3.0 features: the alias (decoy) engine, the multi-region phone detector and label-anchored secrets. On 14 October I was building an n8n node for Sether. Its job is to check a model's proposed tool call before any real value goes back into it. So I was looking hard at the moment the real values go back in. That is where I found the bug. Sether put originals back into JSON as plain text. If an original value held a quote, it could rewrite the JSON around it. Nobody reported this to me. I found it in my own library. It was in both packages, the TypeScript one and this Python port, and in Sether's own middleware too. It is fixed in `sether` 0.4.0 on PyPI and in `@raeven-co/sether` 0.8.0 on npm. The advisory is here: [the Sether security advisories on GitHub](https://github.com/raeven-co/sether/security/advisories). This post covers what Sether is, what went wrong, what changed, and how to upgrade, in Python. ## Sether in one picture ![Your app and Sether sit inside your process with the vault. The prompt "Email alice@example.com" leaves as "Email ". The LLM provider only sees tokens. The reply "Sent to " comes back and Sether restores it to "Sent to alice@example.com".](https://sether.ai/blog/img/sether-release-one-picture.png) Sether is an open-source library that sits between your app and any LLM. Before a prompt leaves your process, it swaps personal data for tokens shaped like ``. By default it catches emails, phone numbers written with a country code, card numbers, US Social Security numbers, IBANs and IP addresses. Opt-in packs add secrets such as API keys, and labelled identity fields such as `Name:`. For detected values, the edited request contains tokens instead of those originals; unmatched text and identifying context can remain. Recognised reply tokens restore only while their mapping is available. The default vault stays in process, and the core package does not phone home. Keep the full data flow in view, including your own logs and stores. It runs on Python 3.9 and up, with sync and async streaming, and integrations for httpx, ASGI (FastAPI, Starlette), WSGI (Flask), and the OpenAI and Anthropic SDKs. ```bash pip install sether ``` ## The bug: restoring into JSON as text Restoring sounds like the easy half. Find each token in the reply, look it up in the vault, and put the original in its place. For a chat reply in plain prose, that is exactly right. But models don't only write prose. They write tool calls, and tool-call arguments are serialized JSON: ```json {"to": "", "subject": "Hello "} ``` Now say the original behind `` is a name someone typed into your app: ```text Alice", "bcc": "attacker@example.com ``` Plain text substitution drops that straight into the document. The first quote ends the `subject` string early. The rest of the name becomes JSON structure. Here it is end to end. This runs as-is against 0.4.0: ```python import json from sether import MemoryVault, restore_json, restore_sync EMAIL = "" NAME = "" # The vault maps tokens back to originals. This name was typed by someone # who controls their own name field. vault = MemoryVault() vault.set(EMAIL, "bob@example.com") vault.set(NAME, 'Alice", "bcc": "attacker@example.com') # Tool-call arguments as the model wrote them: valid JSON, tokens only. args = json.dumps({"to": EMAIL, "subject": f"Hello {NAME}"}) # Text substitution: the original lands in the document unescaped. as_text = restore_sync(args, vault) print(as_text) print(list(json.loads(as_text))) # Value by value (new in 0.4.0): the original stays inside its string. as_json = restore_json(args, vault) print(as_json) print(list(json.loads(as_json))) ``` Output: ```text {"to": "bob@example.com", "subject": "Hello Alice", "bcc": "attacker@example.com"} ['to', 'subject', 'bcc'] {"to":"bob@example.com","subject":"Hello Alice\", \"bcc\": \"attacker@example.com"} ['to', 'subject'] ``` The model wrote two keys. Text restore produced three. A `send_email` tool reading those arguments now has a `bcc` recipient nobody asked for. `restore_json` is new in 0.4.0. It parses the document, restores each string value on its own, and lets `json.dumps` escape it. The same name stays inside `subject`, as data. ![The bug versus the fix: the same tool-call JSON restored as text gains an extra bcc key; restored value by value it keeps two keys and the quotes are escaped.](https://sether.ai/blog/img/sether-python-0-4-0-bug-vs-fix.png) ### Who could actually trigger it I want to be precise here, because "PII library had an injection bug" can sound bigger or smaller than it is. - **The original has to carry a `"` to add keys, or end in a `\` to break the document.** And an attacker has to be able to influence it, for example their own name. - **The built-in detector paths checked in the affected releases did not capture a `"`.** The demonstrated key-addition case therefore uses a custom or externally populated mapping. - **Two built-in detectors can capture a trailing backslash:** the labeled-password detector (in the opt-in `secrets_detectors` pack) and the address detector (in the opt-in `identity_detectors` pack). That breaks the document. - **Custom detectors, vaults filled from other systems, and direct `vault.set` calls can carry any character.** There, full key injection is possible. The backslash case, with a detector that ships in the package and no custom code: ```python import json from sether import TOKEN_RE, MemoryVault, redact_sync, restore_json, restore_sync, secrets_detectors # A built-in detector, no custom code: the labeled-password detector # captures the trailing backslash as part of the password. vault = MemoryVault() prompt = redact_sync("password: hunter2\\", secrets_detectors, vault) token = TOKEN_RE.search(prompt).group(0) args = json.dumps({"note": token, "to": "bob@example.com"}) as_text = restore_sync(args, vault) print(as_text) try: json.loads(as_text) except json.JSONDecodeError as err: print(type(err).__name__) print(restore_json(args, vault)) ``` Output: ```text {"note": "hunter2\", "to": "bob@example.com"} JSONDecodeError {"note":"hunter2\\","to":"bob@example.com"} ``` ## It wasn't only in code that called Sether Sether's own restore paths did the same thing. I ran the same tool-call arguments through the code at the `v0.2.0` tag (the last release on PyPI), the `v0.3.0` tag, and 0.4.0, and parsed what came out: | Restore path | 0.2.0 and 0.3.0: keys after parsing | 0.4.0: keys after parsing | | --- | --- | --- | | `sse_restore_iter`, JSON `data:` payload | `to`, `subject`, `bcc` | `to`, `subject` | | `SetherWSGIMiddleware`, `application/json` body | `to`, `subject`, `bcc` | `to`, `subject` | | `wrap_openai`, JSON-mode reply | `to`, `subject`, `bcc` | `to`, `subject` | | `wrap_openai`, streamed JSON-mode deltas, joined | `to`, `subject`, `bcc` | `to`, `subject` (left tokenized) | That last row is a deliberate change. Streamed JSON-mode deltas are fragments of one document, so 0.4.0 no longer restores them one by one. Join them and call `restore_json`. ## What changed in 0.4.0 ### New: `restore_json`, `restore_json_value` and `Sether.restore_json` - `restore_json(text, vault)` takes serialized JSON and returns serialized JSON (compact). Each string value is restored and re-escaped. - `restore_json_value(value, vault)` does the same for a value you already parsed, like an Anthropic `tool_use` block's `input`. It returns a new value and doesn't touch the input. - `sether.restore_json(text)` is the same thing on the `Sether` facade, using its shared vault. - Object keys are never restored. - Invalid JSON raises `json.JSONDecodeError`, and a non-string raises `TypeError`. There is no fallback to text replacement. The facade, as a round trip: ```python import json from sether import TOKEN_RE, Sether sether = Sether() # Outgoing: the prompt the model sees. prompt = sether.redact_sync("Email alice@example.com about the refund") print(prompt) # Incoming: the model answers in JSON mode and quotes the token back. token = TOKEN_RE.search(prompt).group(0) reply = json.dumps({"action": "send_email", "to": token}) # Same vault, JSON-safe restore (new in 0.4.0). print(sether.restore_json(reply, strict=True)) ``` Output (the UUID in the token is random on every run): ```text Email about the refund {"action":"send_email","to":"alice@example.com"} ``` ### New: strict mode By default, a token Sether can't resolve passes through untouched. That's quiet in the wrong way. A model can invent a token, copy one with the wrong case, or put one in a key. `strict=True` raises `UnresolvedTokenError` (a `ValueError`) instead. Its `tokens` attribute lists every token that would survive. It never includes restored originals. `restore_sync`, `Sether.restore_sync`, `restore_json` and `restore_json_value` all accept it. ```python import json from sether import MemoryVault, UnresolvedTokenError, restore_json EMAIL = "" vault = MemoryVault() vault.set(EMAIL, "bob@example.com") # The model copied one real token, invented one, and changed the case of one. args = json.dumps({ "to": EMAIL, "cc": "", "bcc": EMAIL.lower(), }) # Default: anything that can't be restored passes through silently. print(restore_json(args, vault)) # Strict: raise instead, and name every token that would survive. try: restore_json(args, vault, strict=True) except UnresolvedTokenError as err: print(err.tokens) ``` Output: ```text {"to":"bob@example.com","cc":"","bcc":""} ('', '') ``` Strict mode is not authorization. A model can still put a real token in the wrong field, like the customer's own address in `bcc`. Validate tool arguments before acting on them. Also new at the package root: `TOKEN_RE` (it used to live only in `sether.stream`) and the `JsonValue` type. ### Fixed: Sether's own restore paths restore JSON value by value ![Which restore paths are JSON-safe in @raeven-co/sether 0.8.0 and sether 0.4.0.](https://sether.ai/blog/img/sether-release-json-safe-paths.png) These paths now change only the string values that hold a token, and re-encode just those. Every other byte stays as it was: formatting, key order, escapes and numbers. Keys are never restored. - **`sse_restore_iter` / `create_sse_restore_stream`:** a `data:` payload that parses as JSON is restored value by value. `[DONE]` and plain-text payloads are still restored as text. In a text payload, a line break inside an original used to end the line, so the rest could inject `event:` or `id:` fields. Now the original continues on a new `data: ` line. - **`wrap_httpx`, `SetherASGIMiddleware`, `SetherWSGIMiddleware`:** an `event-stream` body is handled per `data:` payload, as above. A body that parses as JSON is restored value by value. A JSON-typed body that doesn't parse as a whole (NDJSON, JSON Lines) is restored line by line, and lines that aren't valid JSON stay tokenized. Other text is restored as text. - **`wrap_openai` / `wrap_anthropic`:** when the request asks for JSON output (`response_format` `json_object` or `json_schema`; Anthropic `output_config.format`, or the deprecated `output_format`, with `json_schema`), the reply is restored value by value. If it isn't valid JSON, it stays tokenized. Streamed JSON-mode deltas stay tokenized. ### New, opt-in: restore tool-call arguments in the SDK wrappers Tool-call arguments still stay tokenized by default. The tests pin that. If you want real values in them, pass `restore_tool_arguments=True`. Non-streaming `tool_calls[].function.arguments` (OpenAI) and `tool_use` `input` (Anthropic) are then restored value by value. ```python import json from openai import OpenAI from sether import Sether, wrap_openai sether = Sether() client = wrap_openai( OpenAI(), detectors=sether.detectors, vault=sether.vault, restore_tool_arguments=True, # new in 0.4.0, default False ) reply = client.chat.completions.create( model="gpt-4.1-mini", messages=[{"role": "user", "content": "Send the invoice to alice@example.com"}], tools=[{ "type": "function", "function": { "name": "send_email", "parameters": { "type": "object", "properties": {"to": {"type": "string"}, "subject": {"type": "string"}}, "required": ["to", "subject"], }, }, }], ) for call in reply.choices[0].message.tool_calls or []: # The model only saw . These arguments now carry real data: # validate them before your tool acts on them. print(call.function.name, json.loads(call.function.arguments)) ``` When I ran this, a stand-in for the model received `Send the invoice to ` and answered with a `send_email` call holding that token. The script printed: ```text send_email {'to': 'alice@example.com', 'subject': 'Your invoice'} ``` Restored arguments carry real PII into your tool code. Validate them before you act. ### Also in 0.4.0: the 0.3.0 features, on PyPI for the first time 0.3.0 was tagged on GitHub but never reached PyPI. 0.4.0 carries it. If you are coming from 0.2.0, you also get: - **The alias engine** (`alias_value`, `suggest_aliases`, `shape_alias`, `AliasVault`): swap a value for a realistic decoy instead of a token, and reverse it later. - **`create_multi_region_phone_detector(countries)`**: one phone detector for several regions' national formats. - **Label-anchored secrets** (`labeled_api_key_detector`, `labeled_password_detector`), both now part of `secrets_detectors`. - **Detection fixes** for dates of birth and addresses. Those change what gets detected, so read the [0.3.0 changelog](https://github.com/emorilebo/sether-py/blob/main/CHANGELOG.md) before you upgrade. ## Behaviour changes to check before you upgrade - **`restore_sync`, `Sether.restore_sync` and the restore streams behave exactly as before.** Upgrading does not fix your own code if it restores JSON with them. You have to switch to `restore_json` or `restore_json_value`. - On Sether's own paths, a token sitting in an object key is no longer restored. - JSON-mode content that isn't valid JSON, for example cut off by a token limit, now stays tokenized. - Streamed JSON-mode deltas from `wrap_openai` / `wrap_anthropic` now stay tokenized. Streamed text deltas outside JSON mode are still restored. - NDJSON lines that aren't valid JSON now stay tokenized. - Tokens escaped as `<...>` are now restored. Rehearse the upgrade with a fictional original containing a quote and backslash, an unknown token, a truncated JSON reply and an otherwise valid token in an unauthorized destination field. Assert both syntax and allowed fields after restoration. Use a stubbed tool: a successful parse or strict restore must never be treated as permission to send an email or change a record. These restore APIs and the alias engine remain part of the full MIT libraries. Optional Pro workflows in the official tools are separate; an SDK upgrade does not add a Sether usage quota or require a Pro account. See [pricing](/pricing) for that product distinction. ## How to upgrade 1. Install 0.4.0: ```bash pip install --upgrade "sether>=0.4.0" ``` 1. Find the places where you restore text yourself: ```bash grep -rnE "restore_sync|restore_stream|restore_iter|AliasVault" --include="*.py" . ``` 1. For each one that can receive JSON (tool-call arguments, JSON-mode replies, JSON response bodies), switch to `restore_json` for a string or `restore_json_value` for a parsed value. Add `strict=True` if a surviving token should be an error. 2. If you stream a JSON-mode response, collect the tokenized fragments first, then call `restore_json` on the joined text. See the limits below. 3. If you want real values in tool-call arguments from `wrap_openai` / `wrap_anthropic`, pass `restore_tool_arguments=True`, and validate the arguments before acting. ## What 0.4.0 does not fix - **JSON-mode output streamed over SSE** (`sse_restore_iter`, or a middleware on an event-stream body). Each SSE chunk stays valid JSON. But the `delta.content` fragments join into a second JSON document that the stream never sees whole. An original can land in that document unescaped. Collect the fragments and call `restore_json`. - **Text restore is still text restore, by design.** `restore_sync` and the restore streams are plain substitution. So is `AliasVault.restore()`: on JSON, apply it to each parsed string value, never to the serialized document. - **`restore_json` re-serializes compactly**, so input formatting is not kept. Sether's middleware paths keep exact bytes. - **One level at a time.** A string value that itself holds serialized JSON is restored as plain text. Parse and restore it separately. - **A JSON payload split across several `data:` lines** is restored as text. Each `data:` line is handled on its own. - **Strict mode only catches token-shaped text** in the `` form. It is not authorization. ## Disclosure - Found by me on 14 October 2026, while building `n8n-nodes-sether`. It was not reported by anyone else. - Affected: `sether` 0.2.0 and earlier on PyPI (and the 0.3.0 tag, which never reached PyPI). Fixed in 0.4.0. - TypeScript: `@raeven-co/sether` 0.7.0 and earlier, fixed in 0.8.0. That post also covers a separate Express middleware issue that only affects the TypeScript package: [Sether 0.8.0: JSON-safe restore](/blog/sether-0-8-0-json-safe-restore). - Advisory: [the Sether security advisories on GitHub](https://github.com/raeven-co/sether/security/advisories) - To report a security issue, email the address in the [README](https://github.com/emorilebo/sether-py#reporting-security-issues). Please don't open a public issue. 0.4.0 passes 183 tests on Python 3.12 with httpx installed. ## Links - PyPI: [pypi.org/project/sether/](https://pypi.org/project/sether/) - GitHub: [github.com/emorilebo/sether-py](https://github.com/emorilebo/sether-py) - Changelog: [github.com/emorilebo/sether-py/blob/main/CHANGELOG.md](https://github.com/emorilebo/sether-py/blob/main/CHANGELOG.md) - Python docs: [sether.raevenmarket.com.ng/docs/python](/docs/python) ## Frequently asked questions ### Which versions are affected? `sether` 0.2.0 and earlier on PyPI, fixed in 0.4.0 (the 0.3.0 tag never reached PyPI), and `@raeven-co/sether` 0.7.0 and earlier on npm, fixed in 0.8.0. The original value has to contain a quote to add keys, or end in a backslash to break the document, and an attacker has to be able to influence it. ### Does upgrading fix my code automatically? Only where you rely on Sether's own restore paths: the SSE helpers, the httpx, ASGI and WSGI middleware and the SDK wrappers. `restore_sync` is unchanged, so code that restores serialized JSON with it must switch to `restore_json` or `restore_json_value`. ### What does strict mode catch? Token-shaped text that would survive restore: well-formed tokens the vault cannot resolve (invented, expired or from another vault), case-altered copies and tokens in object keys. The error lists tokens only, never originals. It is not authorization, so validate a tool call before you act on it. ### How do I upgrade? Run `pip install --upgrade "sether>=0.4.0"`. TypeScript users run `npm install @raeven-co/sether@^0.8.0`. --- Source: https://sether.ai/blog/pii-redaction-for-llm-apps-complete-guide # PII redaction for LLM applications: the complete guide (2026) > PII redaction for LLM apps detects supported personal details and secrets in prompts, then replaces matched values before the provider request. Reversible tokenization keeps a local token-to-original mapping so recognised tokens in the reply can be restored. It can run in an application library, a gateway, or a browser extension; each covers a different boundary. Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - Map the actual data flow first: provider roles, contracts, retention and legal obligations depend on your use case. Installing a redactor is not a compliance certificate. - Three places to redact: in-process library, proxy gateway, or browser extension. The library is the default for product teams. - Detection is layered: validated patterns (email, card, IBAN) catch structured PII; label anchoring catches "name: Sarah"; NER catches names in free text. - Reversible tokenization can keep replies useful while withholding matched originals. Unmatched details and identifying context can still pass through. - Streaming breaks naive redaction: a value split across two chunks slips through unless the redactor holds back a safety window. ## What is PII redaction for LLM applications? A prompt is an application payload. If a user types "Hi, I’m Sarah Chen, my email is sarah@acme.com, calling about order 4532", those values reach the provider unless your application changes the payload first. HTTPS protects transport; it does not hide the values from the intended recipient. Redaction replaces supported matches before that request, with restoration later when the task needs the originals back. Keep the replacement mapping on your side of the integration. The model can receive a token such as `` in place of a matched email, and your app can restore that token from its vault. This is a reduction in disclosed detail, not proof that the remaining message is anonymous: an order reference or the surrounding story may still identify someone. The mechanics are in [Reversible PII redaction: how redact-then-restore works](/blog/reversible-pii-redaction-redact-restore). ## Why this became urgent in 2026 An AI feature adds a data recipient and often another place where prompts, replies or logs can persist. Before launch, ask a concrete question: which fields does this task need, and which can remain in your application? Then review the resulting data flow with the people responsible for security, privacy and contracts. That produces a useful engineering requirement without relying on a blanket regulatory deadline or promising that redaction resolves every obligation. If you are wondering whether your specific setup is a problem, start with [Is it GDPR-compliant to send customer data to OpenAI?](/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai) That post walks the actual legal reasoning instead of fear-mongering. ## Where to redact: library, gateway, or browser | Approach | Where it runs | Best for | Trade-off | | --- | --- | --- | --- | | In-process library | Inside your app, wrapping the LLM call | Product teams who own the code path | One integration per service | | Proxy gateway | A separate service your SDK points at | Many services, one policy; polyglot stacks | New infrastructure to run and trust | | Browser extension | The user's browser, watching the chat input | Employees pasting into ChatGPT/Claude/Gemini directly | Protects humans, not your API traffic | These are complements, not competitors. A library protects your product's API calls; an extension protects the copy-paste habits of the humans in your company. We cover the human side in [How to stop pasting sensitive data into ChatGPT](/blog/stop-pasting-sensitive-data-into-chatgpt). ## Detection: patterns, labels, and NER No single technique catches everything. Production redaction layers stack three: 1. **Validated patterns** identify candidate structured values, with checks such as Luhn for cards and mod-97 for IBANs. These checks reject some invalid candidates; they do not prove a value belongs to a person or eliminate false matches. 2. **Label anchoring** uses nearby cues such as "name: Sarah Chen" or "DOB: 12/04/1991". Coverage depends on the enabled labels, scripts and value formats. 3. **NER (named-entity recognition)** can identify entities in free prose where shape and labels offer less signal. It adds model and evaluation costs. Known-term lists and custom detectors are other options for specific vocabularies. [Regex vs NER for PII detection](/blog/regex-vs-ner-pii-detection) explains coverage and evaluation choices. Local inference can avoid sending text to a separate detection service; it also adds model-loading and runtime responsibilities to assess. ## Not just PII: API keys and secrets The same pipeline that catches emails should catch `sk-...` OpenAI keys, AWS access keys, GitHub tokens, Slack tokens, Stripe keys, JWTs, and high-entropy strings. Developers paste config into AI chats constantly, and a leaked provider key is a worse day than a leaked email address. Sether ships this as a separate detector pack (8 detectors including a Shannon-entropy catch-all) that you enable alongside the basic pack. ## The streaming trap: values split across chunks Streaming introduces a second boundary: a detector may receive `sarah@ac` and `me.com` in different chunks. Scanning chunks independently can miss the joined email. Sether uses a configurable hold-back distance (`safeDistanceBytes`, default setting 256) and additional buffering guards. These are bounded safeguards, so test long values, custom detectors and actual transport framing rather than treating the default as a guarantee for every possible input. The [chunk-boundary discussion](/blog/streaming-llm-redaction-chunk-boundary-bug) shows a reproducible example, partition-test design and the current implementation’s buffering limits. Use those examples to test the actual code path and retain the failing partitions as regression fixtures. ## The tool landscape, honestly | Tool | Language | Approach | Notes | | --- | --- | --- | --- | | Microsoft Presidio | Python | NER (spaCy) + patterns, analyzer/anonymizer services | Analyzer and configurable anonymization operators; reversible encrypt/decrypt is also supported | | LLM Guard (Protect AI) | Python | Guardrail scanner suite; Anonymize is one scanner among many | Broad guardrails (prompt injection, toxicity) rather than a focused redaction layer | | Sether | TypeScript + Python | Streaming redact/restore with a token vault; pattern + label + optional local NER packs | MIT SDK, one declared TypeScript runtime dependency; official tools have separate Free and Pro tiers | | DIY regex | Any | Hand-rolled patterns in your request path | Fine for a prototype; typically falls to the streaming trap and lacks restore, validation, and tests | Choose by integration shape and measured coverage. Presidio exposes configurable detection and anonymization operators, including a [deanonymizer for reversible operations](https://presidio.dataprivacystack.org/anonymizer/). Sether focuses on an in-process redact/vault/restore flow for TypeScript and Python. Compare both on representative prompts and deployment constraints; the longer [Presidio comparison](/blog/lightweight-presidio-alternatives-typescript) explains what to test. ## A minimal implementation In TypeScript ([full walkthrough](/blog/redact-pii-before-openai-typescript)): ```ts import { Sether } from '@raeven-co/sether'; import { Readable } from 'node:stream'; const sether = new Sether(); // Outgoing: pipe the prompt through redact() before it leaves const safeForLLM = Readable.from([userMessage]).pipe(sether.redact()); // Incoming: pipe the model reply through restore() before the user sees it const safeForUser = llmResponse.pipe(sether.restore()); ``` In Python ([full walkthrough](/blog/redact-pii-llm-prompts-python)): ```python from sether import Sether sether = Sether() safe = sether.redact_sync(user_message) # tokens out reply = call_openai(safe) restored = sether.restore_sync(reply) # real values home ``` ## Trusting the redactor itself A redaction layer handles sensitive inputs, so inspect its source, dependencies, storage and network behavior. Review the exact version you plan to deploy, including optional models and wrappers. The [supply-chain hardening account](/blog/npm-supply-chain-zero-alerts-socket) documents a historical scan, not a permanent security rating or certification. Use a fictional support ticket as an acceptance fixture. Mark the email as a required replacement, leave the refund question readable, and make the model reply echo the token. Verify the outgoing payload, the restored reply and your error logs separately. A passing round trip proves the mapping works; it does not prove that every sensitive field was detected. | Boundary | What to inspect | | --- | --- | | Before the provider call | Matched fields are replaced; unnecessary context is removed; raw prompts are absent from logs. | | After the model reply | Only recognised mappings restore; unexpected tokens or fields are reviewed. | | After the session | Vault retention and saved application data follow your chosen lifecycle. | ## The 10-point checklist 1. List every place your code sends text to an LLM provider (including background jobs and evals). 2. Review provider roles, applicable agreements and any required customer disclosures for the actual data flow. 3. Put a redaction layer in each request path (library or gateway). 4. Enable structured-PII detectors with validation (email, phone, card + Luhn, SSN, IBAN + mod-97, IPs). 5. Enable the secrets pack (provider keys, tokens, JWTs, high-entropy). 6. Turn on label-anchored identity detection, multilingual if you serve non-English users. 7. Add NER if your prompts carry free-text names (support tickets, chat, documents). 8. Verify streaming safety: test a value split across chunk boundaries. 9. Keep the matching vault available for the reply, with deliberate access, capacity and retention limits; test missing and expired mappings. 10. Give employees a browser-side guard for direct ChatGPT/Claude/Gemini use. ## Frequently asked questions ### Does redaction hurt LLM answer quality? It can change answer quality. Typed tokens preserve a hint about the field, but the model may omit, alter or misplace them, and some tasks need facts carried by the original value. Evaluate the actual task with tokenized fixtures, validate the result and restore only recognised replacements. ### Is prompt redaction required by GDPR? GDPR does not prescribe Sether or a particular redactor. Appropriate controls depend on the processing and risk. Redaction can support minimisation, but source inspection and a successful demo do not establish legal compliance. See the [GDPR discussion](/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai) and obtain advice for your circumstances. ### Why not just use OpenAI's zero-retention or EU endpoints? Provider retention controls and pre-request redaction address different parts of the data flow. Redaction withholds matched values; settings control the treatment of what you do send. Neither proves that all identifying context has been removed or that every legal obligation is satisfied. ### What does Sether cost? The full TypeScript and Python libraries, including decoy and restore, remain free under MIT with no Sether usage quota. Official tools offer Free scanning, masking, token replacement and all restore of known mappings. New licensed releases add optional Pro realistic decoys and custom watchlists in both tools, plus custom regex rules in Shield, for one person across Shield and the editor: $5 for one month or $48 for one year, manual renewal. The seven-day trial is opt-in after installing a compatible build, requires no card and does not charge automatically. Current store builds may retain legacy free features. See [pricing](/pricing) for pilot availability. --- Source: https://sether.ai/blog/redact-pii-in-json-before-llm # How to redact PII in JSON and structured data before sending it to an LLM > For JSON sent to an LLM, use structure-aware detection and verify the resulting payload still parses. Sether’s opt-in identity pack uses supported key cues for names, dates of birth, passports and addresses; other detectors recognize structured values. Restore complete JSON replies with JSON-aware APIs, then validate the resulting schema and intended fields. Published 2026-07-23, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - The PII your backend sends to an LLM is usually in JSON values, and the key right next to each value names exactly what it is. - Redactors tuned for chat prose ("Name: Sarah") miss `"customer_name": "Sarah"`, because there is no sentence, only structure. - Sether 0.6.0 (npm) and 0.2.0 (Python) anchor the identity pack on JSON keys, so structured payloads get redacted too. - Value validators narrow candidates, but do not prove every valid-looking value is personal or catch every real format. - Test JSON syntax and field structure separately from detection coverage. Restoring quoted originals requires JSON-aware restoration. ## Your PII is in the values, and the keys name it Most writing about redaction for LLMs assumes a chat box: a human types a sentence, and somewhere in that sentence is an email or a card number. But a large share of the data that backends send to OpenAI or Claude never looked like a sentence. It is a JSON object: a form submission, an API request body, a row pulled from your database, a webhook payload you are asking the model to classify, extract, or turn into a document. ```json { "customer_name": "Amara Okafor", "date_of_birth": "1990-05-12", "passport_number": "A1234567", "billing_address": "12 Marina Road, Lagos", "question": "Can you update the delivery address on my order?" } ``` Four of those five fields are personal data, and the thing that makes them easy to leak is the same thing that makes them easy to catch: the key sitting immediately to the left of each value spells out precisely what the value is. `customer_name` is a name. `date_of_birth` is a DOB. You do not need to guess. ## Why prose-tuned redaction walks right past this A redactor built for chat looks for a label followed by a value in running text: "Name: Sarah Chen", "DOB 1990-05-12", "my passport is A1234567". That is a good, low-false-positive strategy for prose, and it is exactly what Sether's identity pack did originally. The trouble is that `"customer_name": "Amara Okafor"` is not that shape. There is no salutation, no sentence, no natural-language label. There is a quoted key, a colon, and a quoted value. So a prose detector sees the address in "Address: 12 Marina Road" but misses `"billing_address": "12 Marina Road, Lagos"`, and a whole category of real traffic slips through unredacted. This is the gap Sether closes in 0.6.0. The full trade-off between label-anchored detection and free-text NER is covered in [Regex vs NER for PII detection](/blog/regex-vs-ner-pii-detection); this post is about the structured case specifically. ## Enable the identity pack (TypeScript and Python) JSON-key detection lives in the opt-in identity pack, not the default basic pack, because names and addresses are higher-variance than checksummed data like cards or IBANs. You turn it on by adding `identityDetectors` to the detector list when you construct the instance. ```ts import { Sether, basicDetectors, identityDetectors, redactSync, restoreSync, } from '@raeven-co/sether'; const sether = new Sether({ detectors: [...basicDetectors, ...identityDetectors], }); const payload = JSON.stringify({ customer_name: 'Amara Okafor', date_of_birth: '1990-05-12', passport_number: 'A1234567', billing_address: '12 Marina Road, Lagos', }); const safe = redactSync(payload, { detectors: sether.detectors, vault: sether.vault }); // -> {"customer_name":"","date_of_birth":"", ...} // send `safe` to the model. Its reply is JSON too: parse it first, // then restore the string fields you are going to use. const draft = JSON.parse(modelReply) as { subject: string; body: string }; const subject = restoreSync(draft.subject, { vault: sether.vault }); const body = restoreSync(draft.body, { vault: sether.vault }); ``` ```python import json from sether import Sether, basic_detectors, identity_detectors sether = Sether(detectors=[*basic_detectors, *identity_detectors]) payload = '{"customer_name": "Amara Okafor", "date_of_birth": "1990-05-12"}' safe = sether.redact_sync(payload) # -> {"customer_name": "", "date_of_birth": ""} # the reply is JSON too: parse it, then restore the fields you use draft = json.loads(model_reply) subject = sether.restore_sync(draft["subject"]) body = sether.restore_sync(draft["body"]) ``` The shape is the same in both languages: one instance holds the detectors and the vault that maps each token to its original value. In Python you call `redact_sync` and `restore_sync` on the instance. In TypeScript the one-shot helpers are the exported `redactSync` and `restoreSync` functions, and you pass them the instance's `detectors` and `vault`. On the way back, parse the model's JSON first and restore the string fields you use. The round-trip mechanics are the same as the prose case, walked through in [How to redact PII before OpenAI in TypeScript](/blog/redact-pii-before-openai-typescript) and [Redact PII in LLM prompts with Python](/blog/redact-pii-llm-prompts-python). To restore a whole JSON reply in one call, `restoreJson` (npm 0.8.0) and `restore_json` (PyPI 0.4.0) parse it, restore each string value and re-escape it. Never run `restoreSync` or `restore_sync` over serialized JSON as text: an original containing a quote can add keys. [Why, and what changed in 0.8.0](/blog/sether-0-8-0-json-safe-restore). ## What the JSON-key detection catches The identity pack fires when a JSON key contains a class word, across the naming styles real codebases actually use: snake_case, kebab-case, camelCase, and spaced keys. It matches the key, then applies the same value validation it uses for prose. | Class | Example keys it fires on | Token | | --- | --- | --- | | Name | "customer_name", "full_name", "firstName", "patient name" | | | Date of birth | "date_of_birth", "dob", "birthDate" | | | Passport | "passport_number", "passport", "passportNo" | | | Address | "billing_address", "shipping_addr", "home address" | | The type prefix gives the model a field hint. It does not establish anonymity: other fields or the surrounding record may still identify the person. Check which details the task needs, and validate whether the model preserves the replacement correctly. ## The redacted output is still valid JSON This is the detail that makes structured redaction usable rather than a nice demo. If a detector captures a value greedily, it will swallow the closing quote and the comma, and the payload you send to the model is no longer parseable JSON. Sether's address detector, the one most prone to running long, stops its capture at the closing double quote. ```json // in { "billing_address": "12 Marina Road, Lagos", "priority": "high" } // out { "billing_address": "", "priority": "high" } ``` The illustrated token sits inside the value’s quotes, leaving that sample’s other fields intact. Make parsing an assertion in your own fixtures rather than assuming arbitrary serialized input is covered. On the return path, use JSON-aware restoration and validate keys, types and application permissions before using the result. ## Why a loose key match does not over-fire The obvious risk with matching on keys is that "name" appears in keys that are not personal at all: `"filename"`, `"username"`, `"hostname"`, `"nickname"`. Matching the key alone would redact half your config. Sether guards against this by validating the value, not trusting the key. - Names must look like names: an uppercase-first token, checked against a denylist of placeholders and common words. `"filename": "report.pdf"` and `"username": "amara_dev"` do not fire, because "report.pdf" and "amara_dev" are not name-shaped. - Dates of birth must be calendar-plausible. `"birth_place": "Lagos"` never fires (Lagos is not a date), and `"date_of_birth": "2099-01-01"` is rejected as an implausible future year. - Passports must carry at least one digit, so `"passport": "VALIDONLY"` is ignored while `"passport_number": "A1234567"` is caught. - The match requires the `"key": "value"` shape, so ordinary prose that merely mentions a class word ("the name is unknown right now") is left completely alone. A permissive key match creates a candidate, and the value validator narrows it. That is a useful heuristic, not a measured accuracy guarantee. Include both names your users actually have and innocent values such as a document title so the trade-off is visible. ## Streaming structured data Streaming serialized records adds both text boundaries and JSON framing. Sether uses a configurable hold-back distance (`safeDistanceBytes`, default setting 256) and bounded buffering safeguards, which need long-value and custom-detector tests. If a model streams one JSON document as content fragments, collect it before JSON-aware restore; a stream of independently parseable event envelopes is not the same thing. The failure mode this prevents, and how it was caught in testing, is written up in [The streaming redaction chunk-boundary bug](/blog/streaming-llm-redaction-chunk-boundary-bug). For structured data the lesson is the same: never run redaction per-chunk without a hold-back, or a value straddling a boundary escapes. | Acceptance check | Example assertion | | --- | --- | | Syntax | The redacted payload and JSON-aware restored reply both parse. | | Structure | Keys, nesting and field types match the application schema. | | Coverage | Expected sample names and identifiers were replaced; harmless fields were retained. | | Adversarial originals | Quotes, backslashes and nested serialized strings cannot become new keys. | | Execution | A restored destination is still checked against authorization and business rules. | The key-detection versions described above are historical introduction points. For structured reply restoration, use the APIs added in [npm 0.8.0](/blog/sether-0-8-0-json-safe-restore) or [Python 0.4.0](/blog/sether-python-0-4-0-json-safe-restore). These are full MIT library capabilities, independent of optional Pro workflows in the official tools. ## Limitations, honestly - This catches PII whose key names the class. A value sitting under an opaque key like `"field_7": "Amara Okafor"` has no signal in the key, so the JSON-key path will not fire on it; that is the free-text NER problem, not the structured one. - The basic pack (email, phone, card, SSN, IPv4/IPv6, IBAN) fires on values everywhere regardless of key, because those are self-describing. The key-anchoring only matters for name, DOB, passport, and address, which are not. - Redaction is a technical control that supports data minimisation. It does not by itself make a data flow lawful. The legal reasoning for sending customer data to a provider is in [Is it GDPR-compliant to send customer data to OpenAI?](/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai). - The default vault is in-memory. If redact and restore run in different processes, back it with a shared vault or the tokens cannot be resolved. You can paste a JSON payload straight into the [live sandbox](/sandbox) and watch the fields tokenize, the full API is at [/docs](/docs), and the source for both packages is at [github.com/raeven-co/sether](https://github.com/raeven-co/sether). ## Frequently asked questions ### Does redacting a JSON value break the JSON structure? The illustrated fixtures preserve their JSON syntax, but validate your actual payloads. Parse the edited document and assert that expected keys and types remain. For restoration, use restoreJson/restore_json or restore parsed string values; raw text substitution into serialized JSON can change its structure. ### Will it redact keys like "filename" or "username" by mistake? Those illustrated values are rejected by the name validators, but this is not a guarantee for every string under those keys. Test harmless lookalikes and supported personal-name formats in your own records; keys and value validators both influence the result. ### What if the personal data is under a meaningless key like "field_7"? An opaque key provides no identity-class cue. Other enabled detectors may still recognize values such as an email or card candidate. Names and addresses may need a known-term detector, schema-specific handling or evaluated NER; none guarantees complete coverage. ### Which versions have JSON-key detection? The @raeven-co/sether npm package from 0.6.0 and the sether Python package from 0.2.0. It is part of the opt-in identity pack, so enable identityDetectors (identity_detectors in Python) when you construct the instance. --- Source: https://sether.ai/blog/tokenize-pii-before-ai-document-generation # Tokenize PII before AI document generation: keep real customer data out of the model > Many document workflows can separate drafting from filling sensitive fields. Replace supported personal values before the model call, keep the mapping in your application and restore recognised replacements at rendering. Validate the draft, fields and final output. This withholds matched originals; unnecessary context, missed values and destination escaping still need attention. Published 2026-07-23, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - Generating a document with AI is really two jobs: the model structures and drafts, then a template engine fills in the real values. - A drafting step often needs field roles rather than literal identities. Derive any required facts deliberately instead of assuming the model can infer them from tokens. - Redact the JSON payload before the AI call, keep the token-to-value map in your vault, and restore into the final render. - Pre-request minimisation complements provider retention and contract controls; it does not remove every identifying detail or obligation. - Sether does the redact and restore in TypeScript and Python, including PII that lives in JSON keys. ## AI document generation is two jobs, not one When a product generates a document with AI, whether that is a contract, a letter, a filled-in template, or a PDF, it feels like one action. Under the hood it is almost always two. First, a model decides structure and wording: which clauses apply, how to phrase a paragraph, what tone fits. Second, a deterministic step drops the actual values into place: this customer's name, this date of birth, this address. Those two jobs have very different data needs. The drafting job needs to know that a name goes here and an address goes there. It does not need to know that the name is Amara Okafor. The filling job needs the real values, but it is your own code running on your own infrastructure, not a call to a third-party model. Once you see the split, the privacy design almost writes itself. ## The common mistake: real values through the AI phase A one-shot implementation can send a complete customer record even when drafting needs only a subset of its fields. Review that payload first. Provider handling and retention depend on the service and settings; avoiding unnecessary disclosure is a separate design decision. ```json { "customer_name": "Amara Okafor", "date_of_birth": "1990-05-12", "passport_number": "A1234567", "billing_address": "12 Marina Road, Lagos", "template": "welcome_letter" } ``` Provider terms, retention and access controls still matter for what you send. A two-stage design lets you reduce that payload before those controls apply, while keeping final field insertion under your own application’s rules. ## The pattern: tokens through the AI, originals only at render Replace supported personal fields before the AI phase. Let the model draft around the resulting references, then check the draft and restore known replacements when your own code renders the document. Review the remaining context too: a distinctive case description can identify someone even after names are removed. 1. Redact the payload. Personal fields become tokens: , , . The token-to-value map stays in your vault. 2. Call the model with the tokenized payload. It structures and drafts the document, referring to wherever the name belongs. 3. Restore. Your render step swaps each token back to its original value as it produces the final document or PDF. Tokens carry field hints, not the facts inside the originals. The model can omit a token or put it in the wrong place. Validate the output against a template or schema and calculate sensitive business decisions in your own code instead of expecting tokenization to preserve every aspect of reasoning. ## Wiring it with Sether Because document pipelines pass structured records, most of the personal data is in JSON values whose keys name them (`customer_name`, `date_of_birth`). Sether's identity pack anchors on those keys, so a single redact call tokenizes the whole payload. The how and why of that key-anchoring is in [How to redact PII in JSON and structured data before an LLM](/blog/redact-pii-in-json-before-llm). ```ts import { Sether, basicDetectors, identityDetectors, redactSync, restoreSync, } from '@raeven-co/sether'; const sether = new Sether({ detectors: [...basicDetectors, ...identityDetectors], }); // 1. Redact the record before the AI phase const safePayload = redactSync(JSON.stringify(record), { detectors: sether.detectors, vault: sether.vault, }); // 2. The model drafts the document from tokens only const draft = await callModel(safePayload); // provider sees , never the name // 3. Restore real values as you render the final document const finalDocument = restoreSync(draft, { vault: sether.vault }); ``` ```python from sether import Sether, basic_detectors, identity_detectors import json sether = Sether(detectors=[*basic_detectors, *identity_detectors]) safe_payload = sether.redact_sync(json.dumps(record)) draft = call_model(safe_payload) # provider sees tokens only final_document = sether.restore_sync(draft) ``` If the draft is JSON, for example template fields for a document API, restore it with `restoreJson` (npm 0.8.0) or `restore_json` (PyPI 0.4.0) instead. Plain text restore puts originals back unescaped, so a value containing a quote could change the structure. [What changed in 0.8.0](/blog/sether-0-8-0-json-safe-restore). Both stages need access to the matching retained vault. One instance is the simplest arrangement. Separate workers need a compatible shared mapping strategy you operate; the TypeScript lookup contract is synchronous, so an ordinary async Redis client is not a drop-in vault. Cover the intended rendering delay, isolate conversations and decide what happens if an entry expires. ## How this complements provider controls A provider’s training and retention controls describe what happens to received data. Pre-request replacement changes which matched values are sent in the first place. These controls can complement each other; neither establishes that the remaining document context is anonymous. Use a precise statement in your review: “these fields are replaced before the provider request, these fields remain, and known replacements restore only at render.” That is more useful than “we never send personal data” when the surrounding record has not been assessed. For legal context, see [Is it GDPR-compliant to send customer data to OpenAI?](/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai). ## What the AI still does well on tokens A model may help draft structure and wording without a literal name or address. Conditions need explicit facts, though. If a document requires an under-18 clause, your code can compute the required age category locally and provide that minimal fact; the model cannot infer age from an opaque DOB token. Validate the resulting clause before rendering. Evaluate quality on your actual templates. Test missing, repeated and misplaced fields, not only natural wording. Email-format checks, date calculations, recipient validation and authorization should be deterministic steps rather than assumptions about what the model understood. A useful fictional fixture is a welcome letter with a name, an email and a locally computed age category. Assert that the model request contains replacements and the category, the reply contains only allowed fields, and the final renderer escapes restored text for its destination. JSON-safe restore protects JSON syntax; it does not also escape HTML, SQL or a PDF template. | Stage | Keep the responsibility here | | --- | --- | | Before the model | Select fields, compute required facts and replace supported values. | | After drafting | Validate required fields, token placement and document rules. | | At rendering | Restore known mappings and escape for the destination format. | | On failure | Stop or request review rather than inserting guessed originals. | ## Limitations, honestly - This works because document pipelines separate drafting from filling. If your generation genuinely fuses the two and the model must emit the finished value inline, you keep the token in the draft and restore at output, which is the pattern above; but a design that truly needs the model to reason over the real value is out of scope for redaction. - Opaque keys provide no identity-class cue. Other detectors may recognize self-describing values, while names or addresses can need schema-specific handling, known terms or evaluated NER. - Redaction can reduce disclosed detail; it does not itself determine lawfulness or remove obligations for the remaining context. - Keep redact and restore on a shared vault if they run in different processes, or the tokens will not resolve at render time. You can watch a JSON record tokenize and restore in the [live sandbox](/sandbox), read the full API at [/docs](/docs), and see both packages at [github.com/raeven-co/sether](https://github.com/raeven-co/sether). The concepts underneath are in [the complete guide to PII redaction for LLM apps](/blog/pii-redaction-for-llm-apps-complete-guide). ## Frequently asked questions ### Does the document quality suffer if the AI only sees tokens? It can. Field tokens may preserve enough structure for some templates, but the model can omit or misplace them and cannot infer facts hidden inside a value. Test representative templates, derive necessary facts locally and validate the draft before rendering. ### How is this different from trusting the provider not to train on my data? They address different stages. Provider controls govern received data; tokenization withholds matched originals from that edited request. Remaining context can still identify someone, so keep the provider review and avoid a blanket no-personal-data claim. ### Where do the real values get filled back in? Restore recognised replacements at the final render step in code you operate, while the matching mapping is available. Validate the output first and escape originals for the destination format. Restoring text is not authorization to generate or send a document. ### Does this handle PII that sits in JSON fields? Yes. Document pipelines pass structured records, so Sether's identity pack anchors on JSON keys like customer_name and date_of_birth, tokenizing the whole payload in one redact call while keeping the output valid JSON. --- Source: https://sether.ai/blog/redact-pii-before-openai-typescript # How to redact PII before sending prompts to OpenAI (TypeScript) > In TypeScript, redact supported matches before the provider call and restore recognised reply tokens with the matching vault. `@raeven-co/sether` offers stream transforms, one-shot helpers and wrappers for supported client paths. Inspect the edited payload and test your response format; JSON replies need JSON-aware restoration rather than raw text replacement. Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - The API sends the payload you supply. HTTPS protects transport, but the intended provider can read the request content. - Keep the matching vault available in both directions; reusing one instance is the simplest configuration. - wrapOpenAI wraps supported client paths; test the SDK methods and response formats your app actually uses. - Streaming helpers preserve context across chunks, with bounded safeguards and format-specific limits to test. - Test replacement coverage, known-map restoration, missing mappings and the actual outbound request; one round-trip assertion is only the start. ## What actually leaves your server when you call OpenAI? Open the network tab on any AI chat feature and look at the request body. The `messages` array contains exactly what the user typed: their name, their email, the card number they pasted while asking about a refund. In a local demo, inspect a fictional prompt in the request body. A visible sample email there is application content the provider will receive; it does not mean HTTPS is disabled. Provider SDKs do not automatically remove personal details simply because those details look sensitive. The fix is a redaction layer in the request path: replace personal values with tokens before the request leaves, and put the real values back in the reply. This post is the hands-on TypeScript version; the concepts are covered in the [complete guide to PII redaction for LLM apps](/blog/pii-redaction-for-llm-apps-complete-guide). ## Install ```bash npm install @raeven-co/sether ``` The package is MIT-licensed and provides ESM, CJS and a browser entry at `@raeven-co/sether/browser`. Its one declared TypeScript runtime dependency is `libphonenumber-js` for phone validation. Check the selected release’s engine requirements and build footprint when adding it to your deployment. ## The 3-step round trip The core pattern is three steps: redact the prompt, call OpenAI, restore the reply. One Sether instance handles both directions, because the instance holds the vault that maps tokens back to real values. ```ts import { Sether } from '@raeven-co/sether'; import { Readable } from 'node:stream'; import { text } from 'node:stream/consumers'; const sether = new Sether(); // 1. Redact the user message before it leaves const safePrompt = await text( Readable.from([userMessage]).pipe(sether.redact()) ); // 2. Call OpenAI with the redacted prompt const completion = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: safePrompt }], }); // 3. Restore real values in the reply, same instance const reply = await text( Readable.from([completion.choices[0].message.content ?? '']) .pipe(sether.restore()) ); ``` If you are not streaming and just want a string in and a string out, the exported `redactSync(text, { detectors, vault })` and `restoreSync(text, { vault })` functions skip the stream plumbing. The important rule is unchanged: both sides must use the same vault, usually `sether.vault`, because that is where tokens map back to values. The retained vault is the requirement; the object identity is a convenient way to satisfy it. Separate instances can use the same configured vault. The deeper explanation is in [Reversible PII redaction: how redact-then-restore works](/blog/reversible-pii-redaction-redact-restore). ## Can I do this without changing my call sites? Sether ships `wrapOpenAI` for supported client methods, redacting request text and restoring supported reply content. Keep a fixture for each method, SDK shape and response format you rely on; a wrapper is not evidence that every future SDK feature is covered. One design note worth knowing: the wrappers are structurally typed. Sether never imports the OpenAI SDK as a dependency, so you do not inherit a version pin or an extra package. The same applies to `wrapAnthropic`, `wrapFetch`, and `createExpressMiddleware`. ## What gets detected out of the box? | Pack | Detectors | Enabled by default | | --- | --- | --- | | Basic | Email, phone (libphonenumber-js), credit card (regex + Luhn), SSN, IPv4, IPv6, IBAN (mod-97) | Yes | | Secrets | AWS access key, OpenAI key, Anthropic key, GitHub PAT, Slack token, Stripe key, JWT, high-entropy strings | Opt-in | | Identity | Label-anchored name, DOB, passport, address, with multilingual labels across Latin, CJK, Cyrillic, and Arabic scripts | Opt-in | A card candidate must pass Luhn and an IBAN candidate must pass mod-97 in the configured detectors. Those checks reduce some invalid matches; they do not prove the candidate belongs to a person or establish a measured false-positive rate for your inputs. The opt-in secrets pack includes fixed-format and entropy-based detectors. Entropy is a signal, not proof of a secret, so test ordinary high-entropy content and the actual key formats you use. Enable additional packs deliberately and review both missed and unwanted replacements. ## How do I handle streaming responses? Most chat UIs stream over Server-Sent Events, and streaming is where naive redaction breaks. An email split across two chunks as `sarah@ac` and `me.com` passes a per-chunk regex on both sides. Sether keeps recent text to provide context across chunks, using `safeDistanceBytes` (default 256) and bounded buffering guards. For SSE, use the framing-aware helpers and test your event shape. A JSON document assembled inside streamed content fragments needs to be collected and restored as JSON; see the [0.8.0 release limits](/blog/sether-0-8-0-json-safe-restore). The [chunk-boundary post](/blog/streaming-llm-redaction-chunk-boundary-bug) explains the failure mode with a controlled example, partition tests, encoding questions and documented buffer limits. ## What do the tokens look like? Tokens include a type prefix and a fresh identifier per match, such as ``. The prefix hints at the field’s role. Test whether the model preserves and places tokens correctly; it may still change or omit one, and surrounding text may remain identifying. The default vault is `MemoryVault`, an in-process LRU holding 10,000 entries with a one-hour TTL. The interface is pluggable, but TypeScript vault lookups are synchronous: do not substitute an async Redis get() implementation and expect text restore to await it. Use a compatible mapping strategy and test missing entries. ## How do I test the integration? A round-trip assertion checks that known replacements resolve with the corresponding vault. Add a separate assertion that the outgoing sample no longer contains the values you intended to replace. Otherwise an unchanged string could pass a round-trip test without detecting anything. ```ts import { Sether } from '@raeven-co/sether'; import { Readable } from 'node:stream'; import { text } from 'node:stream/consumers'; import assert from 'node:assert'; const roundTrip = async (input: string) => { const sether = new Sether(); const redacted = await text( Readable.from([input]).pipe(sether.redact()) ); assert.ok(!redacted.includes('sarah@acme.com')); // value is gone const restored = await text( Readable.from([redacted]).pipe(sether.restore()) ); assert.strictEqual(restored, input); // and comes back exactly }; await roundTrip('Hi, I am Sarah, my email is sarah@acme.com'); ``` Also test chunk partitions, repeated values, missing or expired mappings and a captured request body from your actual wrapper. For structured replies, validate the parsed keys and schema after JSON-aware restore. The [current release account](/blog/sether-0-8-0-json-safe-restore) explains why text substitution is not safe for serialized JSON. The full TypeScript SDK, including decoys and restoration, is free under MIT with no Sether usage quota. Optional Pro applies to convenience workflows in official tools, not these library calls. If you need a realistic stand-in rather than a typed token, evaluate the alias APIs separately and validate the task with those stand-ins. [Pricing](/pricing) explains the product boundary. ## Limitations, honestly - Free-text names are not caught by the core package. "Tell Chidi I will send the money Friday" has no label and no structure, so it needs the NER add-on. The trade-offs are in [Regex vs NER for PII detection](/blog/regex-vs-ner-pii-detection). - The identity pack is label-anchored. It catches "name: Sarah Chen" in many scripts, but not names floating in prose. - The default vault is in-memory. If your redact and restore steps run in different processes, plug in a shared vault or the tokens cannot be resolved. - Redaction is a technical measure that helps with data minimisation. It does not, on its own, make you compliant with anything. See [the GDPR post](/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai) for the legal side. You can try all of this without installing anything in the [live sandbox](/sandbox), and the full API is documented at [/docs](/docs). The source is at [github.com/raeven-co/sether](https://github.com/raeven-co/sether). ## Frequently asked questions ### Does this work with the OpenAI streaming API? Supported stream paths can restore recognised tokens, and framing-aware helpers are available for SSE. Test the exact transport, final fragment and missing-map behavior. Streamed JSON-mode content has additional limits described in the [0.8.0 release](/blog/sether-0-8-0-json-safe-restore). ### Do I need to change my OpenAI SDK version? The wrapper uses structural types rather than importing the provider SDK as a runtime dependency. That removes a package dependency, not the need for compatibility tests: changed SDK methods or response shapes can still require integration work. ### Will OpenAI answer worse because it sees tokens instead of real values? It can change output quality or correctness. Evaluate your task with redacted fixtures, check for missing or altered tokens and validate restored output. Do deterministic checks such as email-format validation in your own code when possible. ### Is this enough for GDPR? Redaction is a technical measure that helps, particularly with Article 5 data minimisation and Article 32 technical measures. It does not replace processor agreements or a lawful basis. The legal reasoning is in our GDPR post. --- Source: https://sether.ai/blog/redact-pii-llm-prompts-python # How to redact PII from LLM prompts in Python > In Python, call `sether.redact_sync(prompt)` before a provider request and restore recognised reply tokens with the same retained vault. Stream APIs and wrappers cover supported integration paths. For JSON output, use JSON-aware restore on a complete document or parsed values; inspect outgoing fields and test missing mappings instead of assuming every sensitive detail was caught. Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - redact_sync and restore_sync provide a basic text round trip. Both directions need the matching retained vault. - Extras add drop-in wrappers: pip install "sether[openai]" for wrap_openai, plus wrap_anthropic and wrap_httpx. - SetherASGIMiddleware and SetherWSGIMiddleware cover FastAPI and Flask with no extra dependency. - Sync and async streaming are supported, with bounded buffering and transport-specific behavior to verify. - The Python 0.4.0 release adds JSON-safe restore and strict unresolved-token handling. Read the upgrade notes for changed structured-output behavior. ## Why does my Python app leak PII to LLM providers? Because that is the default. Whatever string you pass to the OpenAI or Anthropic client goes to their servers verbatim, and user input carries names, emails, and pasted card numbers more often than anyone expects. The fix is a redaction step in the request path: swap personal values for tokens before the call, swap them back in the reply. This post shows how in Python; the background and the tool landscape are in [the complete guide](/blog/pii-redaction-for-llm-apps-complete-guide). ## Install ```bash pip install sether # or with the wrapper for your LLM client: pip install "sether[openai]" pip install "sether[anthropic]" pip install "sether[httpx]" ``` Install `sether` from PyPI and review the requirements for the release you deploy. The [Python 0.4.0 release notes](/blog/sether-python-0-4-0-json-safe-restore) cover the JSON-safe restore APIs and compatibility changes. Phone detection uses the `phonenumbers` package. ## The redact-then-restore round trip ```python from sether import Sether sether = Sether() user_message = "Hi, I'm Sarah, my email is sarah@acme.com" safe = sether.redact_sync(user_message) # -> "Hi, I'm Sarah, my email is " reply = call_openai(safe) # your existing call restored = sether.restore_sync(reply) # real values back ``` Redaction stores token-to-original entries and restoration reads those entries. Reusing one instance is the simplest setup. Separate instances can restore through a shared configured vault, while a fresh default vault has no earlier mappings. Test the behavior when entries are missing, expired or evicted. Why the design works this way, and what the vault stores, is covered in [Reversible PII redaction: how redact-then-restore works](/blog/reversible-pii-redaction-redact-restore). ## Which integration path should I use? | Your setup | Use | Install | | --- | --- | --- | | Direct OpenAI SDK calls | wrap_openai | pip install "sether[openai]" | | Direct Anthropic SDK calls | wrap_anthropic | pip install "sether[anthropic]" | | Any HTTP client via httpx | wrap_httpx | pip install "sether[httpx]" | | FastAPI (or any ASGI app) | SetherASGIMiddleware | pip install sether (built in) | | Flask (or any WSGI app) | SetherWSGIMiddleware | pip install sether (built in) | | Full control, custom pipeline | redact_sync / restore_sync | pip install sether | The wrappers cover supported client request and reply shapes. The extras install the libraries those wrappers use. Keep integration fixtures for your selected SDK version and response modes; “drop-in” does not mean that every endpoint or future shape is automatically handled. ## How do I cover a whole FastAPI or Flask app? If a service forwards text from several routes, middleware can centralize part of the transformation. Verify which request and response fields it touches and keep its placement deliberate. Data redacted on inbound requests reaches your route handler already transformed; that is a different boundary from wrapping only the outbound provider call. Place it like any other ASGI middleware in FastAPI, and scope it to the routes that forward text to a provider rather than your whole API. `SetherWSGIMiddleware` does the same job for Flask and other WSGI apps. Both ship in the base package with no extra dependency. ## Does it handle streaming responses? Yes, both sync and async streaming. This matters because streaming is where naive redaction silently fails: a value split across two chunks passes a per-chunk regex on both sides. Sether preserves surrounding text between chunks with bounded buffering, rather than scanning each chunk in isolation. Configure and test long values, final fragments and your actual event framing. The [chunk-boundary post](/blog/streaming-llm-redaction-chunk-boundary-bug) explains both the failure mode and the limits of bounded buffering. ## What do tokens and the vault look like? Tokens have a type prefix and fresh identifier per match. They reference vault entries rather than encoding originals. The model can still alter or omit them, and unmatched text or context can still identify someone, so validate the edited prompt and resulting reply. The default vault lives in process memory. The library does not send that mapping to the model provider. If you implement a persistent or shared vault, its access controls, tenant separation and retention become part of your application’s design. ## What gets detected? - **Basic pack (default):** email, phone (via phonenumbers), credit card with Luhn validation, SSN, IPv4, IPv6, IBAN with mod-97 validation. - **Secrets pack (opt-in):** AWS access keys, OpenAI keys, Anthropic keys, GitHub PATs, Slack tokens, Stripe keys, JWTs, and high-entropy strings (Shannon entropy of 3.5+ bits per character). - **Identity pack (opt-in):** label-anchored name, date of birth, passport, and address, with labels recognised across Latin, CJK, Cyrillic, and Arabic scripts. ## How do I test it? Assert the round-trip property: restoring a redacted string returns the original exactly. Also assert the redacted form no longer contains the raw value. ```python from sether import Sether def test_round_trip(): sether = Sether() original = "Reach me at sarah@acme.com or +14155552671" redacted = sether.redact_sync(original) assert "sarah@acme.com" not in redacted assert "+14155552671" not in redacted restored = sether.restore_sync(redacted) assert restored == original ``` For a production acceptance fixture, capture a fictional outbound request, a reply that echoes its token, a reply with an unknown token, and an expired vault entry. If the result drives a tool, validate arguments and authorization before acting. A successful restore does not authorize sending an email or writing a record. The full Python library remains free under MIT, including decoys and restore. Optional Pro in the official tools is a separate product tier; there is no Sether per-redaction quota in these SDK calls. See [pricing](/pricing) for that distinction. ## Limitations and maturity, honestly Follow the release notes for the version you install rather than assuming perfect feature parity from a shared design. Python 0.4.0 and TypeScript 0.8.0 both add JSON-aware restoration, with some different streaming behavior. Review both articles if your system mixes the two languages. - Free-text names ("tell Chidi I sent it") are out of scope for pattern and label detection. That class needs NER; see [Regex vs NER for PII detection](/blog/regex-vs-ner-pii-detection). - The default vault is in-memory, so redact and restore must happen in the same process unless you plug in a shared store. - Redaction is a technical measure that helps with data minimisation. It is not a compliance certificate on its own. Docs live at [/docs/python](/docs/python), the source is at [github.com/raeven-co/sether](https://github.com/raeven-co/sether), and you can try detection in the [browser sandbox](/sandbox) first. ## Frequently asked questions ### How is this different from Microsoft Presidio? Presidio offers configurable detection and anonymization, including reversible operations such as decrypt. Sether focuses on a request-path redact/vault/restore flow. Compare the actual integration and labeled detection fixtures; neither project’s name establishes better accuracy for your data. See the [comparison](/blog/lightweight-presidio-alternatives-typescript). ### Does it work with async FastAPI handlers? Yes. Async streaming is supported alongside the sync API, and SetherASGIMiddleware is native ASGI, so it sits in an async FastAPI stack normally. ### Is the Python package as complete as the TypeScript one? They share a general design but have language- and version-specific behavior. The [Python 0.4.0 release notes](/blog/sether-python-0-4-0-json-safe-restore) and [Python docs](/docs/python) describe supported APIs and current structured-output limits. Test the integration you intend to deploy. ### Does redacting prompts make me GDPR-compliant? No single tool does. Redaction is a technical measure that helps, especially with data minimisation (Article 5) and technical measures (Article 32). You still need processor agreements and a lawful basis for the processing. --- Source: https://sether.ai/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai # Is it GDPR-compliant to send customer data to OpenAI? > An OpenAI API integration can be compatible with GDPR, but a product name or redaction tool cannot establish that. Assess whether GDPR applies, the purpose and lawful basis, provider roles and contracts, transfers, retention and risk. Minimise the request and review residual identifying context. This is an engineering discussion, not a legal conclusion about your service. Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - GDPR scope depends on the processing and territorial connection, including EU establishment or certain targeting and monitoring activities; citizenship alone is not the test. - Controller, processor and sub-processor roles depend on the actual activity and terms. Identify them before treating a contract checklist as complete. - Review processing locations, subprocessors and applicable transfer safeguards for the exact provider service. - Redaction can reduce disclosed detail. It does not automatically anonymize a message, remove all obligations or establish compliance. - Keep a reviewable data-flow record and seek qualified advice for your situation. This post is educational, not legal advice. ## When does GDPR apply to an LLM prompt? First establish scope. GDPR can apply through an EU establishment or, for organisations outside the EU, through relevant offers of goods or services to people in the Union or monitoring of their behavior there. A person’s citizenship alone is not the test. The [EU’s business guide](https://europa.eu/youreurope/business/governance-and-sustainability/digital-and-data-compliance/data-protection-gdpr/index_en.htm) explains these starting points. There is no prompt exception. If your support bot forwards "Hi, I'm Sarah Chen, sarah@acme.com, my card was charged twice" to the OpenAI API, you just processed personal data and disclosed it to a third party. The same is true for background jobs, evals, and fine-tuning datasets, not just live chat. Start with a fictional request that represents the feature, then inventory its fields and recipients. An email is easy to spot; a ticket reference or distinctive incident description can also connect the request to a person. Calling the payload “synthetic” or “redacted” is not a substitute for checking what it contains. ## What is OpenAI, legally, in this relationship? The roles are functional. A controller determines purposes and means; a processor acts on a controller’s behalf. Your business may itself act as a processor for a customer. Review the activity and provider terms before deciding which role applies; do not infer every relationship from the fact that an API call was made. Where a controller uses a processor, [GDPR Article 28](https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng#art_28) requires a binding arrangement containing specified terms, in writing including electronic form. Locate the agreement that actually governs your account. Not having personally signed a separate PDF does not prove that nobody accepted applicable terms. ## What paperwork do you actually need? 1. **Locate the governing terms.** Ask your account owner which entity, product terms and data-processing arrangement cover the service; keep the version reviewed. 2. **Map the recipients.** Record the model service, hosting, subprocessors, observability and support paths rather than assuming the visible API endpoint is the whole flow. 3. **Record the purpose and lifecycle.** Show why each field is sent, where replies and logs persist, who can access them and how deletion works. 4. **Ask your privacy adviser which duties apply.** Lawful basis, transparency, records, customer authorizations and impact assessment requirements depend on the processing; a generic blog checklist is not a determination. OpenAI’s [API data controls](https://developers.openai.com/api/docs/guides/your-data) state that API inputs are not used for training unless you opt in. They also distinguish abuse-monitoring logs from application state: default logs may be retained up to 30 days, while endpoint and feature behavior varies. Zero Data Retention has eligibility and feature limits. Review the controls for your actual account and endpoint; consumer chat settings are a separate service question. ## Does sending data to a US company break the transfer rules? A company’s name or headquarters does not by itself describe every transfer in your integration. Check the contracting entity, processing locations, remote access and subprocessors against the actual service configuration. For transfers outside the EU, adequacy decisions and appropriate safeguards such as standard contractual clauses are possible routes, with conditions that must be assessed. The [EU business guide’s transfer section](https://europa.eu/youreurope/business/governance-and-sustainability/digital-and-data-compliance/data-protection-gdpr/index_en.htm#data-transfer-outside-the-eu) is a starting point, not confirmation that a particular vendor or transfer qualifies. For an engineering review, keep the provider’s documented locations and selected settings beside the contract version. If your team relies on certification, contractual clauses or residency controls, have the responsible adviser verify the current coverage and any remaining access paths. ## What technical measures does GDPR expect? The [Commission’s GDPR principles guide](https://commission.europa.eu/law/law-topic/data-protection/information-business-and-organisations/principles-gdpr_en) connects purpose, minimisation, retention and safeguards. For an AI feature, translate that into an engineering question: does drafting this refund reply require the actual email address, or just an address-shaped reference? Try removing unnecessary fields before adding detection. Then use replacement for supported values the task can do without. Verify the edited request, including metadata and error paths. Redaction is one useful control alongside access restrictions, retention settings and review; it is not universally the strongest control for every use case. Reversible replacement keeps a way to reconnect data to a person. The [Commission explains that re-identifiable pseudonymised data remains personal data](https://commission.europa.eu/law/law-topic/data-protection/information-business-and-organisations/application-gdpr_en). Identifying context can remain even without the mapping. The implementation mechanics are in the [complete guide to PII redaction for LLM apps](/blog/pii-redaction-for-llm-apps-complete-guide); they are not a legal exemption. ## What have regulators signaled, and where does the EU AI Act fit? Treat privacy review as a description of what the feature actually does, rather than a badge conferred by a model provider or a library. A useful review can show the request before and after minimisation, the destinations, retention choices and the handling of errors without circulating real customer records. The EU AI Act is a separate, role- and use-dependent framework. Its obligations have a staged timetable; there is no single deadline or fine that describes every application calling an LLM. Check the [Commission’s current AI Act page](https://digital-strategy.ec.europa.eu/en/policies/regulatory-framework-ai) for the applicable categories and dates. Neither an SDK installation nor this article certifies compliance with it. ## What do you need for each type of data? | What the feature handles | Questions for the review | Engineering evidence | | --- | --- | --- | | Your own non-personal or truly synthetic content | Does later user input change that assumption? | Representative fixtures and a field inventory | | Names, emails or customer references | What purpose, roles, terms and disclosures apply? | Edited requests, retention settings and access paths | | Support histories or CRM records | Which contextual details remain identifying? | Minimisation decisions and false-negative tests | | Health or other sensitive information | Which additional conditions and risk assessments apply? Get qualified advice. | Restricted test data, access controls and a documented launch decision | ## So, is it compliant or not? There is no responsible yes-or-no answer for an unspecified application. The conclusion depends on the actual processing, agreements and controls. A redaction demo is evidence about one technical step; it is not evidence that the entire data flow meets the law. Prepare a short review packet: a fictional request and reply, the field inventory, a diagram of recipients, the provider settings and terms reviewed, and the chosen behavior when detection or restoration fails. That gives your adviser concrete facts to assess and gives engineering a record it can maintain as the feature changes. **This post is not legal advice.** It is an engineer's map of the terrain, written to help you ask your lawyer better questions. For decisions about your specific data and contracts, consult a qualified data protection professional. ## Frequently asked questions ### Does OpenAI train on my API data? OpenAI’s API documentation says inputs are not used for training unless you opt in. Retention and feature settings are separate. Check the [API data controls](https://developers.openai.com/api/docs/guides/your-data) for your endpoint and account; no-training alone does not determine whether your processing is lawful. ### Is redacted or pseudonymised data still personal data under GDPR? It can be. Pseudonymisation is not automatically anonymisation; retained mappings and other identifying information matter. The [Commission’s explanation](https://commission.europa.eu/law/law-topic/data-protection/information-business-and-organisations/application-gdpr_en) makes clear that re-identifiable pseudonymised data remains personal data. ### Do I need consent from users before sending their data to OpenAI? Consent is not the only possible lawful basis, and no basis should be assumed simply because a feature is useful. Describe the purpose, data and recipients to a qualified privacy adviser and ask which basis and other conditions apply to that processing. ### Does using an EU region or zero-retention endpoint solve this? They address particular aspects of location and retention, with provider-specific limits. They do not determine every role, contract or minimisation question. Review the actual service configuration and processing rather than treating a region selector or retention label as a compliance certificate. --- Source: https://sether.ai/blog/lightweight-presidio-alternatives-typescript # Microsoft Presidio alternatives for Node.js and TypeScript (2026) > For a TypeScript service, compare the integration you will operate: Presidio’s Python detection and configurable anonymization, an LLM guardrail suite, an in-process Sether redact/vault/restore flow, or your own detectors. Presidio supports reversible operations too. Evaluate actual detection coverage, state management, streaming behavior and deployment cost; there is no universal winner. Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - Presidio offers configurable detection and anonymization, including reversible encrypt/decrypt operations. Do not dismiss it as one-way-only. - A Node service can call a Python service, but that adds a sensitive-data hop to secure and operate. - Sether provides TypeScript and Python SDKs with a vault-backed restore flow. The full SDKs are free under MIT, including decoys and restore. - Choose a broader guardrail suite if the problem includes checks beyond PII; evaluate each enabled scanner and its deployment needs. - Compare labeled fixtures and actual transport behavior. A small install or a successful demo does not establish better accuracy or security. ## What is Presidio, and what is it great at? Microsoft Presidio is an open-source PII detection and anonymization toolkit from Microsoft, written in Python. It splits the job into an analyzer (NER via spaCy models plus a large set of pattern recognizers) and an anonymizer (configurable operators: replace, mask, hash, encrypt). Presidio exposes configurable recognizers and anonymization operators. Its [anonymizer documentation](https://presidio.dataprivacystack.org/anonymizer/) also includes a deanonymizer for reversible operations such as encryption. That flexibility can be useful in an existing Python data stack; the right choice depends on the workflow you need to maintain. This post is not "Presidio bad". It is about a specific mismatch: TypeScript and Node.js product teams who need redaction on the LLM request path, which is a different job than batch anonymization. ## Where is the friction for Node and TypeScript teams? - **Language boundary.** Presidio’s Python packages can be exposed as a service for Node callers. Account for authentication, transport security and availability on that extra data path. - **Restoration design.** Presidio has reversible operators; it is incorrect to say it cannot restore data. Compare how you will retain the operation metadata, keys or mappings, and how returned model text is resolved. - **Deployment footprint.** Measure the packages and NLP model you actually enable. Presidio’s [installation choices](https://microsoft.github.io/presidio/installation/) and Sether’s optional NER path make a single headline size misleading. - **Streaming integration.** Verify framing, bounded buffering and reply restoration for your own transport. Avoid assuming a whole-text anonymizer can simply be called once per chunk. The [chunk-boundary discussion](/blog/streaming-llm-redaction-chunk-boundary-bug) shows the failure mode. None of these are design flaws. They are the natural shape of a tool built for data pipelines being pressed into a latency-sensitive request path in a different language. ## How do the alternatives compare? | Decision | Presidio | Broader guardrail suite | Sether | DIY | | --- | --- | --- | --- | --- | | Integration shape | Python packages or service | Depends on selected suite and scanners | In-process TypeScript or Python SDK | Your application code | | Detection | Configurable NLP engines and recognizers | Evaluate each chosen scanner | Patterns and labels; optional NER add-on | Whatever you implement and maintain | | Reversibility | Reversible operators including decrypt | Check scanner state and restoration API | Token vault and known-map restore; decoy alias engine | You design state and resolution | | Streaming | Test adapter and buffering design | Test selected scanner/transport | Bounded stream helpers; documented limits still apply | You implement boundaries and failure paths | | Evaluation | Use your labeled fixtures | Use your labeled fixtures | Use your labeled fixtures | Use your labeled fixtures | | SDK licensing | MIT | Review the selected project’s license | MIT; official application tiers are separate | Your responsibility | The comparison should start with a small labeled corpus of your own inputs: valid and invalid structured values, prose names, your users’ languages, repeated terms and harmless lookalikes. Count both missed sensitive values and unwanted replacements. Checksums, a language model and an attractive demo are all insufficient substitutes for that test. The [patterns versus NER guide](/blog/regex-vs-ner-pii-detection) explains the coverage questions. ## Which one should you pick for your scenario? | Your situation | What to evaluate first | Why | | --- | --- | --- | | Existing Python data workflow | Presidio’s configured recognizers and operators | Reuse a deployment model your team can operate | | Free-prose names across languages | NER models against a representative labeled corpus | Model coverage and errors matter more than a brand name | | Several guardrail requirements | A suite including the required checks | Compare the enabled checks and their failure behavior | | TypeScript LLM request path | Sether’s in-process integration | Redact/vault/restore without a required Python sidecar | | Python request path | Sether’s Python SDK and your adapter | Shared design, with version-specific APIs to verify | | A narrow custom format | A custom detector plus regression fixtures | Keep the scope explicit and avoid silent expansion | For a useful trial, run one fictional support case through each candidate: an email that should be replaced, an order number that should remain, an unlabelled name, and a reply that echoes the replacement. Add an expired mapping and a split stream. Compare the failed cases as carefully as the successful ones before making a migration decision. ## What does a Presidio-to-Sether migration map to? A migration is an integration change, not merely a rename. Inventory the entities, recognizers, anonymization operations, retention and restoration behavior your current flow depends on. Then map only the capabilities the destination actually provides; validate the changed output before switching traffic. Sether emits a fresh typed token per match and retains token-to-original entries in a vault. Presidio’s reversible operators use their own operation and key handling. Both need state and lifecycle decisions. For Sether’s default `MemoryVault`, the capacity is 10,000 entries with a one-hour TTL; shared storage is an interface you implement and secure, not a managed service included with the SDK. ```ts import { Sether, redactSync, restoreSync } from '@raeven-co/sether'; const sether = new Sether(); // Presidio analyzer + anonymizer, in one sync call: const safe = redactSync(userMessage, { detectors: sether.detectors, vault: sether.vault }); // The part Presidio does not do: restore in the reply const reply = await callOpenAI(safe); const restored = restoreSync(reply, { vault: sether.vault }); ``` For streaming, Node streams pipe through `sether.redact()` and `sether.restore()`, and there are SSE helpers plus wrappers (`wrapOpenAI`, `wrapAnthropic`, `wrapFetch`) if you would rather not touch the call sites at all. The full setup is in [Redact PII before OpenAI in TypeScript](/blog/redact-pii-before-openai-typescript). The trade-off is deployment and integration shape. An in-process SDK can remove a service hop; a configured NLP pipeline can provide detection capabilities your application needs. Neither fact proves a comparative latency, memory or accuracy result. Measure the actual versions, detectors, models and workload. ## Why does "lightweight" matter on the request path? A redaction layer on the request path runs on every single LLM call your product makes. Its latency is added to every response, its memory is resident in every instance, and its dependency tree is attack surface on your most sensitive data flow. A sidecar adds a network hop that itself carries the raw PII, which means TLS, auth, and availability questions for one more internal service. An in-process library with one runtime dependency keeps the data in the process that already had it. Supply-chain weight matters here too, which is why we documented [getting Sether to zero open audit alerts](/blog/npm-supply-chain-zero-alerts-socket). The broader picture, including where gateways and browser extensions fit alongside libraries, is in the [complete guide to PII redaction for LLM apps](/blog/pii-redaction-for-llm-apps-complete-guide). ## Frequently asked questions ### Is there a native Node.js port of Presidio? The documented Presidio analyzer/anonymizer packages are Python. A Node application can integrate through a service boundary; assess the deployment and sensitive-data transport involved. See the [Presidio installation documentation](https://microsoft.github.io/presidio/installation/). ### Is Sether as accurate as Presidio? There is no comparative benchmark in this article establishing that. Test both on labeled examples from your own use case, including missed values and unwanted replacements. Model choice, enabled recognizers, languages and input structure all matter. ### Can I use Sether from Python, or is it TypeScript only? Yes. `pip install sether` provides the same general redact/vault/restore design with Python-specific APIs and integrations. The [Python 0.4.0 release](/blog/sether-python-0-4-0-json-safe-restore) explains JSON-aware restore and upgrade limits; check the [Python docs](/docs/python) for integration details. ### What does Sether cost compared to Presidio? The Presidio and Sether SDKs are MIT-licensed. The full Sether TypeScript and Python libraries, including decoys and restore, have no Sether usage charge. Official Shield and editor Free/Pro tiers are separate from using the library. Operational cost depends on your deployment, models and maintenance. --- Source: https://sether.ai/blog/reversible-pii-redaction-redact-restore # Reversible PII redaction: how redact-then-restore works > Reversible PII redaction replaces matched values with typed tokens before a provider request and keeps the token-to-original mapping in a vault you operate. When a reply contains recognised tokens, restoration puts those originals back. Unmatched text still passes through, and successful restoration depends on the matching vault remaining available. Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - One-way masks are useful when originals are not needed again. Reversible tokens suit workflows where a reply or final document needs those values. - Typed tokens preserve field hints, not a promise of unchanged model behavior. Keep redacted history and test whether the model preserves the replacements. - The default vault is in-process; a shared store is possible. Your storage configuration determines who can access mappings and how long they remain. - Redact and restore need access to the same retained mapping. One instance is the simplest setup; separate instances can share a vault. - If a destination has no justified need to recover originals, avoid keeping a restoration path merely because it is available. ## Why does one-way masking break chat apps? Classic redaction is one-way: find the email, replace it with `[REDACTED]`, done. That is correct for logs and documents, where nobody needs the value back. A placeholder such as `[REDACTED]` removes detail, which is often the right goal. But a draft addressed to a customer may need an unambiguous reference to the value that your app will insert later. Typed, distinct tokens let the model refer to those fields without requiring the original strings. Think of a coat check: the ticket refers to something held elsewhere. The analogy has a limit. A story about a distinctive coat may reveal its owner even without the ticket mapping. Likewise, tokenizing an email does not anonymize the rest of a customer’s message. ## What makes a good redaction token? Sether tokens look like ``: a type prefix plus a unique identifier. Both parts do work. - **The type prefix** tells the model what kind of value sits there. It can still say "I will send the invoice to " and reason about it as an email address. - **The identifier** is not an encoding of the original string. Resolving it uses the vault entry. Protect both the vault and the surrounding content; context can still reveal identity. - **A fresh token for every match** means repeated appearances of sarah@acme.com can produce different tokens. Each retained entry can restore to the same address. The model cannot infer equality from those token strings alone, although the surrounding text may connect them. ## Where do the real values live? In the vault: a key-value store mapping tokens to original values. Sether ships `MemoryVault`, an in-process LRU that holds 10,000 entries with a 1-hour TTL. The vault interface is pluggable. Different processes can redact and restore through a compatible mapping store you implement, provided authorized entries remain available. The TypeScript `Vault` lookup contract is synchronous: an ordinary asynchronous Redis client is not a drop-in implementation, because restore cannot await per-token lookups. Design the storage boundary explicitly, including isolation, availability and retention. ## Why must both directions share the mapping? The default instance owns a `MemoryVault`. Redaction writes a mapping there and restoration reads it back. Reusing that instance is the simplest way to keep both directions connected. A second instance with its own default vault has no access to the first instance’s entries. By default, unresolved tokens stay in the output; strict restore can reject them in supported releases. A second instance configured with the same vault is different: it can resolve mappings that are still available and authorized for that conversation. ## How does this survive a multi-turn conversation? The redacted history carries the conversation. Turn one redacts sarah@acme.com to a token. If you keep the redacted messages as the history you resend, then on turn three, when the user says "actually send it to that same address", the history still holds that exact token, so the model refers back to it and the reply restores to the real address. If you re-redact the plain history on every turn instead, each pass mints new tokens, so one address can appear as several different tokens in the same prompt. Replies still restore correctly, but the model has less to go on. Keeping redacted history avoids repeatedly sending originals from earlier turns, but new messages and unmatched context still need review. Set the vault lifetime for your actual session behavior. The one-hour default is a configuration choice, not a guarantee that a delayed reply or a resumed conversation will still have its mapping. ## What are the security properties? - **Matched originals are withheld from that edited request.** Unmatched values, attachments and identifying context are outside that statement. - **A token is a reference, not anonymization proof.** Its random identifier does not encode the original, but context or other data may identify the person. - **The vault is sensitive application data.** Restrict access, isolate tenants or conversations, and avoid copying mappings into ordinary logs. - **Retention must cover the intended round trip.** Expiry and eviction limit stored mappings but can also prevent restoration; test both outcomes. To be precise about the claim: this is a technical measure that helps with data minimisation. It shrinks what leaves your boundary; it does not remove the need for processor agreements or a lawful basis, which we walk through in [the GDPR post](/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai). | Failure to rehearse | Expected handling | | --- | --- | | A mapping expired or was evicted | Leave unresolved or raise in strict mode; do not fabricate a replacement. | | The model invented or altered a token | Reject or review it before a tool action. | | A valid token appears in the wrong argument | Apply authorization and field validation; a known token is not permission. | | The reply is JSON | Use JSON-aware restore or restore parsed string values, then validate the schema. | Restoration is part of the full MIT SDK and remains a Free essential in the official tools. Optional Pro workflows do not sell your originals back to you: tool restoration still depends on having the matching session mapping. [Pricing](/pricing) explains the separate tool pilot. ## When is reversibility the wrong choice? Reversibility exists to serve a live user who needs the real value back in the reply. If no user is waiting, keeping a path back to the data is a liability, not a feature. | Destination | Redaction mode | Why | | --- | --- | --- | | LLM prompt in a chat app | Reversible | A user is waiting for a reply that must read naturally | | Application logs | One-way | Nobody should ever recover PII from a log line | | Analytics and metrics pipelines | One-way | Aggregates need counts, not identities | | Training or fine-tuning data | One-way | A restore path into a dataset defeats the purpose of redacting it | | Data shared with third parties | One-way | You cannot control their handling of a reversible token stream | Choose retention by destination and purpose. A log scrubber may never need a reverse mapping; a live customer reply may. Do not keep originals in a vault by default if the workflow has no reason to recover them. ## The round trip in code In TypeScript, redact and restore are stream transforms on one instance. The full tutorial, including the drop-in OpenAI wrapper and SSE streaming, is [here](/blog/redact-pii-before-openai-typescript). ```ts import { Sether } from '@raeven-co/sether'; import { Readable } from 'node:stream'; const sether = new Sether(); // holds the vault // Outgoing: values become tokens const safeForLLM = Readable.from([userMessage]).pipe(sether.redact()); // Incoming: tokens become values again, same instance const safeForUser = llmResponse.pipe(sether.restore()); ``` In Python, the same design with sync calls (async and streaming also exist; the [Python walkthrough](/blog/redact-pii-llm-prompts-python) covers them). ```python from sether import Sether sether = Sether() # holds the vault safe = sether.redact_sync(user_message) # values -> tokens reply = call_llm(safe) restored = sether.restore_sync(reply) # tokens -> values ``` One subtlety worth knowing before you ship: restoring a streamed reply means tokens can split across chunks, and a naive restore misses them. Sether holds back a small window to stay boundary-safe; the details are in [the chunk-boundary bug post](/blog/streaming-llm-redaction-chunk-boundary-bug). ## Frequently asked questions ### Is a redaction token the same as encryption? No. A token identifies a vault entry rather than encrypting the original string. Protect the vault as sensitive data. Tokenization does not prevent someone inferring identity from the surrounding message or other available information. ### What happens if a vault entry expires before the reply arrives? It cannot be restored from that missing entry. Default restore leaves the token unresolved; supported strict modes raise an error instead. Decide whether to show a placeholder, ask the user to retry or stop the operation. Never guess an original value. See the [JSON-safe restore release](/blog/sether-0-8-0-json-safe-restore). ### Can the LLM provider reverse the tokens? The random identifier does not encode the original, and Sether does not send the vault to the model provider. That does not make the whole message anonymous: unmatched text and other context may still identify someone. ### Should I use reversible redaction for my logs too? Use reversibility only where a justified workflow needs originals back. For logs, analytics or shared datasets, assess whether retaining any reverse mapping is necessary and restrict both the content and its retention accordingly. --- Source: https://sether.ai/blog/stop-pasting-sensitive-data-into-chatgpt # How to stop pasting sensitive data into ChatGPT (without banning it) > Reduce accidental sharing by reviewing a draft before you send it. A browser-side guard can flag supported personal details and replace matched values in compatible editable fields. Sether Shield offers local text processing, Free essentials and optional Pro workflows in licensed releases. It edits page text; it is not a network filter or a guarantee that every sensitive detail is caught. Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - People paste support tickets, customer lists, and config files with live keys into AI chats because it works. That incentive does not go away. - A ban alone can leave users without an approved workflow. Pair policy with practical review tools and clear limits instead of assuming a browser guard covers every channel. - Shield is a seatbelt: review supported matches and the edited draft before sending. It does not make a website unable to read its own input field. - Shield processes prompts locally. Free essentials include scanning, masks, explicit redaction and known-mapping restore; new licensed releases add optional Pro decoys and custom controls. Licensing contacts Sether with account metadata, not prompt content. Current store builds may retain legacy free features. - It protects humans at the chat box, not your API traffic. Your product's LLM calls need a library-side layer too. ## What do people actually paste into ChatGPT? A support ticket can contain a customer name and email. A customer list can include identifiers in ordinary columns. A copied configuration can contain a live key. The task may need the question or error message while needing very little of that identifying detail. Nobody does this maliciously. They do it because it works: the model writes the reply, finds the bug, summarizes the list. The sensitive data was just along for the ride, and once it is sent, you cannot unsend it. This is a different problem from your product's own API calls to OpenAI or Anthropic. Those run through code you control and are covered in our [complete guide to PII redaction for LLM apps](/blog/pii-redaction-for-llm-apps-complete-guide). This post is about the humans and their browsers. ## Why do blanket bans fail? A restriction is easier to follow when people have an approved way to do the task. Without one, someone may switch to an unmanaged account or device. That is a risk to plan for, not a claim that every ban or every team behaves the same way. Give users a clear alternative: which tasks are allowed, which details to remove, how to review replacements and when not to send a request at all. A browser control supports that workflow; it does not replace organization policy or device controls. The seatbelt analogy is useful if its limits stay visible. The guard helps at the moment a person reviews a draft. It does not turn every AI use into an approved use, and it cannot protect channels it does not inspect. ## What does browser-side guarding look like? The idea is simple. Something watches the prompt input box on the AI chat sites, the same way Grammarly watches a text field. When it sees an email, a phone number, a card number, or an API key in the draft, it tells you before you hit send. In the compatible licensed pilot, the default Mask action edits matched values to masks. Optional Pro decoys use realistic stand-ins; an explicit Redact selection or watchlist redact action can use placeholders such as `[redacted-1]`. Review the result before sending. The website may already read text in its own page, and detection can miss a value. This is the human-side complement to API-side redaction, not a replacement for it. Your app's server-side calls need a library in the request path, in [TypeScript](/blog/redact-pii-before-openai-typescript) or [Python](/blog/redact-pii-llm-prompts-python). The extension covers the copy-paste habits no server-side code can see. ## How does Sether Shield work? Sether Shield is a Chrome extension (Manifest V3) with protected-site input controls for compatible editors, including chatgpt.com, chat.openai.com, claude.ai and gemini.google.com. It complements the library integration by helping people review drafts in their browser. - **Input watcher, not a proxy.** Shield watches compatible editable fields on protected origins. It edits page text rather than filtering the actual outgoing network request. Review the result before sending. - **Floating shield with a live count.** As you type or paste, the shield shows how many pieces of PII are currently in the draft. - **Choose the handling action.** Mask is the default in the compatible licensed pilot. Optional Pro decoys insert realistic stand-ins; explicit selection/watchlist redact actions can produce `[redacted-N]` placeholders. Review the edited text rather than assuming one action or format across every release. - **Undo and restore.** Originals and replacements are mapped in page memory and Chrome memory-only session storage, so a mapping can survive a refresh and is cleared when the browser closes. Settings, watchlists and limited replacement history are stored locally across restarts. Known-mapping restore remains Free after trial or paid access expires. - **Real detectors, not toy regex.** It runs the same detector packs as the open-source `@raeven-co/sether/browser` build: validated email, phone, card with Luhn, IBAN with mod-97, the secrets pack for API keys and tokens, plus conversational detection for phrases like "my name is..." and "I live at...". - **Local text processing.** Shield’s detection and replacement do not upload prompt text, replies, watchlists or vault mappings for analysis. It uses storage, context-menu and page-access permissions. Free essentials need no Pro account; explicit trial or license activation in compatible releases exchanges account/license metadata with Sether, separate from prompt processing. The detector libraries remain fully MIT and free at [github.com/raeven-co/sether](https://github.com/raeven-co/sether). [Shield source](https://github.com/raeven-co/sether-shield) is available for inspection. Free essentials remain available without a Pro account; new official releases introduce optional Pro. See [pricing](/pricing) for compatible-build and pilot availability before starting a trial. | Workflow in compatible licensed releases | Tier and boundary | | --- | --- | | Scan and mask, or explicitly redact a selection | Free essentials; review matched fields and the selected action before send. | | Restore recognised replacements | Free, including after Pro expires, while the session mapping remains available. | | Use realistic decoys or custom watchlists | Optional Pro in Shield and the official editor. | | Add custom regex rules | Optional Pro in Shield; do not assume editor regex support. | The tool pilot offers seven days of Pro only after an explicit in-app opt-in; installation alone does not start the clock, and no card or automatic charge is involved. The pilot paid periods are $5 for one calendar month or $48 for one calendar year, manual renewal, for one person across both tools. Check [pricing](/pricing) for compatible builds and availability; older store builds may retain legacy Free features. Try a fictional draft first: use an email, a phone and one company term you want to keep private. Inspect the matches, choose the replacement style, read the result and rehearse undo. To restore a reply, the session mapping must still exist. Showing originals on the page exposes them to that page’s scripts; copying a restored reply puts them on the system clipboard instead. ## What can a browser extension not do? Being honest about limits matters. Sether Shield watches compatible input fields and supported rendered replies. It is not a network proxy and does not filter actual outgoing HTTP traffic. - It does not cover your product's API calls to LLM providers. That needs a server-side layer: see the [TypeScript walkthrough](/blog/redact-pii-before-openai-typescript) or the [Python one](/blog/redact-pii-llm-prompts-python). - Its prompt controls activate on origins in the protected-site allowlist. You can add other origins, but a custom editor or changed site interface may be incompatible. - It cannot catch what detectors do not detect. Validated patterns and conversational cues cover a lot, but a name dropped mid-sentence with no cue can slip through. It is a seatbelt, not a force field. - It is a Chrome extension today. Prompt controls do not inspect file uploads, attachments, images or your application’s server-side API traffic. ## How do you roll this out to a team? | Step | What to do | Why it works | | --- | --- | --- | | 1. Frame it as a seatbelt | Announce it as protection for employees, not surveillance of them | Explain local text processing and license metadata accurately; do not present it as a team reporting or monitoring service. | | 2. Start with volunteers | Ask the support and engineering teams to try it for two weeks | Collect compatibility and usability feedback before assuming it works with every editor. | | 3. Show the count, not the shame | Share aggregate "the shield flagged N items this month" stories, self-reported | A self-reported example is useful feedback, not a verified organization-wide security metric. | | 4. Pair it with a clear policy | One page: what is fine to paste, what is not, and what the shield does | People follow rules they understand and can actually comply with | | 5. Cover the API side too | Add server-side redaction to your product's LLM calls | The extension covers humans; your code paths need their own layer | The goal is not zero AI use. The goal is that the fast path and the safe path are the same path, so nobody has a reason to route around you. ## Frequently asked questions ### Does Sether Shield send my prompts anywhere for analysis? Detection and replacement run locally; prompt text, replies, mappings and watchlist contents are not uploaded for analysis. Compatible licensed releases make account/license metadata requests when you explicitly activate a trial or license. Free essentials require no Pro account. Protected-site and editor compatibility still matter. ### Will scrubbing break the AI's answer? It can change the result when a literal value matters or a replacement is misplaced. Review the edited draft and use undo or known-map restore deliberately. Sending originals again is your choice, not a requirement to make the product function. ### Can this replace a DLP product or server-side redaction? No. It covers one specific, high-frequency gap: humans pasting into AI chat sites. Your product's own API calls to LLM providers need a library or gateway in the request path, and enterprise DLP covers channels an extension never sees. ### What happens to the original values after a scrub? Mappings are held in page memory and Chrome memory-only session storage. They can survive a refresh and are cleared when the browser closes. Saved settings, watchlists and limited replacement history persist locally until removed. Restore needs an available mapping and a supported interface; it remains Free after Pro expiry. Showing restored values in a reply inserts originals into the website DOM, where site scripts may read them. Copying the restored reply uses the system clipboard instead. --- Source: https://sether.ai/blog/streaming-llm-redaction-chunk-boundary-bug # The chunk-boundary bug: why streaming PII redaction is hard > Scanning streaming chunks independently can miss a value split across them: "sarah@ac" followed by "me.com" is the same email after joining. A hold-back buffer gives detectors surrounding text to work with. Sether uses a configurable distance and bounded buffering safeguards; supported match lengths, long inputs and framing still need explicit tests. Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - A per-chunk regex can miss a value that only becomes recognisable after adjacent chunks are joined. - A realistic test deliberately splits inside values, rather than relying on whichever boundaries a live demo happens to produce. - Property-based partition tests exercise many split positions and retain failing fixtures; compare replaced spans and restoration rather than fresh random identifiers. - Sether uses safeDistanceBytes (default 256) plus bounded buffering safeguards. A bounded implementation needs limits and tests for unusually long or custom matches. - Test each enabled detector, actual transport framing, end-of-stream behavior and oversized inputs before relying on a streaming integration. ## How can you reproduce the chunk-boundary bug? The simplest reproduction needs no live model. Start with text containing an email, split the address into two chunks, and compare a per-chunk detector with one that preserves context. The example isolates a transport-boundary failure from model behavior. A naive streaming implementation takes each chunk, runs detectors on it, replaces matches and emits the result. It can pass ordinary happy-path fixtures whenever each sensitive value happens to arrive whole. Deliberately splitting the value exposes what those fixtures did not exercise. Property-based partition tests can explore this systematically: generate many partitions of the same PII-bearing text and compare behavior. With random token identifiers, compare equivalent replaced spans and restoration rather than literal equality between fresh UUIDs. In the email example, neither isolated half is a complete address. Joining the emitted chunks still reconstructs the original, so the user-visible result leaks the value even though each individual detector call returned no match. ## Why does per-chunk redaction fail? Here is the naive version, simplified. It looks reasonable until an input deliberately splits in the middle of a value: ```ts // Looks right. Is not. async function* redactStream(chunks: AsyncIterable) { for await (const chunk of chunks) { // Run detectors on this chunk in isolation yield redactText(chunk); // regex + validation on chunk only } } ``` The problem is that the stream does not respect your detectors. Chunk boundaries land wherever the network and the provider's tokenizer put them. So "sarah@acme.com" can arrive as "sarah@ac" followed by "me.com". Run an email regex on "sarah@ac" and it does not match, there is no dot-something after the domain start. Run it on "me.com" and it does not match either, there is no @ before it. Two innocent halves, one leaked email. | Chunk 1 | Chunk 2 | Naive per-chunk result | Correct result | | --- | --- | --- | --- | | sarah@ac | me.com | sarah@acme.com leaks | | | call +234 80 | 31 234 5678 | phone number leaks | | | 4111 1111 11 | 11 1111 | card number leaks | | | sk-proj-Ab | Cd3fGh... | API key leaks | | This is a design failure a per-chunk scanner can share, rather than a problem unique to one regex. Unless the implementation preserves context between chunks, the detector never gets a chance to inspect the joined value. ## Why can a demo miss it? A short demo may deliver a whole email in one chunk. That is a useful happy-path check, but it exercises one partition of the text. The boundary case needs a deliberate fixture. Chunk boundaries depend on token lengths, network buffering, and provider behavior, none of which you control. A redactor can run cleanly for a long time and then split a value at exactly the wrong byte. It is not flaky in a way you can see; it is correct-looking with a hole in it. A live demonstration answers “did this request work?” A partition test asks a different question: “what happens when the same input arrives in smaller pieces?” Both are useful, but the second is what exposes the missing context. ## How does the hold-back window fix it? The fix is to preserve context between chunks. In the current Node redactor, the option named `safeDistanceBytes` defaults to 256, but the implementation uses JavaScript string lengths and slices: UTF-16 code units, not a literal UTF-8 byte bound. Additional rules retain long non-whitespace runs, with a maximum-buffer guard of `max(safeDistanceBytes * 4, 8192)`. Values beyond that guard can retain residual risk; test encoding and input limits as well as the nominal setting. So when "sarah@ac" arrives, the tail of it stays in the window instead of going out the door. When "me.com" arrives, the detector sees the joined text, matches the full email, and emits ``. The reader downstream never knows the chunks were reassembled. Buffering everything is an option when the application can wait for the full text and afford its memory cost. A bounded buffer preserves incremental output, but its safety depends on detector behavior and input limits. Test the longest values you permit, including custom secrets, rather than assuming every built-in or future detector fits inside the configured hold-back distance. ```ts import { Sether } from '@raeven-co/sether'; const sether = new Sether(); // Node streams: redact on the way out, restore on the way back prompt.pipe(sether.redact()); // bounded context; safeDistanceBytes defaults to 256 reply.pipe(sether.restore()); // restore recognised tokens with the matching vault // SSE responses have their own helpers that respect event framing: // createSSERedactStream / createSSERestoreStream ``` ## How does a property-based test lock it down? The test that caught the bug is now the test that guards the fix. The idea fits in a few lines of pseudocode: ```ts // Property: redaction output must not depend on chunking. // Token ids are random in production, so the test seeds them. property('chunking-invariant', (text: PiiBearingText, seed: number) => { const expected = redactSync(text, { detectors, vault, uuid: counter() }); // whole text at once const chunks = randomPartition(text, seed); // split anywhere, // including mid-email, mid-card, mid-key const actual = collect(streamRedact(chunks, { uuid: counter() })); assert.equal(actual, expected); }); ``` Generate texts with known matches, partition them at varied positions, and compare detected/replaced spans with the whole-text path. When token identifiers are random, normalize identifiers or inject deterministic ones in a test fixture; also check that restoration with each run’s own vault recovers the input. Literal UUID equality is not the property being promised. That last part matters. When the invariant fails, you do not get "something is wrong somewhere", you get "splitting at byte 14 inside this email leaks it". The bug that would have been invisible in production is a one-line reproduction in CI. Add two layers of fixtures: raw text chunks and the HTTP/SSE envelopes that carry them. A text transform can pass its tests while an adapter drops a final fragment, joins events incorrectly or restores a nested JSON document as plain text. For a tool call, validate the assembled arguments and authorization separately from restoring its values. ## How do you test your own implementation? If you have a streaming redactor in production, home-grown or vendored, here is the five-minute check I would run first, and the fuller list after it. 1. **The five-minute check.** Feed "my email is sarah@ac" then "me.com" as two chunks. Inspect the joined redacted output for the original address. 2. **Split every enabled detector type.** Include phone numbers, cards, API keys and long values, not only email. 3. **Vary byte boundaries and Unicode.** Exercise multibyte characters, one-byte chunks, line endings and stream completion with a possible match still held. 4. **Compare behavior, not fresh UUID strings.** Normalize generated token IDs and verify restoration with the corresponding vault. Keep each failing partition as a regression fixture. 5. **Exercise limits and failure paths.** Include long unbroken values beyond the buffer guard, custom detector widths, cancellation and upstream errors. Decide whether your application should buffer fully, reject oversized input or accept documented residual risk. The repository’s [historical migration notes](https://github.com/raeven-co/sether/blob/main/CHANGELOG.md) document a chunk-boundary defect and a property-tested correction. They do not establish that every implementation or detector is safe for every partition. Keep the technical lesson reproducible: retain failing fixtures, exercise current limits and validate the adapter you actually deploy. For the wider context on where streaming redaction fits in an LLM data-protection setup, start with the [complete guide to PII redaction for LLM apps](/blog/pii-redaction-for-llm-apps-complete-guide). And if you are wiring this into a TypeScript service today, the [step-by-step OpenAI walkthrough](/blog/redact-pii-before-openai-typescript) covers the full redact-then-restore round trip. ## Frequently asked questions ### What does the default safeDistanceBytes setting of 256 mean? 256 is the default safeDistanceBytes setting. Despite the name, the current Node implementation indexes JavaScript strings in UTF-16 code units rather than imposing a literal UTF-8 byte bound. Long-run buffering also has a maximum guard. Test Unicode, long values, custom detectors and the actual decoder/transport path. ### Does the hold-back window add noticeable latency? It delays some emission and can affect time to first output. The effect depends on chunk sizes, input shape, detector work and configured distance. Measure end-to-end latency on your transport; there is no fixed “one small chunk” or user-perception guarantee. ### Does this apply to SSE streams too? Yes, and SSE adds framing to the problem. Test the framing your application emits and the helper you use. Restoring a JSON document assembled inside streamed delta.content fragments requires collecting that document and using JSON-aware restore; see the [0.8.0 limits](/blog/sether-0-8-0-json-safe-restore). ### Could I just buffer the whole response and redact once? You can scan a fully buffered response once when its size and latency are acceptable. That removes this particular chunk-context issue, but it does not make detection perfect. Bounded streaming trades memory and delay against documented match and buffer limits. --- Source: https://sether.ai/blog/regex-vs-ner-pii-detection # Regex vs NER for PII detection: when patterns aren't enough > Use validated patterns for structured candidates such as card numbers and IBANs, labels for fields with explicit cues, and evaluate NER for entities in free prose. Known-term lists and custom detectors can help with specific vocabularies. Measure missed and unwanted matches on representative data; local inference can avoid sending text to a separate detection service. Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - Checksums and format validation reject some invalid candidates; they do not guarantee near-zero false positives or complete coverage. - Label anchoring uses explicit field cues. Coverage depends on the labels, scripts and formats you enable. - NER can help with prose names, while known-term lists and custom detectors handle specific vocabularies. No single method catches every case. - Local inference can keep prompt text out of a separate detection API. Model download, cache and deployment settings still need review. - Start with the required data classes, build labeled fixtures and add techniques when those fixtures demonstrate a gap. ## Where do validated patterns help? Some PII announces itself by shape. An email address has a grammar. A credit card number is a digit sequence that must pass a Luhn checksum. An IBAN carries a mod-97 check built into the standard. A phone number parses against real country metadata. Patterns identify candidates; validation rejects some malformed ones. Passing a checksum does not establish that digits are a real customer’s card, and failing one does not mean the surrounding text is harmless. Measure both false positives and missed values for the formats your application accepts. Sether's basic pack works this way: email, phone (via libphonenumber-js, the one runtime dependency), card with Luhn, SSN, IPv4/IPv6, IBAN with mod-97. The secrets pack does the same for API keys: AWS, OpenAI, Anthropic, GitHub, Slack, Stripe, JWTs, plus a Shannon-entropy catch-all for high-entropy strings. Patterns avoid model inference and can be a practical first layer for structured values. They still have CPU and memory costs, format limits and adversarial-input concerns. Use bounded patterns, test them and measure the actual request path before making latency claims. ## What does label anchoring add? A name has no checksum. “Sarah Chen”, “New York” and “Prime Minister” can have similar capitalization. A broad capitalization rule can mistake ordinary phrases for people; an evaluation set needs those harmless lookalikes. But a lot of real data labels itself. Forms, tickets, CRM exports, and copied records read like "name: Sarah Chen", "DOB: 12/04/1991", "passport: A1234567". The label is the anchor: match the label, capture the value that follows. Sether’s identity pack uses multilingual labels across Latin, CJK, Cyrillic and Arabic scripts. An explicit cue helps distinguish a candidate value from ordinary words, but labels do not guarantee accuracy. Unlabelled text and unsupported value formats remain gaps to test. ## When does free-prose entity recognition help? A message such as "tell Chidi I’ll send the money on Friday" lacks an explicit name field. General shape and label detectors may not identify the person. NER is one option; a known-term list can also match Chidi when your application already has the relevant vocabulary. A named-entity model predicts spans and classes from context. It can identify names, organisations and locations, but it can also misclassify ordinary words or miss an unfamiliar name. Evaluate its predictions in the languages and situations your users actually produce. This is what [@raeven-co/sether-ner](https://github.com/raeven-co/sether) exists for. It detects names, organisations, and locations in running prose, and emits the same token format as the core library, so a `` token restores through the core Sether vault with no extra wiring. ```ts import { Sether } from '@raeven-co/sether'; import { createNerRedactor } from '@raeven-co/sether-ner'; const sether = new Sether(); const ner = createNerRedactor(); // default model: Xenova/bert-base-NER // Batched pass over the outbound prompt, locally, before it leaves const safe = await ner.redact("tell Chidi I'll send the money on Friday"); // -> "tell I'll send the money on Friday" // The model reply restores through the core Sether vault, // same as pattern-based tokens, no extra wiring: modelReply.pipe(sether.restore()); ``` ## What do the trade-offs actually look like? | Technique | Useful signal | Failure to test | Operational cost | | --- | --- | --- | --- | | Validated patterns | Shape, checksums and format rules | Valid-looking order IDs; unusual but accepted formats | Pattern work on every input; no inference model | | Label anchoring | A cue such as name: or DOB: | Missing or ambiguous labels; unsupported scripts | Rules and value validators | | Local NER | Contextual entity predictions in prose | Names outside training coverage; ambiguous words | Model artifacts, startup, inference and cache | | Known-term/custom detector | Application-specific names or identifiers | Overlapping terms; ordinary-word collisions | Maintained vocabulary/rules and regression tests | | Cloud detection service | The selected service’s classifiers | Coverage, access, retention and errors | Network request, service dependency and billing | A separate detection API adds a recipient for the text it classifies. That may be acceptable under an assessed design, but it is a different privacy boundary from local detection. Do not describe a cloud-assisted pipeline as entirely local. ## Why should NER run locally? If your requirement is to avoid a separate text-analysis recipient, local inference is a useful design choice. It shifts model execution and operations to your environment; it does not remove evaluation, access-control or model-supply responsibilities. The add-on runs configured inference through transformers.js, with `Xenova/bert-base-NER` as its default model. The inference path processes text in your environment, but initial model loading can fetch artifacts. Review caching, offline deployment and the chosen inference function. The [GDPR discussion](/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai) explains why the full data flow matters. ## How do you keep the core small if NER is heavy? Keep optional model dependencies separate from applications that do not need them. Sether’s TypeScript core has one declared runtime dependency; `@raeven-co/sether-ner` uses an optional `@huggingface/transformers` peer and loads its model when needed. Actual package and model sizes depend on the version and artifacts selected. The NER add-on uses the core token/vault restoration model. Wire both paths to the same retained vault and test the entity output. Keeping model dependencies optional lets applications that do not need NER avoid those artifacts and startup costs. ## What are the honest limits of NER here? - **Plan model loading.** Test a cold start as well as a cached one. Preload approved artifacts if the production environment cannot download them. - **Benchmark your runtime.** This article does not establish a universal ONNX-versus-PyTorch speed ratio. Hardware, model, quantization and workload change the result. - **Keep the integration scope clear.** The add-on is a batched outbound-text pass, not transformer inference on every streaming chunk. See the [chunk-boundary discussion](/blog/streaming-llm-redaction-chunk-boundary-bug) for the separate stream problem. - **Evaluate coverage.** Include local names, multiple scripts and ambiguous words. A default model and confidence threshold are starting configurations, not proof of fairness or accuracy. - **Test custom inference too.** The `infer`, `threshold` and `labels` options allow customization. A replacement model needs its own compatibility and quality checks. | Fixture | What it tells you | | --- | --- | | A checksum-valid order ID that is not a card | Whether structured validation over-replaces harmless values. | | An unlabelled name in a locally relevant language | Whether the configured NER or known-term path covers your users. | | Name: followed by a placeholder or ordinary word | Whether labels and deny-lists behave as intended. | | The same sample under a cold/offline start | Whether model loading matches your deployment promise. | Keep the test corpus separate from public demos. Use fictional or appropriately controlled examples, record the expected spans and review disagreements. The result you want is an understood error profile for the task, not a “perfect detector” claim. ## So which should you use? It is a ladder, not a choice. Climb only as far as your data requires. 1. **Start with the data classes.** Inventory structured identifiers, labels, prose entities and business terms the task can do without. 2. **Enable and evaluate patterns and labels.** Add both positive cases and harmless lookalikes before selecting thresholds or rules. 3. **Add NER or known-term detection where fixtures reveal a gap.** Use a representative corpus rather than a single impressive example. 4. **Review the execution boundary.** Know whether text, model artifacts or account metadata leave your environment, and describe each accurately. For where detection fits in the full redact-then-restore pipeline, the [complete guide to PII redaction for LLM apps](/blog/pii-redaction-for-llm-apps-complete-guide) covers the whole picture. If you are comparing this approach to Presidio's analyzer stack, see [lightweight Presidio alternatives for TypeScript](/blog/lightweight-presidio-alternatives-typescript). ## Frequently asked questions ### Can I skip patterns and just use NER for everything? You can evaluate that approach, but NER is not automatically better at every structured format and costs inference work. Compare it with validated patterns on the actual data classes; a hybrid can cover different gaps without treating either technique as infallible. ### How big is the NER model download? It depends on the selected model artifacts, format and quantization. Measure the actual download, cached footprint and cold-start behavior of your deployment. Avoid treating one historical artifact size as a package guarantee. ### Can I use a different model than bert-base-NER? Yes. `createNerRedactor` accepts custom inference through `infer`, plus `threshold` and `labels`. Evaluate the replacement model’s outputs, runtime needs and licensing on your own corpus before relying on it. ### Does NER run on streaming responses? No. It runs as a batched pass on the outbound prompt before it leaves. The per-chunk streaming path stays pattern-based, because transformer inference inside a hold-back window would add latency streaming exists to avoid. --- Source: https://sether.ai/blog/npm-supply-chain-zero-alerts-socket # How I took my npm package's supply-chain score from 78 to zero alerts > This is a June 2026 hardening account, not a current security certificate. Sether’s reported scan showed seven development-tool alerts and a score of 78; the work reduced dependencies and cleared the alerts checked in that pass. Evaluate the exact package version and advisories you install today, including the build pipeline as well as runtime dependencies. Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo. ## TL;DR - A supply-chain scanner is a useful input to evaluation, not a substitute for inspecting source, artifacts and the integration. - The reported seven alerts in this historical pass were development-tool dependencies; that still matters to maintainer machines and release pipelines. - The post records an audit result at the time. A clean result does not prove the absence of vulnerabilities or predict future advisories. - The hardening work removed SDK peer dependencies and recorded ASCII bundle checks and a scan of the 161 regex patterns in that version. Counts and scan results are version-specific. - Trace each alert, check affected paths and versions, preserve the result with the release, and repeat the review when dependencies change. ## Why does a supply-chain score matter for a security library? Put yourself in the shoes of an engineer evaluating Sether. I am asking them to pipe their most sensitive traffic, the prompts containing their customers' data, through my code. The first sane thing to do is check whether my package is itself a risk. Supply-chain tools can surface advisories and suspicious package behavior. Their output helps prioritize review, but a score is neither SOC 2 nor an independent security certification. It should lead to inspecting the package, version, dependency path and actual exposure. This article’s June 2026 account reported a score of 78 during the 0.5.2–0.5.4 hardening period. The [0.5.4 changelog](https://github.com/raeven-co/sether/blob/main/CHANGELOG.md) independently records seven development-tool alerts, the dependency upgrades and that release’s test and regex-scan counts. A historical score is not a current package assessment. ## What actually caused the score of 78? Start by tracing each alert to its dependency path, affected versions and execution context. A score alone cannot show whether the issue affects a runtime package, development server or release job. The historical pass attributed seven alerts to development tooling. One was [CVE-2026-47429 / GHSA-5xrq-8626-4rwp](https://github.com/advisories/GHSA-5xrq-8626-4rwp), rated Critical in GitHub’s advisory. Its documented path involves the Vitest UI server on Windows, especially when exposed to the network. The affected-version ranges matter: “upgrade to 4.x” alone is not a sufficient remediation instruction. Those alerts were attributed to the development dependency graph rather than the published runtime dependency graph. Ordinary registry consumers do not install a dependency package’s development tools, while contributors and release jobs can run them. Review both contexts instead of assuming a devDependency has no possible effect. | Alert source | Where it lives | Runtime dependency? | Fix | | --- | --- | --- | --- | | Vitest advisory CVE-2026-47429 | Development test server | Not a declared runtime dependency | Check the advisory’s fixed ranges: 4.1.0 or 3.2.6 and later on the corresponding line | | esbuild advisories (transitive) | devDependencies (build tooling) | No | Resolved by the tooling upgrade | | vite advisories (transitive) | devDependencies (build tooling) | No | Resolved by the tooling upgrade | | brace-expansion advisory (transitive) | devDependencies | No | Resolved by the tooling upgrade | Check a repository-specific alert view where available. An organization-wide dashboard can include findings from other projects; keep the package version and dependency path beside each finding so the scope stays reviewable. ## So why not just say "it's only devDeps"? Because nobody evaluating a security library owes me that level of reading. The score says 78. Explaining that the alerts are "only" in my test runner is technically correct and rhetorically dead. If my pitch is "trust me with your PII", my answer to a supply-chain flag cannot start with "well, actually". There is also a real, if smaller, risk story. devDependencies run on maintainer machines and in CI, where releases are built and signed. A compromised dev toolchain is a classic path to a poisoned release. "It can't reach users" assumes the build pipeline stays honest. The reported hardening pass aimed to resolve the identified alerts and describe their scope. “Zero open alerts” records that check at that time; it does not mean zero unknown vulnerabilities. ## What did the fix involve? The report recorded a tooling upgrade and zero vulnerabilities in the audit run used for that release, with 134 tests passing then. For a new installation, check the actual lockfile and current advisory ranges rather than copying a historical major-version instruction. Then I used the momentum to fix things no scanner had flagged yet, starting with peer dependencies. Sether ships `wrapOpenAI` and `wrapAnthropic` middlewares, and the lazy way to type those is to depend on the `openai` and `@anthropic-ai/sdk` packages. That put two heavyweight SDKs in my dependency story for what is, structurally, a function that wraps another function. Structural typing lets a wrapper describe the part of a client it uses without importing the provider SDK. That reduces package coupling. It does not guarantee compatibility with future methods or response shapes: keep contract fixtures for the SDK versions and modes your integration supports. ```ts // Before: peerDependencies: { "openai": "^..." } just for a type. // After: describe only the shape we touch. No import, no dependency. interface ChatCompletionsLike { chat: { completions: { create: (body: unknown, options?: unknown) => Promise; }; }; } export function wrapOpenAI(client: T): T { // intercept create(), redact the messages, restore the reply // ... return client; } ``` That left exactly one declared runtime dependency: libphonenumber-js, which does real work (phone parsing against country metadata) that I should not hand-roll. One dependency is not an accident of the codebase, it is now a design constraint. ## What did going further look like? - **ASCII bundle checks.** Legitimate multilingual labels were escaped in the reported bundles. That addresses a specific scanner signal; ASCII-only output does not prove the absence of malicious logic or every Unicode-related issue. - **Regex review.** The reported scan checked 161 patterns in that version with none flagged unsafe. Static scanner results are evidence with limits; also test adversarial inputs, runtime behavior and later pattern changes. - **Byte-identical verification.** Where a release contained no runtime change, I verified the published bundles were byte-identical to the previous version. If only devDependencies moved, users should be able to confirm the artifact did not. The inspectable [source on GitHub](https://github.com/raeven-co/sether), MIT licensing and deliberately small runtime dependency surface make review easier. They do not remove the need to review the release you deploy, its artifacts and how your application uses them. For your next release, save the package version, dependency lockfile, audit command and result together. Then check the published artifact, not only the working tree. Re-run integration fixtures after a tooling upgrade; clearing an advisory while breaking a wrapper is not a useful trade. No scanner result in this article is a promise about the package version a reader installs later. ## What should other maintainers take from this? 1. **Trace before drawing a conclusion.** In this pass the seven alerts were development tooling. Your package’s alert mix may be different. 2. **Check the repo-specific alerts view.** Org-wide dashboards blur provenance across repositories. The per-repo view tells you what is actually attributed to the package people install. 3. **Separate provenance from exposure.** Runtime and development dependencies have different execution contexts. Read the affected path and platform before deciding severity for your deployment. 4. **Structural typing kills peer dependencies.** If you depend on an SDK only for its types, describe the shape you touch instead. Your users lose an entire dependency subtree. 5. **A small dependency surface is a feature.** "One runtime dependency" fits in a README line and an evaluator's head. It is also simply less code you have to vouch for. 6. **Hold your own tool to its own standard.** If your library exists because you do not trust data paths, your package should be the easiest data path in the stack to audit. This post is part of a series on building a redaction layer you can actually trust. The big picture is in the [complete guide to PII redaction for LLM apps](/blog/pii-redaction-for-llm-apps-complete-guide), the comparison with heavier tooling is in [lightweight Presidio alternatives for TypeScript](/blog/lightweight-presidio-alternatives-typescript), and the hardest bug the test suite caught is in [the chunk-boundary story](/blog/streaming-llm-redaction-chunk-boundary-bug). ## Frequently asked questions ### Do devDependency CVEs affect people who install my package? Ordinary registry installs do not install a dependency package’s devDependencies, but contributors and CI can run them. A vulnerable development server or compromised build tool can matter to release integrity. Read each advisory’s affected paths, versions and exposure conditions. ### How does dropping a peer dependency help users? It can remove an installation and version-resolution burden when a wrapper only needs a small client shape. It does not make SDK behavior changes irrelevant; compatibility and response-shape tests are still necessary. ### What is a Trojan-Source flag? Trojan Source concerns deceptive Unicode rendering of source, including directional controls. An ASCII-only bundle can avoid a particular Unicode signal but cannot certify source integrity. Inspect the flagged content and use the scanner as one review input. ### How do I check my own package? Run npm audit locally, then look your package up on socket.dev and open the repo-specific alerts view. Trace every alert to runtime versus dev provenance before deciding anything. Then look at your own dependency tree the way a stranger would: every entry is something you are asking users to trust.