SurrogateShield Documentation

SurrogateShield is a privacy-preserving proxy for LLMs. It intercepts every message before it reaches an LLM API, replaces personally identifiable information (PII) with realistic fake surrogates, sends the sanitised payload, and restores your originals in the response. All cryptographic operations run locally. Nothing sensitive is ever transmitted.

SurrogateShield ships in two forms:

  • Interactive dashboard — a full terminal UI with chat, PII Finder, batch evaluation, and the Attacker Experiment
  • pip install surrogateshield — a standalone Python library with a five-function API you can embed in any application

Installation

Requirements

  • Python 3.9 or newer
  • pip
  • A virtual environment (strongly recommended — see below)
Important: Always install into a virtual environment. Installing into the system or base conda Python is the most common cause of "package not found" errors at runtime.

Step-by-step

1

Clone the repository

git clone https://github.com/sherwinvishesh/SurrogateShield.git
cd SurrogateShield
2

Create and activate a virtual environment

# macOS / Linux
python -m venv .venv
source .venv/bin/activate

# Windows
python -m venv .venv
.venv\Scripts\activate

You should see (.venv) in your prompt after activation.

3

Install Python dependencies

pip install -r requirements.txt

This installs everything: the Anthropic client, spaCy, Faker, cryptography, sentence-transformers, and more. First install can take 2–5 minutes.

4

Download the spaCy model

python -m spacy download en_core_web_lg

Required by EntityTrace (stage 2 NER detection) and optionally by the Presidio comparison panel. This is a one-time download (~700 MB).

5

ContextGuard model (automatic)

The dslim/distilbert-NER model (~250 MB) is downloaded automatically from HuggingFace Hub on first run. No manual command needed. Subsequent runs are instant — it is cached locally.

Full dependency list

PackageVersionPurpose
anthropic≥0.25.0Claude API client
python-dotenv≥1.0.0.env file loading
spacy≥3.7.0EntityTrace NER + Presidio NLP backend
faker≥24.0.0Surrogate generation
cryptography≥42.0.0AES-256-GCM, HKDF for ShadowMap
rapidfuzz≥3.6.0Fuzzy reconstruction matching in ResolvePass
typer≥0.12.0CLI framework
rich≥13.7.0Terminal UI
chromadb≥0.4.0RAG vector store (in-process)
sentence-transformers≥2.7.0RAG embeddings (all-MiniLM-L6-v2)
transformers≥4.40.0ContextGuard (distilbert-NER)
torch≥2.0.0ContextGuard inference
requests≥2.31.0Address verification via Nominatim
bert-score≥0.3.13Utility preservation scoring (optional)
ollama≥0.1.8Local LLM support (optional)
presidio-analyzer≥2.2.0Presidio comparison panel (optional)
presidio-anonymizer≥2.2.0Presidio anonymization (optional)

Environment Variables

Create a .env file in the project root. You only need the key for the provider you intend to use:

# Required for Claude (default provider)
ANTHROPIC_API_KEY=sk-ant-...

# Required for Gemini
GEMINI_API_KEY=AIzaSy...

# Required for ChatGPT
OPENAI_API_KEY=sk-...

# No key needed for local Ollama

The .env file is listed in .gitignore — it will never be accidentally committed.

Quick Start

# 1. Activate your venv
source .venv/bin/activate

# 2. Add your API key
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env

# 3. Launch the dashboard
./run.sh
# or: python main.py

The launcher script run.sh activates the venv and loads the .env file automatically.

First Run

On the first launch, ContextGuard downloads its model (~250 MB) from HuggingFace Hub. This may take 1–5 minutes depending on your connection. Progress is shown in the terminal. After that, the model is cached at ~/.cache/huggingface/ and loads instantly.

The interactive dashboard will appear. You will see:

  • The SurrogateShield ASCII banner
  • The pipeline overview (7 stages)
  • A list of any saved conversations
  • The action menu

Press N to start a new conversation. Type any message containing PII and watch the pipeline work.

Pipeline Overview

Every message you type goes through the following stages before it reaches the LLM. On the way back, surrogates in the response are replaced with your originals before you see anything.

