npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@permanentlymobile/pm-privacy-guard

v1.0.0

Published

PM Privacy Guard -- AI message privacy routing, classification, and sanitization. Self-hosted. Zero-data-retention provider routing. One-time ownership.

Downloads

26

Readme

PM Privacy Guard

Protect sensitive data before it reaches AI providers.

ci

PM Privacy Guard classifies, sanitizes, and routes messages based on sensitivity. PII, financial data, medical records, crypto wallets, legal content -- all detected and handled automatically before anything leaves your application.

What It Does

  • Sensitivity Classification -- Scores every message on a 0-1 scale across 5 built-in categories (PII, financial, medical, legal, crypto) plus a personal-names category, with an optional customPatterns slot per category
  • Automatic Sanitization -- Redacts, masks, or tokenizes sensitive content; tokenize mode returns a reversible token map
  • Privacy Routing -- Returns a recommendedProvider of privacy, standard, or block based on score thresholds. Your application maps privacy to whichever zero-retention provider you have configured (e.g. Venice, Ollama)
  • Provider Freedom -- Sanitization happens before the prompt leaves your machine, so the downstream model becomes a cost-and-quality decision rather than a data-exposure one. Capable low-cost and free models -- DeepSeek and other open-weight options that now rival GPT and Claude on many tasks, including providers hosted in other jurisdictions -- become usable for sensitive work, because the personal data was already stripped upstream
  • Intent Detection -- Classifies each message as agent (needs tool execution), privacy (privacy keywords or sensitive content), or chat (safe for standard providers)
  • Audit Logging -- Append-only SQLite audit trail with CSV export, encrypted at rest with SQLCipher when a passphrase is configured
  • Context & Prompt Sanitization -- Strips sensitive content from memory context and system prompts before they reach external providers
  • Per-Chat Privacy Levels -- Override routing behavior per conversation (off, keywords, content, full)

Scope and Limitations

PM Privacy Guard defends against accidental disclosure -- personal or sensitive data that would otherwise reach an external LLM without you intending it. "Accidental" is broad on purpose. It includes data you never typed by hand: a pasted spreadsheet or document, content copied out of a PDF, OCR output, text from another application, or digits entered through a non-Latin keyboard. Privacy Guard classifies the entire outbound prompt, not only your last keystroke, so PII arriving in a format you never chose is exactly what it is built to catch.

It is built for individuals and small teams who want a practical privacy layer without enterprise overhead.

What it is not. No content classifier is a 100% guarantee, and Privacy Guard does not claim to be one. In normal use it catches the large majority of accidental disclosures, and it is most effective combined with good habits -- review what you paste, and prefer your privacy-routed provider for anything sensitive.

The deliberate-evasion caveat. Privacy Guard does not defend against a user intentionally obfuscating their own data to slip it past the classifier -- for example, splicing stray letters through their own Social Security number. Bypassing your own privacy tool is a choice, not an accident, and it is outside what this product is designed to stop. Privacy Guard protects you from mistakes, not from yourself on purpose.

Quick Start

PM Privacy Guard ships as plain JavaScript. No build step, no bytecode loader, no native compilation outside better-sqlite3-multiple-ciphers (used only when audit logging is enabled).

# 1. Install dependencies (Node >= 20 required).
#    Use `npm ci` for a deterministic install from the committed lockfile;
#    plain `npm install` will work but may pull in newer transitive versions.
#    If you're on Node < 20, upgrade first -- better-sqlite3-multiple-ciphers
#    will fail to build on older Node releases.
npm ci

# 2. Create your config (interactive wizard or copy the example).
#    If you skip this step and run pm-privacy-guard.mjs without a
#    config.yaml, the engine prints a one-line hint telling you to
#    run the wizard, then falls back to safe defaults with an
#    unencrypted audit log.
node setup-wizard.mjs
# OR: cp config.example.yaml config.yaml && edit

The wizard now asks two new questions in addition to the existing sensitivity / provider setup:

  1. Which AI harness will you use Privacy Guard with? claude-code | openai-shape | anthropic-direct | custom
  2. Privacy backend: local Ollama (free, slower) or Venice (paid, faster)? Default: local.

Both answers are persisted as harness and privacy_backend in config.yaml and honored at runtime.

# 3. Test with demo mode
node pm-privacy-guard.mjs --demo

# 4. Start with your config
node pm-privacy-guard.mjs

Integration

