DEV Community

Cover image for Sensitive data out of the prompt: a TypeScript library written in 39 minutes from its specification
Ramón Chancay 👨🏻‍💻
Ramón Chancay 👨🏻‍💻

Posted on Originally published at ramonchancay.me

Sensitive data out of the prompt: a TypeScript library written in 39 minutes from its specification

Every time a backend concatenates a user message into a prompt, writes a request body to a log or forwards a payload to a helpdesk, it is moving personal data out of the process and into a system that was never designed to hold it. sanitype is a TypeScript library that puts one call between that payload and its destination: it takes the object and returns a copy with the same shape and the sensitive fields redacted, masked, hashed or dropped, plus a report of everything it touched. It runs entirely in-process, with no network calls and no runtime dependencies. It was also built in an unusual way: the idea was dictated by voice to an agent, the agent wrote 563 lines of specification, and Claude Code implemented version 0.1.0 against them in a single goal, 39 minutes later.

TL;DR

  • It combines two strategies: field-path rules for what you already know is sensitive, and detectors over free text for the email someone pasted into a notes field.
  • By default an object goes in and an object with the same structure comes out. Every call returns a report of what was touched, where and by which detector, and that report never contains the original value.
  • The repository was written the other way round: first SPEC, ARCHITECTURE, ROADMAP and COMPARISON, then the code. 39 minutes passed between the specification commit and the implementation commit.

sanitype in twenty seconds:

your backend
    │
    ▼
sanitize(payload)
    ├─ field rules   what you already know is sensitive
    ├─ detectors     what shows up inside free text
    └─ action        redact · mask · hash · drop · tokenize
    │
    ▼
LLM · logs · analytics · third-party APIs
    +  report: what was touched, where and by which detector
Enter fullscreen mode Exit fullscreen mode

What sensitive data leaks out of a backend

The problem is not that somebody wants to leak data. It is that there are four common exits through which a payload leaves the process whole, and none of the four feels like a decision about privacy when you write it:

user payload
  ├──► LLM prompt        OpenAI, Anthropic, a local model
  ├──► logs and tracing  Sentry, Datadog, the request logger
  ├──► analytics         Segment, PostHog, internal events
  └──► third-party APIs  helpdesk, webhooks, partner integrations
Enter fullscreen mode Exit fullscreen mode

The first one is the newest and the quietest. A support ticket carrying the customer's phone number and national ID gets concatenated into the prompt as it is, and the prompt travels to an external vendor that may retain it. The second is the oldest: somebody added logger.info({ body: req.body }) to debug one case, and that log has been collecting emails and card numbers for two years. The third and fourth are variants of the same carelessness: the whole object goes out because separating the fields the destination actually needs takes work.

What these four have in common is that they happen at the outbound edge of the process. That is where something has to sit, and that something has to be cheap to call, because if it costs a network round trip nobody is going to put it in the logger.

Why nothing that already exists fit

Before writing a line I went through the landscape, and I wrote it down in a COMPARISON.md inside the repository, because "why not just use X" is a question that shows up in the first issue.

Option What it does well Why it did not fit
DLP as a service (Google Cloud DLP, Purview, Macie) Broad entity coverage, model-based detection, compliance tooling around it A network call per scan: latency, cost per request and one more vendor dependency
Self-hosted Microsoft Presidio Mature, strong entity recognition, no per-call cost It is a Python project: from a Node backend it means deploying and monitoring another service
Regex npm packages Lightweight, dependency-free, easy to read They work on loose text: they do not know that user.ssn in your type is a national ID, and they return no report
Browser extensions They catch what a user pastes into a UI They see nothing of server-to-server traffic, which is exactly the case here

The gap is specific: a native TypeScript library that runs in-process, understands the shape of your objects and not just the text, and returns an auditable record of what it did. That is the position sanitype occupies, and the price it pays for it is in the comparison: there is no entity-recognition model behind it, and free-text detection is bounded by the patterns it ships.


Keep reading

Illustration of sanitype: a payload crosses a boundary and comes out with its sensitive fields masked, plus a report of what was touched

That is the first half. The full walkthrough — with the rest of the implementation, the trade-offs and the things that only show up in production — is on my blog:

Read the full post on ramonchancay.me →

Originally published at www.ramonchancay.me/blog/scrubbing-pii-before-the-llm-sanitype.

Top comments (0)