ServiceQueryDetector
detection/service_query.py
Checks if the message is a location query (e.g. "restaurants near 1126 E Apache Blvd"). If yes, applies minimal address fuzzing instead of full replacement. Sensitive keywords (medical, legal, shelter) override this and force full detection.
PatternScan
detection/pattern_scan.py · Stage 1
Regex-based detection for structured PII: SSNs, emails, phone numbers, credit cards, dates of birth, IP addresses, API keys, ZIP codes, crypto wallets, ABA routing numbers, driver's licenses. Runs first so structured PII is masked before any NER model sees it.
EntityTrace
spaCy en_core_web_lg · Stage 2
Named-entity recognition for unstructured PII: people, places, organisations, facilities. Returns two confidence tiers — confirmed (≥0.85) and borderline (0.60–0.85). Borderline candidates are forwarded to ContextGuard.
ContextGuard
dslim/distilbert-NER · Stage 3
Transformer-based verifier that confirms borderline entities from EntityTrace and independently catches anything missed. Word-piece artefact cleaning and a short-token blocklist prevent common false positives.
MimicGen
generation/logic.py
Generates type-consistent surrogates using Faker. A fake name looks like a name, a fake SSN passes format checks, a fake credit card passes the Luhn algorithm. No collisions within a session.
ShadowMap
storage/logic.py · AES-256-GCM
Stores the {surrogate → original} mapping encrypted with AES-256-GCM. A per-conversation key is derived via HKDF-SHA256 using a device-local secret. The map file is unreadable without the device key.
LLM API
HTTPS · surrogates only
Receives only surrogates — your real values never leave the device. The provider cannot see original PII in the prompt, conversation history, or RAG context.
ResolvePass
reconstruction/logic.py
Three-tier restoration: exact match → component match (partial name mentions) → fuzzy match (rapidfuzz ≥85). Restores your originals in the LLM response before you see it.

ServiceQueryDetector

Not all location information is equally sensitive. A message like "restaurants near 1126 E Apache Blvd, Tempe, AZ" needs the city and state to give a useful answer — fully replacing them with fake values would destroy the response quality.

ServiceQueryDetector identifies these location queries and applies a proportional response:

  • Street addresses: house number is shifted by ±2–8 (max ~100 m geographic error), street name and city are preserved
  • City/state names: not replaced — they provide no meaningful re-identification risk in service queries
  • Sensitive topic override: if the message contains keywords like medical, legal, shelter, or immigration, full anonymisation is forced regardless of query structure
Original message
restaurants near 1126 E Apache Blvd, Tempe, AZ
Sent to LLM
restaurants near 1128 E Apache Blvd, Tempe, AZ

Address existence is optionally verified via OpenStreetMap Nominatim (disable with SERVICE_QUERY_VERIFY_ADDRESSES = False in config.py for offline use).

SentinelLayer — The Detection Cascade

Three detectors run sequentially. Spans claimed by an earlier stage are not re-processed downstream, preventing double-detection. Each stage is described in detail below.

PatternScan — Stage 1

Regex-based structural detection. Runs first so all structured PII is masked before any NER model sees it. Format validators (Luhn, ABA checksum) reject false positives before any entity is emitted.

PatternExampleValidator
Street address99 Cathedral Close, 456 Innovation Plaza
SSN544-87-2944
Emailuser@example.com
Phone US+1-480-555-1234
Phone UK+44 7911 123456
Phone international+49 8234 927461
Credit card4111 1111 1111 1111Luhn algorithm
Date of birth01/15/1990, March 14 1990
IPv4192.168.1.100
API key / secretsk-ant-..., Bearer ..., ghp_..., AKIA..., AIzaSy...
Gender indicatorgender: female, she/her, I am a man
UK postcodeSW1A 1AA
US ZIP code85281, 85281-1234
Crypto wallet newBitcoin P2PKH/P2SH/Bech32, Ethereum 0x...
ABA routing number new021000021, 122105155ABA 9-digit checksum
US driver's license newB7654321, F123456789012Context-gated (keyword required)
Pattern order matters. Patterns claim character spans. Later patterns cannot overlap earlier ones. crypto and us_bank_number run before zip_us so that 9-digit routing numbers and long hex strings are claimed before the ZIP pattern can fragment them.

ABA routing number checksum

The ABA 9-digit checksum eliminates false positives — random 9-digit numbers almost always fail:

(3·d₀ + 7·d₁ + d₂ + 3·d₃ + 7·d₄ + d₅ + 3·d₆ + 7·d₇ + d₈) mod 10 = 0

Driver's license context gate

The driver's license regex requires a keyword (driver's license, license number, DL, D.L.) within the same phrase. Only the license value itself is marked as an entity — the keyword prefix stays in the sanitised text.

EntityTrace — Stage 2

spaCy en_core_web_lg Named Entity Recognition. Extracts PERSON, GPE, LOC, ORG, and FAC entities. Returns two confidence tiers:

TierScore rangeAction
Confirmed≥ 0.85Promoted immediately to the entity list
Borderline0.60 – 0.85Forwarded to ContextGuard for verification
Fallback (CG off)≥ 0.65Promoted when ContextGuard is disabled

EntityTrace also includes an ORG→GPE reclassifier: when a location preposition appears before an entity name (e.g. "lives in Google"), the classification is corrected from ORG to GPE.

ContextGuard — Stage 3

Transformer-based verifier using dslim/distilbert-NER (~250 MB, downloaded once from HuggingFace Hub on first run, no server required). Two jobs:

  1. Confirm borderline candidates forwarded from EntityTrace (requires score ≥ 0.70)
  2. Independently detect anything EntityTrace missed

Applies word-piece artefact cleaning and a blocklist of short/title tokens that commonly cause false positives (e.g. "I", "A", "The").

ContextGuard can be disabled via config.py or via the library's shield.config(context_guard_enabled=False) for faster, spaCy-only detection.