import { PrivacyShield } from './lib/privacy/shield.js';

const shield = await new PrivacyShield({
  personalNames: ['John', 'Jane'],
  defaultMode: 'redact',
  sensitivityThreshold: 0.4,
  blockThreshold: 0.9,
  auditEnabled: true,
  dbPath: './store/pm-privacy-guard.db',
  providers: [
    { name: 'venice', type: 'privacy' },
    { name: 'openai', type: 'standard' },
  ],
}).init();

// Classify a message
const result = shield.classify("My SSN is 123-45-6789");
// { score: 0.6, categories: ['pii'], matches: [...] }

// Route a message (classify + sanitize + decide provider)
const decision = shield.route("Send payment to 0xABC...");
// { intent: 'privacy', sensitivity: {...}, recommendedProvider: 'privacy', sanitized: {...} }

// Sanitize content (default mode is "redact")
const clean = shield.sanitize("Card number 4532-1234-5678-9012");
// { sanitized: "Card number [REDACTED-FINANCIAL]", redactions: 1 }

// Tokenize mode also returns a reversible token map
const tokenized = shield.sanitize("My SSN is 123-45-6789", { mode: 'tokenize' });
// { sanitized: "My SSN is [TOKEN_001]", redactions: 1, tokenMap: Map{...} }

// Reverse tokenization (only meaningful for tokenize mode)
const original = shield.detokenize(tokenized.sanitized, tokenized.tokenMap);

// Clean up
shield.close();

Harnesses

PM Privacy Guard is harness-agnostic. The same engine sits in front of whatever AI harness you ship with. A harness adapter does the scrub on the way out and the detokenization on the way back; the call site keeps using whichever SDK it already imports.

| Harness | Targets | Adapter import | |--------------------|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------| | claude-code | Anthropic's Claude Code SDK (@anthropic-ai/claude-code) | import { ClaudeCodeAdapter } from './lib/privacy/harness/claude-code.js'; | | openai-shape | Cursor, Windsurf, Aider, openai-node, Groq, Together, Mistral, any OpenAI-compatible chat.completions | import { OpenAIShapeAdapter } from './lib/privacy/harness/openai-shape.js'; | | anthropic-direct | Raw @anthropic-ai/sdk | import { AnthropicDirectAdapter } from './lib/privacy/harness/anthropic-direct.js'; | | custom | Bring-your-own SDK (subclass BaseHarnessAdapter) | import { BaseHarnessAdapter } from './lib/privacy/harness/base.js'; |

Wiring is the same shape for every adapter. The shield exposes wrap(), unwrap(), and interceptStream() that route through the adapter selected by config.harness -- swap the config value and the runtime path swaps with it. There is no manual selectAdapter step in the integrator's hot path.

shield.wrap() returns { messages, ctx }. The integrator threads the ctx back into shield.unwrap(ctx, response) / shield.interceptStream(ctx, stream) so concurrent requests on a single shared PrivacyShield instance cannot cross-contaminate their token maps. The shield itself holds no per-request state.

import { PrivacyShield } from './lib/privacy/shield.js';

const shield = await new PrivacyShield(config).init();

// Before the SDK call -- routed through the configured harness adapter.
// wrap() returns { messages, ctx }: the ctx carries this request's
// token map and must travel back to unwrap() / interceptStream().
const { messages: wrappedMessages, ctx } = shield.wrap(messages);

// Your usual SDK call (example: openai-node)
const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: wrappedMessages,
});

// Reverse tokenization in the reply -- pass the same ctx back in.
const clean = shield.unwrap(ctx, response);

// Streaming
for await (const chunk of shield.interceptStream(ctx, openaiStream)) {
  process.stdout.write(chunk.choices[0].delta.content ?? '');
}

When config.harness is claude-code, shield.wrap() runs the ClaudeCodeAdapter; flip it to openai-shape and the same call routes through OpenAIShapeAdapter (no other code change needed). The adapter is constructed lazily on the first wrap()/unwrap() and memoized for the lifetime of the shield. The adapter holds no per-request state -- all per-request data lives in the ctx object the integrator threads explicitly -- so a single shared PrivacyShield is safe for any number of overlapping in-flight requests. If you need direct adapter access (e.g. to construct multiple adapters with different modes), selectAdapter is still exported from lib/privacy/harness/index.js.

