prompt-sanitizer — Ruby

Bidirectional PII sanitizer for LLM pipelines. Zero cloud calls. GDPR & HIPAA ready.

Strips PII from prompts before they reach any model API, then optionally restores original values in the response — all in-process, with no third-party telemetry.


Table of contents


Installation

# Gemfile
gem "prompt-sanitizer"
bundle install

Quick start

require "prompt_sanitizer"

sanitizer = PromptSanitizer::Sanitizer.new   # FAST mode — zero dependencies

result = sanitizer.sanitize("Hi, I'm John Doe. Reach me at [email protected] or 555-867-5309")
puts result.text
# => "Hi, I'm [PERSON_1]. Reach me at [EMAIL_1] or [PHONE_1]"

puts result.entities.map { |e| [e.type, e.original] }.inspect
# => [[:person, "John Doe"], [:email, "[email protected]"], [:phone, "555-867-5309"]]

Modes

Mode Engines Latency Catches
:fast (default) Regex + Secrets < 1 ms Email, phone, SSN, CC, IBAN, IP, MAC, URL, ZIP, dates, crypto, bank, passport, DL, API keys, JWTs, AWS keys, DB strings
:smart Fast + NER ~25–50 ms + Names, organisations, locations, miscellaneous entities
:full Smart + Synthetic + Audit ~25–50 ms + Realistic fake replacements, compliance audit trail
# SMART mode — requires `gem "informers"` (or `gem "mitie"`)
sanitizer = PromptSanitizer::Sanitizer.new(mode: :smart)

# FULL mode — also requires `gem "faker"`
sanitizer = PromptSanitizer::Sanitizer.new(mode: :full)

on_detect callbacks

# :redact (default) — replace PII with tokens
sanitizer = PromptSanitizer::Sanitizer.new(on_detect: :redact)

# :warn — replace AND call a warning handler
sanitizer = PromptSanitizer::Sanitizer.new(
  on_detect: :warn,
  on_detect_callback: ->(entities) { Rails.logger.warn "PII: #{entities.map(&:type)}" }
)

# :block — raise PIIDetectedError immediately
sanitizer = PromptSanitizer::Sanitizer.new(on_detect: :block)
begin
  sanitizer.sanitize("SSN: 123-45-6789")
rescue PromptSanitizer::PIIDetectedError => e
  puts e.entities.first.type   # => :ssn
end

Multi-turn sessions

Sessions share a vault across conversation turns so the original values can be restored from the model's reply:

sanitizer = PromptSanitizer::Sanitizer.new
session   = sanitizer.session

# Turn 1
clean_prompt = session.anonymize("Book a flight for Alice Chen, [email protected]")
# => "Book a flight for [PERSON_1], [EMAIL_1]"

llm_reply = YourLLMClient.chat(clean_prompt)
# => "Sure! I've booked a flight for [PERSON_1] ([EMAIL_1])."

final_reply = session.deanonymize(llm_reply)
# => "Sure! I've booked a flight for Alice Chen ([email protected])."

# Block form — vault is cleared automatically on exit
sanitizer.session do |s|
  clean = s.anonymize(user_prompt)
  s.deanonymize(llm_client.chat(clean))
end

Rails integration

Install generator

rails generate prompt_sanitizer:install

This creates config/initializers/prompt_sanitizer.rb with all options commented.

Initializer

# config/initializers/prompt_sanitizer.rb
PromptSanitizer.configure do |config|
  config.mode        = :smart           # :fast | :smart | :full
  config.ner_backend = :informers       # :informers | :mitie
  config.on_detect   = :redact          # :redact | :warn | :block
  config.audit_log   = :memory          # :memory (more backends coming)
end

Rack middleware

Automatically sanitizes JSON request bodies before they hit your controllers. Supports prompt, messages[].content (OpenAI format), input, text, query.

# config/initializers/prompt_sanitizer.rb
PromptSanitizer.configure do |config|
  config.use_middleware     = true
  config.middleware_routes  = ["/api/"]       # only sanitize these path prefixes
  config.restore_response   = false           # set true to deanonymize JSON responses
end

Alternatively, insert manually:

# config/application.rb
config.middleware.use PromptSanitizer::Integrations::SanitizerMiddleware,
  routes: ["/api/v1/chat"],
  restore_response: false

ActionController concern

Fine-grained control inside individual actions:

class ChatController < ApplicationController
  include PromptSanitizer::Integrations::ActionControllerConcern

  def create
    # Sanitize specific params in-place
    sanitize_params!(:prompt, :message)

    # Or use a multi-turn session scoped to this request
    with_pii_session do |session|
      clean   = session.anonymize(params[:prompt])
      raw     = LLMClient.chat(clean)
      @reply  = session.deanonymize(raw)
    end
  end
end

ActiveJob concern

Scrubs PII from job arguments before the job performs:

class LLMJob < ApplicationJob
  include PromptSanitizer::Integrations::ActiveJobConcern

  sanitize_argument :prompt   # sanitized in-place before perform

  def perform(prompt:, user_id:)
    LLMClient.chat(prompt)    # prompt is already clean
  end
end

Audit log

The audit log records every sanitization event — entity type, confidence, session ID, and a SHA-256 hash of the original value. Raw PII is never stored.

PromptSanitizer.configure do |c|
  c.audit_log = :memory
end

sanitizer = PromptSanitizer::Sanitizer.new(mode: :full)
sanitizer.sanitize("Call Jane at 555-123-4567")

log = PromptSanitizer.audit_log
puts log.count                          # => 1
puts log.export(format: :json)          # JSON array of events
puts log.export(since: "1h")            # events in the last hour

Custom patterns

sanitizer = PromptSanitizer::Sanitizer.new
sanitizer.add_pattern(/EMP-\d{6}/, :custom)   # employee IDs
sanitizer.sanitize("Assigned to EMP-004821")
# => "Assigned to [CUSTOM_1]"

Entity types detected

EMAIL · PHONE · SSN · CREDIT_CARD · IBAN · IP_ADDRESS · MAC_ADDRESS · URL · ZIP_CODE · DATE_OF_BIRTH · DATE · CRYPTO_ADDRESS · BANK_ACCOUNT · PASSPORT · DRIVING_LICENSE · API_KEY · JWT · BEARER_TOKEN · AWS_ACCESS_KEY · AWS_SECRET_KEY · PRIVATE_KEY · DB_CONNECTION · PERSON · ORGANIZATION · LOCATION · AGE · CUSTOM


Optional dependencies

Gem Version Required for
informers >= 1.3 SMART / FULL mode NER (recommended)
mitie >= 0.4 SMART / FULL mode NER (alternative)
faker >= 2.0 FULL mode synthetic replacements

Note: informers and mitie require Ruby ≥ 3.3.


License

MIT