Post-processing Passes

Four additional passes run on the combined entity set from all three stages:

PassWhat it does
A — Structural ORGRegex for [the/a/an] <name> [corporation|company|corp|inc|ltd|llc…] — catches corporate names without relying on NER name lists
B — Email-username reclassificationCorrects ORG→PERSON when the entity text is a prefix of a detected email username (e.g. "alice" from alice@corp.com)
C — PERSON component dedupRemoves standalone surnames that are sub-components of already-detected full names (prevents "Mitchell" being a second entity when "Sarah Mitchell" is already detected)
D — Topical geo-entity filterDrops a GPE/LOC only if it appears exclusively in knowledge-query sub-clauses (e.g. "What is the capital of France?" — "France" is topical, not PII)

MimicGen — Surrogate Generation

Generates type-consistent surrogates using Faker. Each surrogate is realistic for its PII type and unique within the session (tracked via a used_surrogates set to guarantee no collisions).

Entity typeGenerated surrogate looks like
PERSONSarah Mitchell
emailjdoe@example.net
ssnXXX-XX-XXXX (valid format)
phone_us+1-###-###-####
phone_uk+44 7### ######
phone_intl+49 8234 927461
address789 Crescent Row, Springfield, IL
credit_cardValid Luhn-format number
dobMM/DD/YYYY (age 18–80)
ip_address10.x.x.x
zip_us / postcode_ukCorrect format
api_keysk- + 32 random chars
GPE / LOC / ORG / FACFaker city/company names
gender_indicatorGrammatically valid gender expression
crypto newBitcoin P2PKH format — 1 + Base58 chars, 26–35 chars
us_bank_number newValid 9-digit ABA routing number (passes checksum)
us_driver_license newCA-format: letter + 7 digits (e.g. B4923817)

Example: before and after substitution

Original
Hi, I'm Sherwin. My email is sjathann@asu.edu and my SSN is 544-87-2944.
Sent to LLM
Hi, I'm Sarah Mitchell. My email is jdoe@example.net and my SSN is 317-42-8806.

ShadowMap — Encrypted Storage

An encrypted, per-conversation mapping of {surrogate → original}. Stored on disk; unreadable without the device secret.

PropertyDetail
EncryptionAES-256-GCM with a fresh 12-byte nonce per save
Key derivationHKDF-SHA256 with device secret as IKM and conversation ID as salt
Device secretGenerated once at ~/.surrogateshield/device.key with 0o600 permissions
File locationconversations/<conv_id>.shadowmap (binary, not human-readable)
Formatnonce (12 bytes) ‖ AES-GCM ciphertext
Graceful degradationMissing or corrupt file → empty mapping, no crash

ResolvePass — Response Restoration

Three-pass restoration of original values in LLM responses:

1

Exact replacement

Handles the vast majority of cases. The surrogate string in the response is replaced with the original value verbatim.

2

Component matching

For multi-word surrogates (e.g. the LLM mentions only "Mitchell" when the surrogate was "Sarah Mitchell"), scoped to unresolved surrogates only to prevent corruption of adjacent text.

3

Fuzzy matching

rapidfuzz partial_ratio with a configurable threshold (default 85). Handles typos and paraphrasing by the LLM. Every failure is categorised as exact_miss, fuzzy_hit, or fuzzy_miss for research analysis.

CLI Commands

# Launch the interactive dashboard (recommended)
python main.py
./run.sh

# Start a new chat conversation directly
python main.py chat

# Continue a saved conversation
python main.py chat --load <conversation-id>

# New conversation with RAG enabled
python main.py chat --rag

# List all saved conversations
python main.py list

# PII detection sandbox — no API calls, no credits
python main.py pii-finder

# Index a document into the RAG vector store
python main.py add-doc path/to/document.txt

Dashboard Keyboard Shortcuts

These shortcuts work from the main dashboard screen.