If you need a harness that's not listed, subclass BaseHarnessAdapter and override _scrubMessage / _readResponseText / _writeResponseText for your SDK's shape -- the scrub logic is inherited and each text block in an array-shape message is scrubbed independently so multi-part messages keep their per-block boundaries.

Privacy backend

Some privacy work needs an LLM (prompt rewrites, tokenization round-trips). The sensitivity classifier itself is regex-only and stays local; only the LLM-driven paths hit a backend. PM Privacy Guard ships with two backend implementations and a runtime selector.

| Backend | Endpoint | Cost | Notes | |----------|-----------------------------------------|-------|-----------------------------------------------------------------------| | local | http://localhost:11434 (Ollama) | Free | No network egress. Slower. Configure model via OLLAMA_MODEL. | | venice | https://api.venice.ai/api/v1 | Paid | Zero-retention. Faster. Needs VENICE_API_KEY. Default model llama-3.3-70b. |

Pick one in config.yaml:

privacy_backend: local   # or 'venice'

Or pick programmatically:

import { selectBackend } from './lib/privacy/backend/index.js';
const backend = selectBackend({ privacy_backend: 'venice' });
const { content } = await backend.chat([
  { role: 'user', content: 'Rewrite this sentence neutrally: ...' },
]);

Inside an initialized PrivacyShield, shield.getBackend() returns the same instance and adapters use it automatically.

The Venice backend is the commercial privacy tier on top of the same engine: every classification, sanitization, and routing decision runs through the identical local pipeline; Venice only carries the LLM round-trip when one is needed. The local Ollama backend exists so the engine has a free, network-egress-free fallback that proves the abstraction. The Venice integration exists so teams that don't want to run their own model can hand the inference half to a zero-retention commercial provider without giving up source-readable controls.

Sensitivity Categories

| Category | Detects | |----------|---------| | PII | SSNs, addresses, phone numbers, names, emails, dates of birth | | Financial | Credit cards, bank accounts, routing numbers, balances, transactions | | Medical | Diagnoses, medications, conditions, medical record numbers | | Legal | Case numbers, attorney-client privilege markers, court dockets | | Crypto | Wallet addresses (ETH, BTC, SOL), private keys, seed phrases |

Sanitization Modes

| Mode | Behavior | Example | |------|----------|---------| | redact | Replace with category tag | [REDACTED-PII] | | mask | Partial masking | 123-**-**** | | tokenize | Replace with reversible token | [TOKEN-a7f3b2] (can be reversed with token map) |

Configuration

Copy config.example.yaml to config.yaml and customize:

# Personal names to detect and protect
personal_names:
  - John
  - Jane

# Sanitization mode: redact | mask | tokenize
default_mode: redact

# Sensitivity threshold (0.0 - 1.0)
sensitivity_threshold: 0.4

# Block threshold (messages above this are blocked)
block_threshold: 0.9

# Your AI providers
providers:
  - name: venice
    type: privacy    # zero-data-retention
  - name: ollama
    type: local      # runs locally
  - name: openai
    type: standard   # external, needs protection

# AI harness Privacy Guard sits in front of
# (claude-code | openai-shape | anthropic-direct | custom)
harness: claude-code

# Privacy backend for LLM-driven privacy work (local Ollama or Venice)
privacy_backend: local

# Audit logging
audit:
  enabled: true
  db_path: ./store/pm-privacy-guard.db

# SQLCipher key for the audit DB (written by the setup wizard).
# Omit the entire audit_log block to run the audit DB unencrypted.
audit_log:
  passphrase: "<32-char alphanumeric, generated by setup-wizard.mjs>"

Backups and the passphrase

When the setup wizard's Encrypt the local audit database at rest? (Y/n) prompt is answered Y (the default), it generates a 32-char alphanumeric passphrase and writes it to config.yaml under audit_log.passphrase. The audit-logger module issues PRAGMA key = '...' immediately after opening the database, so the on-disk .db file is SQLCipher-encrypted from byte zero.

