{"slug": "show-hn-prompt-scrub-local-first-pii-redaction-for-llm-prompts-and-responses", "title": "Show HN: Prompt-scrub – local-first PII redaction for LLM prompts and responses", "summary": "Nano Collective released prompt-scrub, a local-first Node.js utility that redacts personally identifiable information from LLM prompts and responses before they leave the machine, replacing findings with stable placeholders and allowing local rehydration. The tool, available as an npm package and CLI, includes eight detectors (phone, postal address, path, secret, URL, and opt-in name and code-tell) and supports deterministic scrubbing with session-based mapping stored locally.", "body_md": "Built by the [Nano Collective](https://nanocollective.org), a community collective building AI tooling not for profit, but for the community.\n\nThis is the first public release of `prompt-scrub`\n\n, a small Node.js utility that runs entirely on your machine. It detects identifying content inside a prompt (emails, paths, secrets, phone numbers, URLs, postal addresses, and a couple of opt-in categories), replaces each finding with a stable placeholder like `Email_1`\n\nor `Path_2`\n\n, and lets you rehydrate the model's response back to the original values locally after it comes back.\n\nThe motivation is simple: most accidental identifier leakage to a cloud LLM lives in the text of the prompt and the text of its response. Stripping it there, deterministically, before the prompt leaves your machine is a useful layer in a privacy posture, and one that does not need a network round-trip, a new account, or a hosted service to work.\n\n## What it actually does\n\nThe package exposes two functions and a CLI.\n\n`scrub()`\n\ntakes either a plain string or an array of `{ role, content }`\n\nmessages, runs the configured detectors over the text, replaces each finding with a category-namespaced placeholder, and returns the scrubbed content plus a session id:\n\n``` js\nimport { scrub, rehydrate } from '@nanocollective/prompt-scrub';\n\nconst prompt = \"My key is sk-12345 and my email is [email protected]\";\nconst { scrubbedContent, sessionId } = scrub({ content: prompt });\n// scrubbedContent: \"My key is Secret_1 and my email is Email_1\"\n```\n\nYou send `scrubbedContent`\n\nto whatever LLM provider you already use. When the response comes back, `rehydrate()`\n\nwalks the text, looks up each placeholder in the session map, and swaps them back. Unknown placeholders (placeholder text the model hallucinated, or placeholders from a previous session) are passed through unchanged and surfaced as warnings so you can decide whether to trust them:\n\n``` js\nconst response = \"Your email Email_1 looks correct and your key Secret_1 is fine.\";\nconst { content, warnings } = rehydrate({ content: response, sessionId });\n// content: \"Your email [email protected] looks correct and your key sk-12345 is fine.\"\n```\n\nThe session id is the link between the two steps. It points at a small JSON file under your OS config directory that holds the placeholder-to-original mapping. The file is written atomically, with restrictive permissions, and includes a corrupt-file quarantine path so a half-written map does not silently disable rehydration. The location is overridable via the `PROMPT_SCRUB_CONFIG_DIR`\n\nenvironment variable.\n\n## Detectors\n\nEight detectors ship in the box.\n\nOn by default:\n\n- phone\n- postal address\n- path (Unix-style and Windows-style)\n- secret (tuned for common API key and credential shapes, since missing a credential is worse than missing a name)\n- url\n\nOff by default, opt-in:\n\n- name (proper-noun detector, with a stricter allowlist mode)\n- code-tell (user-enumerated private identifiers, for things like internal project codenames)\n\nURL detection also accepts a trusted-host allowlist with subdomain matching, so internal services you control can pass through without a placeholder if that is what you want. Overlapping detector findings (an email that looks like a URL fragment, say) resolve by a documented priority order, with longer span winning on ties. Determinism is deliberate: the same input and session always produce the same scrubbed output, which keeps provider prompt-cache prefixes byte-stable.\n\n## The CLI\n\nA small command-line wrapper around the same logic. The recommended workflow is `inspect`\n\nfirst, then `scrub`\n\n:\n\n```\n# See exactly what would change without writing anything\necho \"My email is [email protected]\" | prompt-scrub inspect\n\n# Scrub stdin (or a file), prints the session id to stderr\necho \"My email is [email protected]\" | prompt-scrub scrub\n\n# Rehydrate a response using --session-id\necho \"Contact Email_1 for details.\" | prompt-scrub rehydrate --session-id <id>\n\n# Inspect a session map\nprompt-scrub sessions list\nprompt-scrub sessions show <id>\n\n# See the active detector set, including any rule-pack additions\nprompt-scrub rules list\n```\n\n`inspect`\n\nis the part we want people to actually use. It does not write a session file and it prints a SHA-256 hash of the scrubbed output so you can verify byte-stable cache prefixes across runs:\n\n```\nDetected entities:\n  [Email]    [email protected]                → Email_1    (chars 12-29)\n\nNo session written.\nHash: 41beda4af0b83488fdf6eea9347775450a1c7c887a6ef377212340f36c445132\n```\n\n## Extensibility\n\nTwo extension points:\n\n- Custom detectors can be passed in via the library API (\n`customDetectors`\n\nin`ScrubOptions`\n\n). Each one returns matches in the same shape the built-ins do, so they slot into the same priority / span logic. - Rule packs are separate npm packages that contribute additional detectors. They are declared in your config or in\n`package.json`\n\n, merged into the active set, and visible in`rules list`\n\n. This is the path for sharing detectors across projects without forking the package.\n\n## What this is, and what it is not\n\n`prompt-scrub`\n\nreduces identity leakage at the content layer. It is partial defence, not anonymity, and the README and the [Threat Model](https://github.com/Nano-Collective/prompt-scrubber/blob/main/docs/features/threat-model.md) are explicit about the distinction because the distinction matters.\n\nIt does defend against:\n\n- Accidental secret leakage in prompts. The secret detector is tuned for high precision on the common API key, token, and credential shapes.\n- Accidental identifier leakage in one-off prompts. Emails, phone numbers, postal addresses, paths, and URLs are caught with conservative defaults, and the original values stay on disk under the session map.\n\nIt partially mitigates:\n\n- Cloud LLM providers reading identifying content. After scrubbing, the prompt contains\n`Email_1`\n\ninstead of your address. That is materially less identifying, but not zero, and a determined provider can still correlate across a session if it has access to its own logs. - Long-term profile building from prompt content. Stable session mappings prevent identifier-level correlation within a single session. They do not address stylistic fingerprinting, the way you phrase things goes out unchanged.\n- Tool call results in agentic settings. The scrubber runs on every message regardless of origin, so\n`ls`\n\n,`git log`\n\n,`cat`\n\n, and`grep`\n\noutputs are scrubbed before the next LLM turn. Coverage is limited to the configured detectors.\n\nIt does not defend against:\n\n- A compromised local machine. The scrubber runs locally. If your environment is compromised, your prompt is too. The session maps on disk are plaintext JSON in v1, and an attacker with your account can read them. Encryption at rest is a v1.1 follow-up.\n- Semantic leakage. A question that is inherently identifying (your private codebase, a niche bug only you have, a number only your accountant knows) cannot be made anonymous by stripping identifiers.\n- Stylistic fingerprinting. The v1 brute-force approach does not rewrite style.\n- The network or key layer. Your IP address, request timing, and headers are outside the scrubber's scope. Pair it with a network tool of your own choosing if you need that.\n\nA user who believes this tool makes them anonymous is worse off than one who never used it, because they stop reading their prompts and trust the defaults. Always use `inspect`\n\nfirst. The Threat Model document spells out the full picture in one place.\n\n## Install\n\n```\n# As a global CLI\nnpm install -g @nanocollective/prompt-scrub\n\n# As a Node.js dependency\nnpm install @nanocollective/prompt-scrub\n```\n\nThe package is open source under the project's licence. Source, full docs, and the Threat Model live at the repo:\n\n[https://github.com/Nano-Collective/prompt-scrubber](https://github.com/Nano-Collective/prompt-scrubber)\n\n## Feedback wanted\n\nThis is the first public release. The things we would most like to hear about:\n\n- Detector coverage. Which common shapes do you paste in that we are missing? Which opt-in category (name, code-tell) should be on by default for your workflow?\n- Rule pack packaging. The extension shape is in, but we have not yet seen real-world rule packs, and the format is open to change while the package is at 0.x.\n- The Threat Model. We chose partial-defence framing deliberately. If the documentation is unclear about what is and is not defended, we want to know before we ship more code on top of it.\n- The inspect UX. The hash output and the dry-run diff are intended to be the thing people actually use. If they are awkward in practice, that is a fast fix.\n\nIssues and PRs are welcome at the repo. There is also a [Nano Collective Discord](https://discord.gg/ktPDV6rekE) for the wider conversation about what the collective is building and why.", "url": "https://wpnews.pro/news/show-hn-prompt-scrub-local-first-pii-redaction-for-llm-prompts-and-responses", "canonical_source": "https://nanocollective.org/blog/prompt-scrub-v100-a-local-first-scrubber-for-prompts-and-their-responses-76", "published_at": "2026-07-31 15:32:39+00:00", "updated_at": "2026-07-31 15:52:37.890821+00:00", "lang": "en", "topics": ["ai-tools", "ai-safety", "ai-ethics"], "entities": ["Nano Collective", "prompt-scrub"], "alternates": {"html": "https://wpnews.pro/news/show-hn-prompt-scrub-local-first-pii-redaction-for-llm-prompts-and-responses", "markdown": "https://wpnews.pro/news/show-hn-prompt-scrub-local-first-pii-redaction-for-llm-prompts-and-responses.md", "text": "https://wpnews.pro/news/show-hn-prompt-scrub-local-first-pii-redaction-for-llm-prompts-and-responses.txt", "jsonld": "https://wpnews.pro/news/show-hn-prompt-scrub-local-first-pii-redaction-for-llm-prompts-and-responses.jsonld"}}