KeyAction
NNew conversation
RNew conversation with RAG mode enabled
PPII Finder — test detection without any API call
19Open saved conversation by number
D1D9Delete conversation by number (e.g. D3 deletes conversation #3)
JJSON Test — batch-process a question file
EEvaluation — score pipeline quality against ground-truth
AAttacker Experiment — simulate adversarial PII recovery
SSettings (provider selection, view mode, Presidio comparison)
HHelp screen
QQuit

Chat Mode

Chat mode is the primary way to use SurrogateShield. Press N from the dashboard or run python main.py chat.

In each turn:

  1. Type your message and press Enter
  2. The full detection cascade runs on your message
  3. Detected PII is replaced with surrogates
  4. The sanitised message is sent to the LLM API
  5. The response is returned and surrogates are restored
  6. You see the response with your real values

If Detailed View is enabled (Settings → D), each turn also shows:

  • Pipeline stage logs
  • A per-entity PII detection table
  • The API transparency panel (what was sent, what was received, the final restored output)

Type exit or press Ctrl+C to return to the dashboard. Conversations are saved automatically.

PII Finder

The PII Finder is a detection sandbox with zero API calls and zero credits spent. Press P from the dashboard or run python main.py pii-finder.

Type any text to see exactly what SurrogateShield would detect and what it would send to the LLM. The service-query path (address fuzzing) is also active, so you can test how location queries are handled.

Special commands in PII Finder:

  • reset — clears the surrogate memory so you can start fresh
  • exit — returns to the dashboard

If the Presidio comparison panel is enabled in Settings, the PII Finder also shows a side-by-side comparison with Microsoft Presidio's [ENTITY_TYPE] placeholder redaction below each result.

RAG Mode

RAG (Retrieval-Augmented Generation) lets you index your own documents and have them automatically retrieved as context for your questions. Backed by ChromaDB and all-MiniLM-L6-v2 embeddings — no server required, runs in-process.

Indexing a document

python main.py add-doc path/to/your/document.txt

Documents are anonymised through the full SentinelLayer pipeline before indexing. Real PII never enters the vector store. Surrogate mappings from indexed documents are stored in a shared rag_global ShadowMap.

Starting a RAG conversation

Press R from the dashboard, or run python main.py chat --rag. Retrieved context is prepended to your sanitised message before the LLM call.

Configuration

Setting in config.pyDefaultEffect
RAG_TOP_K3Number of document chunks retrieved per query
RAG_CHUNK_SIZE512Characters per chunk when splitting indexed documents
EMBEDDING_MODELall-MiniLM-L6-v2sentence-transformers model for embeddings

JSON Batch Test

Batch-process a list of questions through the full pipeline. Press J from the dashboard.

Input format

Create a JSON file in the experiment/ directory:

[
  { "input": "My name is Sherwin and my SSN is 544-87-2944. What are Wyoming's tax benefits?" },
  { "input": "My email is sjathann@asu.edu and phone is 480-555-1234. Draft a resignation letter." }
]

Running

  1. Press J in the dashboard
  2. Enter the filename (e.g. test for experiment/test.json)
  3. Select which output fields to capture (toggle with number keys)
  4. Press Enter to run

Output is saved to experiment/<name>_answers.json. Results are flushed every 25 questions — safe to interrupt and resume.

Available output fields

FieldWhat it captures
questionThe original question text
pattern_scan_piiEntities detected by PatternScan (regex stage)
entity_trace_piiEntities detected by EntityTrace (spaCy NER)
context_guard_piiEntities detected by ContextGuard (distilbert-NER)
confirmed_piiFinal combined confirmed entity list
pii_detailPer-entity type, score, and source
quasi_id_risksQuasi-identifier combination risks detected
surrogate_mapMapping of original → surrogate for every replaced entity
sanitized_inputThe exact text sent to the LLM
llm_responseRaw LLM response (surrogates, before restoration)
stage_timings_msPatternScan / EntityTrace / ContextGuard / surrogate gen / LLM latency in ms
presidio_sanitized_inputPresidio [TYPE]-placeholder redaction (baseline for BERTScore)
presidio_found_piisPresidio raw detected entities — type, value, score
bertscore_ssBERTScore of original vs SurrogateShield sanitised input
bertscore_presidioBERTScore of original vs Presidio sanitised input
Note: presidio_sanitized_input, presidio_found_piis, bertscore_ss, and bertscore_presidio are off by default. Enable them in the field selection screen to generate data for the Presidio comparison and BERTScore tables in Evaluation.

Evaluation Mode

Score a completed answers file against a ground-truth key. Press E from the dashboard.

Ground-truth key format

[
  {
    "Question": "My name is Sherwin and my SSN is 544-87-2944...",
    "Answer-Key": {
      "name": "Sherwin",
      "ssn": "544-87-2944"
    }
  }
]

Save this as experiment/<name>_key.json. The evaluator accepts flexible label names — see the supported labels below.

Supported answer-key labels

Label(s) in key fileInternal type
name, person, PERSONPERSON
emailemail
phone, phone_us, phone_uk, phone_intlphone
ssnssn
addressaddress
dob, date_of_birthdob
org, ORG, organizationORG
gpe, GPE, locationGPE
credit_cardcredit_card
api_keyapi_key
ip, ip_addressip_address
zip, zip_us, postcode, postcode_ukpostal_code
gendergender_indicator
fac, FACFAC
crypto, bitcoin, ethereum, wallet newcrypto
bank_number, us_bank_number, routing_number, routing newus_bank_number
driver_license, us_driver_license, dl, license newus_driver_license

Metrics reported

MetricDescription
Precision / Recall / F1 / AccuracyOverall surrogate detection quality
Error (miss rate)Fraction of key PII values not detected
Stage timingsAverage ms per pipeline stage
ResolvePass leak rateFraction of responses where a surrogate was not restored
Sanitisation qualityFraction of questions where real PII reached the LLM
Per-entity-type breakdownF1 / precision / recall per PII type
Presidio comparison (Table 1)SS vs Presidio side-by-side per comparable type + overall
BERTScore comparison (Table 2)Semantic utility preservation: SS vs Presidio vs no-anonymisation baseline
Ablation study (Table 4)Per-stage F1 contribution across four pipeline configurations

Ablation study

The ablation study quantifies how much each detection stage contributes to overall F1. It is computed post-hoc from the existing answers file — no pipeline re-run needed. Each answer records which stage detected each entity via the source field in pii_detail.

ConfigurationDetected set
PatternScan onlypattern_scan_pii
PatternScan + EntityTracepattern_scan_pii ∪ entity_trace_pii
PatternScan + ContextGuardpattern_scan_pii ∪ context_guard_pii
Full cascade (all three)confirmed_pii

To generate ablation data, enable pattern_scan_pii, entity_trace_pii, context_guard_pii, and confirmed_pii in JSON Test before running (all four are on by default).

Attacker Experiment

Simulates an informed adversary who intercepts the sanitised text SurrogateShield sends to the LLM API and actively tries to recover the original PII values. Press A from the dashboard.

What it tests

Two variants run on each question from an existing answers file:

VariantWhat the attacker seesGoal
SurrogateShieldSanitised text with realistic fake values (fake names, SSNs, emails)Try to recover the originals from the surrogates
PresidioRedacted text with [ENTITY_TYPE] placeholder tokensTry to recover the originals from the placeholders

Expected result

0% recovery for both systems. Surrogates have no cryptographic or statistical relationship to the original values. This proves SurrogateShield achieves equivalent inference resistance to blunt placeholder redaction, while preserving significantly higher semantic utility (as measured by BERTScore).

Running the experiment

Press A in the dashboard. The four-screen flow will:

  1. Explain the experiment
  2. Prompt for an existing answers file from experiment/ (must contain surrogate_map, sanitized_input, and optionally presidio_sanitized_input / presidio_found_piis)
  3. Show question counts and estimated API calls, then run
  4. Display a results summary on completion

The experiment supports resume — if interrupted, re-running with the same answers file picks up where it left off.

Output files

FileContents
<stem>_Attacker_Experiment.jsonPer-question recovery attempt details for both variants
<stem>_Attacker_Experiment_Analysis.jsonAggregated recovery rates, per-type breakdown, overall assessment

Required fields in answers file

FieldRequired for
surrogate_mapSS attacker — original PII values and their surrogates
sanitized_inputSS attacker — the text to attack
pii_detailSS attacker — type metadata for the prompt
presidio_sanitized_inputPresidio attacker — the placeholder-redacted text
presidio_found_piisPresidio attacker — type metadata for the prompt

PII Types Detected

SurrogateShield detects 20 PII types across three detection mechanisms:

CategoryTypes
Structural (regex) PatternScanSSN, email, phone (US/UK/international), credit card, street address, DOB, IPv4, API keys/secrets, gender indicator, US ZIP, UK postcode, crypto wallet (Bitcoin/Ethereum), ABA routing number, US driver's license
Named entities (NER) EntityTrace / ContextGuardPERSON, GPE (geo-political entity), LOC, ORG, FAC (facility)
InferredImplicit location from context

PII Examples

Structured PII (PatternScan)

# SSN
544-87-2944  →  317-42-8806

# Email
sjathann@asu.edu  →  jdoe@example.net

# Phone US
+1-480-555-1234  →  +1-602-555-9871

# Credit card (Luhn-valid)
4111 1111 1111 1111  →  5234 5678 9012 3456

# Bitcoin wallet
1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf  →  1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2

# ABA routing number
021000021  →  071000013

# Driver's license (context-gated)
My DL number is B7654321  →  My DL number is F2390481

Named entities (EntityTrace + ContextGuard)

# Person
Sherwin  →  Sarah Mitchell

# City / GPE
Phoenix, AZ  →  Springfield, IL (in sensitive contexts)
# Note: well-known cities are often preserved in service queries

# Organisation
Arizona State University  →  Lakewood Industries

# Facility
Banner Health Hospital  →  Riverside Medical Center

Quasi-Identifier Risk

Based on Sweeney's k-anonymity research. Even without traditional PII, certain combinations of fields can uniquely re-identify individuals. SurrogateShield detects these and warns you before the message is sent.

CombinationRisk levelNotes
ZIP + DOB + GenderHigh87% of US population uniquely identifiable (Sweeney 2000)
Postcode + DOBHighHigh re-identification risk even without a name
Name + Employer + LocationMediumCombination enables targeted lookup
IP + LocationMediumTogether they narrow to a specific user

Warnings appear in the terminal when these combinations are detected. Enable Detailed View (Settings → D) to always show quasi-identifier analysis.

Runtime Settings

Changed interactively from inside the app (press S → Settings). Persisted across sessions in ~/.surrogateshield/settings.json.

KeyDefaultWhat it controls
llm_providerclaudeActive LLM backend — Claude / Gemini / ChatGPT / Local
detailed_viewfalseShow pipeline stage logs, per-entity PII table, and the API transparency panel in each chat turn
presidio_comparisonfalseShow the Presidio side-by-side panel below each PII Finder result. Off by default — requires presidio packages and spaCy model

Advanced Configuration (config.py)

Hard-coded thresholds and flags. Edit the file directly to change them. No restart required for PII Finder; restart required for chat sessions.

SettingDefaultDescription
ENTITY_TRACE_HIGH_THRESHOLD0.85spaCy score above which an entity is immediately confirmed
ENTITY_TRACE_LOW_THRESHOLD0.60spaCy score above which an entity is forwarded to ContextGuard
CONTEXT_GUARD_CONFIDENCE_THRESHOLD0.70distilbert score required to confirm a borderline entity
ENTITY_TRACE_FALLBACK_THRESHOLD0.65Score used when ContextGuard is disabled to promote borderline entities
FUZZY_MATCH_THRESHOLD85rapidfuzz partial_ratio threshold for ResolvePass reconstruction
SERVICE_QUERY_DETECTION_ENABLEDTrueEnable the lightweight address-fuzzing path for location queries
SERVICE_QUERY_VERIFY_ADDRESSESTrueVerify fuzzed addresses via OpenStreetMap Nominatim (disable for offline use)
SHOW_API_TRANSPARENCYTrueShow the sent / received / restored transparency panel after each chat turn
RAG_TOP_K3Number of document chunks retrieved per RAG query
RAG_CHUNK_SIZE512Characters per chunk when splitting indexed documents
CLAUDE_MODELclaude-sonnet-4-6Claude model identifier
SPACY_MODELen_core_web_lgspaCy model used by EntityTrace and Presidio
EMBEDDING_MODELall-MiniLM-L6-v2sentence-transformers model for RAG embeddings

LLM Providers

Switch providers from the Settings menu (press S inside the dashboard).

ProviderModelEnv var required
Claude (default)claude-sonnet-4-6ANTHROPIC_API_KEY
Geminigemini-1.5-flashGEMINI_API_KEY
ChatGPTgpt-4o-miniOPENAI_API_KEY
Local (Ollama)llama3.2 (configurable)None — runs fully offline

Enabling Presidio comparison

The Presidio comparison panel is off by default. To enable it:

  1. Activate your venv, then install: pip install presidio-analyzer presidio-anonymizer
  2. Download the spaCy model (if not already done): python -m spacy download en_core_web_lg
  3. Open the app and press SC (Settings → Presidio Comparison) to toggle it on

Python Library — Installation

SurrogateShield is also available as a standalone pip package. No dashboard, no CLI — just the core detection and masking pipeline as a clean Python API.

pip install surrogateshield

Both the spaCy model (en_core_web_lg) and the ContextGuard transformer model (dslim/distilbert-NER) install automatically — no separate download step needed.

Self-contained: The library does not import from the main application and carries its own copies of the detection, generation, storage, and reconstruction modules. You can use it independently of the dashboard.

Python Library — Quick Start

import surrogateshield as shield

# Silence the default Rich tables (recommended for production)
shield.config(detailed_view=False)

user_text = (
    "Hi, I'm Sarah Mitchell. My email is sarah.mitchell@gmail.com, "
    "my SSN is 123-45-6789, and I was born on 04/12/1990."
)

sanitized = shield.mask(user_text)
# "Hi, I'm Rachel Torres. My email is torresrachel@yahoo.com,
#  my SSN is 876-32-1045, and I was born on 09/27/1983."

response = any_llm.chat(sanitized)   # send surrogates, not real PII
restored = shield.unmask(response)   # real values restored from the session map
shield.flush()                       # clear session before the next conversation

Python Library — Public API

FunctionDescription
shield.config(**kwargs)Set thresholds, storage mode, pii_off list, and display options
shield.mask(text)Detect all PII, replace with surrogates, store the mapping. Returns the sanitised string.
shield.unmask(response)Restore original PII in an LLM response (any provider object or plain string)
shield.scan(text)Detect PII and return {value: type} dict — no substitution, no shadow map update
shield.pii_finderAlias for shield.scan
shield.flush()Clear the session shadow map and generate a new session ID

scan() — Inspect Without Masking

scan() runs the full detection cascade and returns a {value: type} dict, but it never modifies the session or updates the ShadowMap. Safe to call in a read-only context.

found = shield.scan(
    "Contact Alice Nguyen at alice@corp.com or call +1-415-555-0198."
)
# {"Alice Nguyen": "PERSON", "alice@corp.com": "email", "+1-415-555-0198": "phone_us"}

for value, pii_type in found.items():
    print(f"{pii_type:15s}  {value}")

scan() always returns every detected entity regardless of pii_off settings.

pii_off — Suppress Specific Types

Sometimes you want to detect certain PII types but not replace them. Use pii_off to suppress substitution for specific types while still detecting them.

shield.config(pii_off=["location", "org"])

sanitized = shield.mask(
    "Emma Johnson works at Deloitte in New York, email emma@deloitte.com."
)
# "Emma Johnson"      → replaced (PERSON not in pii_off)
# "emma@deloitte.com" → replaced (email not in pii_off)
# "Deloitte"          → kept  (org in pii_off)
# "New York"          → kept  (location in pii_off)

Available aliases for pii_off

phone, name, location, org, email, ssn, dob, address, zip, postcode, postal_code, credit_card, ip_address, api_key, crypto, bank, license, gender_indicator

Raw type strings ("PERSON", "GPE", etc.) also accepted.

Persistent ShadowMap

By default the session mapping is in-memory only. For web servers or multi-process deployments, point pii_mem at a directory and the map is encrypted to disk with AES-256-GCM.

import os, surrogateshield as shield

os.makedirs("/var/app/shadowmaps", exist_ok=True)
shield.config(pii_mem="/var/app/shadowmaps")

sanitized = shield.mask("My name is Clara Oswald, phone 555-123-4567.")
restored  = shield.unmask(llm.chat(sanitized))
shield.flush()   # deletes the .shadowmap and .key files for this session

When pii_mem is a directory path, each session gets its own .shadowmap file encrypted with AES-256-GCM. shield.flush() deletes the files and generates a new session ID.

Provider Examples

unmask() accepts native SDK response objects directly — no manual text extraction needed.

Anthropic / Claude

import anthropic
import surrogateshield as shield

client = anthropic.Anthropic()
shield.config(detailed_view=False)

sanitized = shield.mask(user_text)
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[{"role": "user", "content": sanitized}]
)
print(shield.unmask(response))   # passes Anthropic Message object directly
shield.flush()