A few things to know:

  • The passphrase lives in config.yaml. It is never transmitted, never re-shown after the wizard prints it the one time. If you lose config.yaml, you lose the ability to read the audit log -- there is no recovery path baked into PM Privacy Guard.
  • Back up config.yaml separately from store/pm-privacy-guard.db. Keeping both backups in the same place (same drive, same archive, same cloud bucket) is equivalent to no encryption at rest -- a compromise of that location yields key + ciphertext together. Store the config in a password manager, a sealed envelope, or your team's secret store, distinct from wherever the DB lives.
  • There is no recovery path if both are lost. The audit log is designed to be tamper-evident and append-only; that means no backdoor. If you lose the passphrase, the audit history is gone.
  • Rotating the passphrase (manual, v1). SQLCipher exposes PRAGMA rekey = '...' which re-encrypts every page with a new key. To rotate, open the DB with the current key, issue PRAGMA rekey = '<new key>', then update audit_log.passphrase in config.yaml to match. PM Privacy Guard does not automate this in v1 -- operators handle it as a deliberate one-time event so the new key never accidentally diverges from the config file.
  • Running without encryption. Omit the entire audit_log block from config.yaml and the audit logger opens the file as plain SQLite. The CLI prints a yellow warning at startup (audit-logger running unencrypted because config.yaml is missing or has no audit_log.passphrase) so this can't happen silently.

Stuck migration lockfile

The first time PM Privacy Guard opens an audit DB with a passphrase configured, it migrates any pre-existing plain SQLite file to SQLCipher in place (the original is preserved alongside as <db>.pre-encrypt.bak). The migration is serialized through an exclusive lockfile at <db>.migrate.lock so two starting processes cannot both rewrite the file. In a normal startup the lockfile is created, held for the duration of the migration, and removed automatically when the constructor returns.

In two scenarios startup will refuse rather than reclaim:

  • A live holder's lockfile is older than 5 minutes. The error message names the holder PID and tells you which file to remove.
  • The lockfile carries a different hostname (e.g. another container shares the volume). PID liveness cannot be probed across PID namespaces, so reclaim is unsafe. The error message names the foreign hostname.

To force-unlock manually:

# Confirm no other PM Privacy Guard process is actually mid-migration:
#   - on this host: `ps -p <holderPid>` shows nothing, OR
#   - on another host: ask the other operator before doing anything.
rm <db_path>.migrate.lock

Force-unlocking while another process is genuinely mid-migration can corrupt the plain-to-encrypted rewrite and the .pre-encrypt.bak recovery file. Only remove the lockfile when you are confident no migration is in progress -- either because the named PID is gone or because you have confirmed with the operator of the other host. If you remove the lockfile in error and the migration ends up corrupt, restore from <db>.pre-encrypt.bak (if present) and re-run startup.

License

Privacy Guard ships as one license, all features. License keys are HMAC-signed JWTs that carry the licensee, a provider cap, the feature list, and optional expiry. A valid key advertises every feature this engine exposes:

  • Sensitivity classification (6 categories)
  • Data sanitization (redact / mask / tokenize)
  • Privacy routing
  • Audit logging (SQLite) and CSV export
  • Context sanitization
  • Prompt sanitization
  • Custom detection patterns
  • Multi-tenant routing
  • Up to 25 registered providers

Generate or validate keys with license-tool.mjs. Set PRIVACY_SHIELD_LICENSE_SECRET in the environment to the same secret used at generation time.

CLI Commands

# Run with config
node pm-privacy-guard.mjs

# Demo mode (sample classifications)
node pm-privacy-guard.mjs --demo

# Show current config info
node pm-privacy-guard.mjs --info

# Setup wizard
node setup-wizard.mjs

# License management
node license-tool.mjs generate --tier standard --type perpetual --holder "[email protected]"
node license-tool.mjs validate <key>
node license-tool.mjs info <key>

API Reference

new PrivacyShield(config).init()

Creates and initializes a PM Privacy Guard instance. init() is async and loads optional modules (audit logger).

.classify(message) -> SensitivityResult

Returns { score, categories, matches } for any text input.

.sanitize(message, options?) -> SanitizeResult

Returns { sanitized, redactions } for redact and mask mode. In tokenize mode the result also includes tokenMap: Map<string,string>. Pass { mode: 'mask' } or { mode: 'tokenize' } to override the default mode. There is no categories field on the sanitize result -- categories are returned by .classify().

.route(message, options?) -> RoutingDecision

Full pipeline: classify, sanitize if needed, return routing decision with { intent, sensitivity, recommendedProvider, sanitized, auditId }. intent is one of 'agent' | 'privacy' | 'chat'. recommendedProvider is one of 'privacy' | 'standard' | 'block'.

.sanitizeContext(memoryBlock, provider) -> string

Strip sensitive content from memory context blocks before sending to a specific provider.

.sanitizeMessage(content, provider) -> string

Sanitize message content for a specific provider.