OpenAI / ChatGPT

from openai import OpenAI
import surrogateshield as shield

client = OpenAI()
shield.config(detailed_view=False)

sanitized = shield.mask(user_text)
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": sanitized}]
)
print(shield.unmask(response))   # passes ChatCompletion object directly
shield.flush()

Google Gemini

import google.generativeai as genai
import surrogateshield as shield

model = genai.GenerativeModel("gemini-1.5-flash")
shield.config(detailed_view=False)

sanitized = shield.mask(user_text)
response = model.generate_content(sanitized)
print(shield.unmask(response))   # passes GenerateContentResponse directly
shield.flush()

Ollama / plain string

import surrogateshield as shield

shield.config(detailed_view=False)
sanitized = shield.mask(user_text)
raw_reply = ask_ollama(sanitized)      # plain string
print(shield.unmask(raw_reply))        # plain strings work too
shield.flush()

Library Config Parameters

ParameterDefaultEffect
detailed_viewTruePrint Rich tables after each call; set False in production
pii_mem"temp""temp" for in-memory; a directory path for AES-256-GCM disk persistence
pii_off[]PII types to detect but not replace
serviceTrueMinimal address fuzzing for location queries instead of full replacement
context_guard_enabledTrueRun the HuggingFace second-pass NER; set False for faster, spaCy-only detection
fuzzy_threshold85rapidfuzz score (0–100) used in the third reconstruction pass of unmask()
spacy_modelen_core_web_lgspaCy model for EntityTrace; swap for en_core_web_sm for faster but lower-accuracy NER

Security Design

ComponentMechanism
Device secret32-byte random key at ~/.surrogateshield/device.key, permissions 0o600
Per-conversation keyHKDF-SHA256 with device secret as IKM and conversation ID as salt
ShadowMap encryptionAES-256-GCM with fresh 12-byte nonce per write
ShadowMap formatnonce (12 bytes) ‖ AES-GCM ciphertext — unreadable without device key
API transmissionOnly surrogates sent — real values never leave the device
Conversation historyStored locally in conversations/; JSON holds surrogate text, not originals
.gitignore*.shadowmap, conversations/*.json, device.key, .env all excluded

Privacy Guarantees

  • No PII crosses the API boundary. Every entity confirmed by SentinelLayer is replaced before the HTTP request is made.
  • Service queries get proportional protection. A restaurant search near your home address gets the house number shifted; the city name is preserved so the answer is useful. Sensitive topic overrides (medical, legal, shelter) force full anonymisation regardless.
  • Geographic generality is preserved. US states, countries, and major cities are never replaced — they provide no meaningful re-identification risk and destroying them would break answer quality.
  • Quasi-identifier risks are surfaced. If your message contains combinations like ZIP+DOB+gender that are statistically re-identifying even without traditional PII, you are warned before the message is sent.
  • RAG documents are anonymised at index time. Real PII never enters the vector store. Retrieval and context injection all operate on surrogates.
  • Financial and identity credentials are protected. Bitcoin/Ethereum wallet addresses, ABA routing numbers, and driver's license numbers are detected and replaced alongside traditional PII.

Project Structure

SurrogateShield/
├── main.py                  # CLI entry point and interactive dashboard
├── pipeline.py              # End-to-end message pipeline orchestration
├── config.py                # All constants and thresholds (single source of truth)
├── util.py                  # Shared dataclasses (DetectedEntity, Conversation)
├── settings_manager.py      # Persistent user settings (~/.surrogateshield/settings.json)
├── evaluator.py             # Precision/recall/F1 evaluation logic + Presidio/BERTScore/ablation
├── json_tester.py           # Batch JSON question processing
├── attacker.py              # Adversarial PII recovery experiment
├── run.sh                   # Launcher script (venv activation, .env loading)
│
├── detection/               # SentinelLayer — three-stage PII detection cascade
│   ├── logic.py             # Cascade orchestration + post-processing passes A–D
│   ├── pattern_scan.py      # PatternScan — regex-based structured PII detection
│   ├── entity_trace.py      # EntityTrace — spaCy NER (en_core_web_lg)
│   ├── context_guard.py     # ContextGuard — distilbert-NER (dslim/distilbert-NER)
│   ├── service_query.py     # ServiceQueryDetector — address fuzzing for location queries
│   ├── quasi_identifier.py  # Quasi-identifier combination risk scorer (k-anonymity)
│   └── geo_data.py          # Geographic pass-through whitelist (US states, countries)
│
├── generation/
│   └── logic.py             # MimicGen — type-consistent surrogate generation (Faker)
│
├── storage/
│   └── logic.py             # ShadowMap — AES-256-GCM encrypted surrogate mapping store
│
├── reconstruction/
│   └── logic.py             # ResolvePass — three-pass surrogate→original restoration
│
├── presidio/                # Presidio integration layer (optional comparison feature)
│   ├── engine.py            # Lazy singleton AnalyzerEngine wrapper
│   ├── detect.py            # detect(text) → list[PresidioEntity]
│   └── redact.py            # redact(text, entities) → [TYPE]-placeholder string
│
├── chatbot/
│   ├── chat.py              # Multi-provider LLM conversation handler
│   └── rag.py               # RAGStore — ChromaDB + sentence-transformers vector store
│
├── experiment/              # Batch test input/output files
│   ├── example.json         # Sample question file
│   ├── example_key.json     # Sample ground-truth answer key
│   └── example_answers.json # Sample output from JSON Test
│
├── tests/                   # Test suite (no API key required)
│   ├── test1.py             # PatternScan, cascade, MimicGen, ShadowMap, ResolvePass
│   ├── test2.py             # Additional detection and generation tests
│   ├── test3.py             # Additional pipeline and storage tests
│   ├── test6.py             # Attacker Experiment — all mocked, no API key needed
│   └── test7.py             # Python library — 134 checks, no API key required
│
└── python-library/          # Standalone pip package (surrogateshield)
    ├── pyproject.toml
    └── surrogateshield/     # Public API: config, scan, mask, unmask, flush

Running Tests

# Activate your venv first
source .venv/bin/activate

# Core pipeline tests (no API key required)
python tests/test1.py
python tests/test2.py
python tests/test3.py

# Attacker Experiment tests (all API calls mocked, no API key required)
python tests/test6.py

# Python library tests (run from repo root)
python tests/test7.py
Test fileWhat it covers
test1.pyPatternScan, EntityTrace, SentinelLayer cascade, MimicGen, ShadowMap, ResolvePass, and a full no-API pipeline round-trip
test2.pyAdditional detection and generation tests
test3.pyAdditional pipeline and storage tests
test6.pyAttacker Experiment module — type formatting, recovery scoring, API response parsing, analysis aggregation, and end-to-end experiment flow (all mocked)
test7.pyStandalone surrogateshield pip package — entities, ShadowMap memory and persistent modes, response parser, state singletons, pipeline pii_off filtering, threshold wiring, ResolvePass, and all five public API functions. 134 checks, no API key required.

Troubleshooting

Presidio shows "not installed"

This almost always means the packages were installed into a different Python than the one running the app. Fix:

# Activate your venv first, then:
pip install presidio-analyzer presidio-anonymizer
python -m spacy download en_core_web_lg

"Package not found" at runtime

You ran pip install without activating the venv first. The packages went into the system Python. Fix:

source .venv/bin/activate   # activate venv
pip install -r requirements.txt   # reinstall into venv

ContextGuard model download is slow

The dslim/distilbert-NER model is ~250 MB. On a slow connection, the first download can take 5–15 minutes. Progress is shown in the terminal. Once downloaded, it is cached at ~/.cache/huggingface/.

BERTScore takes very long

BERTScore uses roberta-large (~1.4 GB). On CPU, scoring a batch of 50 questions can take 15–30 minutes. The model is only downloaded if you enable BERTScore fields in JSON Test. If you have a GPU, it will be used automatically.

Address verification fails in offline environments

ServiceQueryDetector optionally verifies fuzzed addresses via OpenStreetMap Nominatim. In offline environments, disable this:

# In config.py
SERVICE_QUERY_VERIFY_ADDRESSES = False

Conversations are not found after moving the project

Conversations are stored in conversations/ relative to the project root. If you move the project directory, the existing .shadowmap files are no longer decryptable with the device key at ~/.surrogateshield/device.key — but the device key itself stays in place, so new conversations in the new location will work normally.

spaCy model not found

# With venv active:
python -m spacy download en_core_web_lg

SurrogateShield v1.4.0 · Apache-2.0 · Back to home