.sanitizePrompt(systemPrompt, provider) -> string

Strip configured sections from system prompts for external providers.

.setPrivacyLevel(chatId, level) / .getPrivacyLevel(chatId)

Per-chat privacy level override. Levels: off, keywords, content (default), full.

.getAuditStats(chatId?, since?) -> AuditStats

Audit statistics. Filter by chat ID and/or timestamp. Returns the empty-stats object when audit logging is disabled or unavailable.

.getAuditEntries(chatId, limit?) -> AuditEntry[]

Raw audit log entries for a specific chat. Returns [] when audit logging is disabled or unavailable.

.exportAuditCSV(since) -> string

Export audit log as CSV from a given timestamp. Returns "" when audit logging is disabled or unavailable.

.close()

Clean shutdown. Closes database connections.

For AI Agents

Plain-text API reference for LLM and AI agent integration. PM Privacy Guard is a JavaScript library (not an HTTP server). Import and call methods directly in your Node.js application.

Library API

Import: import { PrivacyShield } from './lib/privacy/shield.js';

Initialize: const shield = await new PrivacyShield(config).init();

Constructor config object: { personalNames: string[], // names to detect and protect (default: []) defaultMode: string, // "redact" | "mask" | "tokenize" (default: "redact") sensitivityThreshold: number, // 0.0-1.0, route to privacy above this (default: 0.4) blockThreshold: number, // 0.0-1.0, block messages above this (default: 0.9) auditEnabled: boolean, // enable SQLite audit logging (default: true) dbPath: string, // path to audit database (default: "./store/pm-privacy-guard.db") providers: [ // AI providers for routing decisions { name: "venice", type: "privacy" }, { name: "ollama", type: "local" }, { name: "openai", type: "standard" } ], stripSections: string[], // system prompt sections to strip for external providers customPatterns: object // custom detection patterns: { [category]: RegExp[] } }

Methods

shield.classify(message: string) -> SensitivityResult Classify text for sensitive content. Returns: { score: number, // 0.0-1.0 sensitivity score categories: string[], // any of ["pii", "financial", "medical", "legal", "crypto", "personal"] detections: [ // individual matches { category: string, pattern: string, match: string, confidence: number, position: { start: number, end: number } } ], recommendation: string // "standard" | "privacy" | "block" }

shield.sanitize(message: string, options?: object) -> SanitizeResult Sanitize sensitive content. Classifies first, then replaces detected spans. Options: { mode?: "redact" | "mask" | "tokenize", categories?: string[], preserveStructure?: boolean } Returns (redact, mask): { sanitized: string, // cleaned text redactions: number // count of replacements made } Returns (tokenize): { sanitized: string, redactions: number, tokenMap: Map<string,string> // reversible token map; pass back to detokenize() } Note: there is no categories field on the sanitize result. Use shield.classify() to get categories.

shield.detokenize(sanitized: string, tokenMap: Map) -> string Reverse tokenization. Restores original values from token map.

shield.route(message: string, options?: object) -> RoutingDecision Full pipeline: classify + sanitize + route. Options: { chatId?: string, targetProvider?: string } Returns: { intent: string, // "agent" | "privacy" | "chat" sensitivity: SensitivityResult, recommendedProvider: string, // "privacy" | "standard" | "block" sanitized: SanitizeResult | undefined, auditId: number | undefined } Notes: - intent="agent" -> message likely needs tool execution in your app - intent="privacy" -> privacy keywords detected or sensitivity recommendation is "privacy"/"block" - intent="chat" -> safe-by-default routing - recommendedProvider is independent of intent and reflects threshold + per-chat level.

shield.sanitizeContext(memoryBlock: string, targetProvider: string) -> string Strip sensitive content from memory context blocks before sending to a provider.

shield.sanitizeMessage(content: string, targetProvider: string) -> string Sanitize message content for a specific provider.

shield.sanitizePrompt(systemPrompt: string, targetProvider: string) -> string Strip configured sections from system prompts for external providers.

shield.setPrivacyLevel(chatId: string, level: string) Set per-chat privacy level. Levels: "off" | "keywords" | "content" | "full"

  • off: no privacy enforcement, route everything to standard
  • keywords: only route on explicit privacy keywords
  • content: route based on classifier score (default)
  • full: always route to privacy provider

shield.getPrivacyLevel(chatId: string) -> string Get per-chat privacy level. Returns "content" if not set.

shield.getAuditStats(chatId?: string, since?: number) -> AuditStats Returns: { totalRequests, privacyRouted, standardRouted, blocked, topCategories, averageSensitivityScore } Returns the empty-stats object if audit logging is disabled or unavailable.

shield.getAuditEntries(chatId: string, limit?: number) -> AuditEntry[] Raw audit log entries for a specific chat. Default limit 50. Returns [] if audit logging is disabled or unavailable.

shield.exportAuditCSV(since: number) -> string Export audit entries as CSV from a Unix epoch timestamp. Returns "" if audit logging is disabled or unavailable.

shield.info() -> object Returns: { version, personalNamesConfigured, defaultMode, sensitivityThreshold, blockThreshold, auditEnabled, providers, privacyProviders, activeSessions }

shield.close() Clean shutdown. Closes database connections. Call before process exit.

CLI Commands

node pm-privacy-guard.mjs Start with config.yaml node pm-privacy-guard.mjs --demo Run demo classification on sample inputs node pm-privacy-guard.mjs --info Show current configuration node setup-wizard.mjs Interactive setup, writes config.yaml node license-tool.mjs generate --tier standard --type perpetual --holder "email" node license-tool.mjs validate node license-tool.mjs info

Config Schema (config.yaml)

personal_names: string[] # names to detect and protect default_mode: string # redact | mask | tokenize sensitivity_threshold: number # 0.0-1.0 (default 0.4) block_threshold: number # 0.0-1.0 (default 0.9) providers: - name: string # provider name type: string # "privacy" | "local" | "standard" strip_sections: string[] # system prompt sections to remove for external providers harness: string # claude-code | openai-shape | anthropic-direct | custom (default: claude-code) privacy_backend: string # local | venice (default: local) audit: enabled: boolean db_path: string # path to SQLite audit database audit_log: # optional; omit the block to disable at-rest encryption passphrase: string # 32-char alphanumeric, written by setup-wizard.mjs license: string # license key (optional)

Workflow

  1. npm install
  2. node setup-wizard.mjs (or cp config.example.yaml config.yaml and edit)
  3. In your application code: a. Import and initialize PrivacyShield b. Before sending any user message to an external AI provider, call shield.route(message) c. If recommendedProvider is "block", reject the message d. If recommendedProvider is "privacy", send to a zero-retention provider (Venice, Ollama) e. If recommendedProvider is "standard", safe to send to any provider f. Use the sanitized output from the route() result when sending to external providers
  4. Call shield.close() on process exit

Protocol and Recipes

Classify and route a message before sending to an LLM:

const decision = shield.route(userMessage); if (decision.recommendedProvider === 'block') { return 'Message blocked: contains highly sensitive content.'; } const textToSend = decision.sanitized ? decision.sanitized.sanitized : userMessage; const provider = decision.recommendedProvider; // "privacy" or "standard" // Send textToSend to the appropriate provider

Sanitize and restore (round-trip tokenization):

const result = shield.sanitize("My SSN is 123-45-6789", { mode: "tokenize" }); // result.sanitized = "My SSN is [TOKEN-a7f3b2]" // Send result.sanitized to LLM, get response back const restored = shield.detokenize(llmResponse, result.tokenMap); // Original values restored in the response

Batch-sanitize messages before sending context to an external provider:

const messages = [ { role: 'user', content: 'My balance is $50,000' }, { role: 'assistant', content: 'I can help with that.' }, ]; const cleaned = messages.map(m => ({ ...m, content: shield.sanitizeMessage(m.content, 'openai'), }));

Per-chat privacy override (force all messages through privacy provider):

shield.setPrivacyLevel('chat-123', 'full'); const decision = shield.route("any message", { chatId: 'chat-123' }); // decision.recommendedProvider will always be "privacy"

Requirements

  • Node.js >= 20.0.0
  • npm

Security

PM Privacy Guard runs entirely on your machine. No data is sent to any external service by this module. The routing decisions are recommendations for your application to act on -- PM Privacy Guard itself never transmits data.

PM Privacy Guard ships as plain JavaScript. Source is readable. License keys are HMAC-signed JWTs that authenticate the licensee. Customers, security reviewers, and auditors can read every line of the engine before it touches their data.

If a deployment requires source-level obfuscation, wrap PM Privacy Guard inside your own bundler (esbuild, webpack, ncc) under your own pipeline -- the package does not ship a bytecode build itself.

Support


Built by Permanently Mobile