[CloakLLM]

Usage Guide

Complete guide to CloakLLM — installation, configuration, middleware integration, audit logs, and more.

PII protection middleware for LLMs — detect, tokenize, and audit before prompts leave your infrastructure.


Table of Contents


Installation

Python

pip install cloakllm
python -m spacy download en_core_web_sm

For LiteLLM middleware integration:

pip install cloakllm[litellm]

Requires Python 3.10+.

JavaScript

npm install cloakllm

Zero runtime dependencies. Requires Node.js 18+.

MCP Server

pip install cloakllm-mcp

Depends on cloakllm (the Python SDK).


Quick Start

Python — LiteLLM

One line to protect all your LLM calls:

import cloakllm
from cloakllm import ShieldConfig
 
cloakllm.enable(
    config=ShieldConfig(
        skip_models=["ollama/", "huggingface/"],
        log_dir="./audit_logs",
    )
)
 
# Use LiteLLM normally — CloakLLM works transparently
import litellm
 
response = litellm.completion(
    model="anthropic/claude-sonnet-4-20250514",
    messages=[
        {
            "role": "user",
            "content": (
                "Help me write a follow-up email to Sarah Johnson "
                "(sarah.j@techcorp.io) about the Q3 security audit. "
                "Her direct line is +1-555-0142. "
                "Reference ticket SEC-2024-0891."
            ),
        }
    ],
)
 
# Response is automatically desanitized — original names/emails restored
print(response.choices[0].message.content)
 
# Disable when done
cloakllm.disable()

Python — Standalone Shield

Use the Shield directly without any LLM framework:

from cloakllm import Shield
 
shield = Shield()
 
# Sanitize
prompt = (
    "Please draft an email to John Smith (john.smith@acme.com) about the "
    "Project Falcon deployment. His SSN is 123-45-6789 and the server is "
    "at 192.168.1.100. Use API key sk-abc123def456ghi789jkl012mno345pqr."
)
sanitized, token_map = shield.sanitize(prompt, model="claude-sonnet-4-20250514")
# sanitized → "Please draft an email to [PERSON_0] ([EMAIL_0]) about the ..."
 
# Desanitize an LLM response
llm_response = (
    "I've drafted the email to [PERSON_0] at [EMAIL_0] regarding "
    "Project Falcon. I noticed the server [IP_ADDRESS_0] may need "
    "additional security configuration before deployment."
)
restored = shield.desanitize(llm_response, token_map)
# restored → "I've drafted the email to John Smith at john.smith@acme.com ..."
 
# Analyze without modifying
analysis = shield.analyze("Call me at +972-50-123-4567 or email sarah@example.org")
# → { "entity_count": 2, "entities": [...] }

JavaScript — OpenAI SDK

One line to wrap your OpenAI client:

const { enable } = require('cloakllm');
const OpenAI = require('openai');
 
const client = new OpenAI();
enable(client);
 
const response = await client.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [
    {
      role: 'user',
      content:
        'Write a meeting reminder for sarah.j@techcorp.io ' +
        'about the Q3 security audit. Call +1-555-0142 if needed.',
    },
  ],
});
 
// PII automatically restored in the response
console.log(response.choices[0].message.content);

JavaScript — Vercel AI SDK

Use as language model middleware:

const { createCloakLLMMiddleware } = require('cloakllm');
const { generateText, streamText, wrapLanguageModel } = require('ai');
const { openai } = require('@ai-sdk/openai');
 
const middleware = createCloakLLMMiddleware({
  logDir: './example_audit',
  auditEnabled: true,
});
 
const model = wrapLanguageModel({
  model: openai('gpt-4o-mini'),
  middleware,
});
 
// Non-streaming
const { text } = await generateText({
  model,
  prompt: 'Write a reminder for sarah.j@techcorp.io about the Q3 audit.',
});
 
// Streaming
const result = streamText({
  model,
  prompt: 'Draft an email to sarah.j@techcorp.io about Project Falcon.',
});
 
for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}

JavaScript — Standalone Shield

Use the Shield directly without any LLM framework:

const { Shield, ShieldConfig } = require('cloakllm');
 
const config = new ShieldConfig({
  logDir: './example_audit',
  auditEnabled: true,
});
const shield = new Shield(config);
 
const text = `
  Name: Sarah Johnson
  Email: sarah.j@techcorp.io
  SSN: 123-45-6789
  Phone: +1-555-0142
  Credit Card: 4111111111111111
  Server: 192.168.1.100
`;
 
// Sanitize
const [sanitized, tokenMap] = shield.sanitize(text);
 
// Desanitize an LLM response
const llmResponse = `I've processed the customer record for [EMAIL_0].
Their SSN ([SSN_0]) has been verified. I'll send a confirmation to [PHONE_0].`;
const restored = shield.desanitize(llmResponse, tokenMap);
 
// Verify audit chain
const { valid, errors } = shield.verifyAudit();

MCP — Claude Desktop

Add CloakLLM to your claude_desktop_config.json:

{
  "mcpServers": {
    "cloakllm": {
      "command": "python",
      "args": ["/path/to/cloakllm-mcp/server.py"],
      "env": {
        "CLOAKLLM_LOG_DIR": "./cloakllm_audit",
        "CLOAKLLM_LLM_DETECTION": "false"
      }
    }
  }
}

Or using uvx:

{
  "mcpServers": {
    "cloakllm": {
      "command": "uvx",
      "args": ["mcp", "run", "/path/to/cloakllm-mcp/server.py"]
    }
  }
}

The MCP server exposes 3 tools:

sanitize — Detect and cloak PII, returns sanitized text + token map ID.

// Tool call
{ "text": "Email john@acme.com about the meeting with Sarah Johnson", "model": "claude-sonnet-4-20250514" }
 
// Response
{
  "sanitized": "Email [EMAIL_0] about the meeting with [PERSON_0]",
  "token_map_id": "a1b2c3d4-...",
  "entity_count": 2,
  "categories": { "EMAIL": 1, "PERSON": 1 }
}

desanitize — Restore original values using a token map ID.

// Tool call
{ "text": "I've drafted an email to [EMAIL_0] regarding [PERSON_0]'s request.", "token_map_id": "a1b2c3d4-..." }
 
// Response
{ "restored": "I've drafted an email to john@acme.com regarding Sarah Johnson's request." }

analyze — Detect PII without cloaking.

// Tool call
{ "text": "Contact john@acme.com, SSN 123-45-6789" }
 
// Response
{
  "entity_count": 2,
  "entities": [
    { "text": "john@acme.com", "category": "EMAIL", "start": 8, "end": 21, "confidence": 0.95, "source": "regex" },
    { "text": "123-45-6789", "category": "SSN", "start": 27, "end": 38, "confidence": 0.95, "source": "regex" }
  ]
}

How It Works

CloakLLM uses a multi-pass detection pipeline to find PII before it reaches an LLM provider.

3-Pass Detection

  1. Regex (both SDKs) — High-precision pattern matching for structured data: emails, SSNs, credit cards, phone numbers, IP addresses, API keys, AWS keys, JWTs, IBANs.

  2. spaCy NER (Python only) — Named entity recognition for names, organizations, and locations (PERSON, ORG, GPE). The JS SDK does not include spaCy; instead, these categories are handled by the optional Ollama LLM pass.

  3. Ollama LLM (opt-in, both SDKs) — Local LLM-based semantic detection for contextual PII: addresses, dates of birth, medical terms, financial data, and more. Data never leaves your machine.

Tokenization

Detected entities are replaced with deterministic tokens in [CATEGORY_N] format:

  • john@acme.com[EMAIL_0]
  • Sarah Johnson[PERSON_0]
  • 123-45-6789[SSN_0]

Tokens are deterministic — the same input produces the same token within a session. A TokenMap stores the bidirectional mapping and can be reused across multi-turn conversations.

Token injection is prevented by escaping fullwidth brackets in user input.

Audit Logs

Every sanitize/desanitize operation is logged to hash-chained JSONL files:

  • No PII stored — only hashes and token counts
  • Tamper-evident — each entry's prev_hash links to the previous entry's entry_hash (SHA-256)
  • Genesis hash — first entry links to "0" * 64
  • Designed for EU AI Act Article 12 compliance

Configuration Reference

Python ShieldConfig

OptionTypeDefaultEnv VarDescription
spacy_modelstr"en_core_web_sm"CLOAKLLM_SPACY_MODELspaCy model for NER
ner_entity_typesset[str]{"PERSON", "ORG", "GPE", "LOC", "FAC", "NORP", "EMAIL", "PHONE"}Entity types for spaCy NER
detect_emailsboolTrueDetect email addresses
detect_phonesboolTrueDetect phone numbers
detect_ssnsboolTrueDetect Social Security Numbers
detect_credit_cardsboolTrueDetect credit card numbers
detect_api_keysboolTrueDetect API keys
detect_ip_addressesboolTrueDetect IP addresses
detect_ibanboolTrueDetect IBAN numbers
custom_patternslist[tuple[str, str]][]Custom (name, regex) patterns
llm_detectionboolFalseCLOAKLLM_LLM_DETECTIONEnable Ollama LLM detection
llm_modelstr"llama3.2"CLOAKLLM_LLM_MODELOllama model name
llm_ollama_urlstr"http://localhost:11434"CLOAKLLM_OLLAMA_URLOllama server URL
llm_timeoutfloat10.0LLM request timeout (seconds)
llm_confidencefloat0.85Confidence threshold for LLM detections
descriptive_tokensboolTrue[PERSON_0] vs [TKN_A3F2]
preserve_formatboolFalsePreserve format in replacement
audit_enabledboolTrueEnable audit logging
log_dirPath./cloakllm_auditCLOAKLLM_LOG_DIRAudit log directory
log_original_valuesboolFalseLog original PII values (not recommended)
otel_enabledboolFalseCLOAKLLM_OTEL_ENABLEDEnable OpenTelemetry
otel_service_namestr"cloakllm"OTEL_SERVICE_NAMEOTel service name
auto_modeboolTrueAuto-sanitize in middleware
skip_modelslist[str][]Model prefixes to skip

JavaScript ShieldConfig

OptionTypeDefaultEnv VarDescription
detectEmailsbooleantrueDetect email addresses
detectPhonesbooleantrueDetect phone numbers
detectSsnsbooleantrueDetect Social Security Numbers
detectCreditCardsbooleantrueDetect credit card numbers
detectApiKeysbooleantrueDetect API keys
detectIpAddressesbooleantrueDetect IP addresses
detectIbanbooleantrueDetect IBAN numbers
customPatternsArray<{name, pattern}>[]Custom regex patterns
llmDetectionbooleanfalseCLOAKLLM_LLM_DETECTIONEnable Ollama LLM detection
llmModelstring"llama3.2"CLOAKLLM_LLM_MODELOllama model name
llmOllamaUrlstring"http://localhost:11434"CLOAKLLM_OLLAMA_URLOllama server URL
llmTimeoutnumber10000LLM request timeout (ms)
llmConfidencenumber0.85Confidence threshold for LLM detections
descriptiveTokensbooleantrue[PERSON_0] vs opaque tokens
auditEnabledbooleantrueEnable audit logging
logDirstring"./cloakllm_audit"CLOAKLLM_LOG_DIRAudit log directory
logOriginalValuesbooleanfalseLog original PII values (not recommended)
autoModebooleantrueAuto-sanitize in middleware
skipModelsstring[][]Model prefixes to skip

Environment Variables

These work across all three SDKs:

VariableDefaultDescription
CLOAKLLM_LOG_DIR./cloakllm_auditAudit log directory
CLOAKLLM_LLM_DETECTIONfalseEnable LLM-based detection
CLOAKLLM_LLM_MODELllama3.2Ollama model for LLM detection
CLOAKLLM_OLLAMA_URLhttp://localhost:11434Ollama server URL
CLOAKLLM_SPACY_MODELen_core_web_smspaCy model (Python only)
CLOAKLLM_AUDIT_ENABLEDtrueEnable/disable audit logging (MCP)
CLOAKLLM_OTEL_ENABLEDfalseEnable OpenTelemetry (Python only)
OTEL_SERVICE_NAMEcloakllmOpenTelemetry service name (Python only)

Multi-Turn Conversations

Reuse the token map across turns so the same entities always map to the same tokens.

Python

from cloakllm import Shield
 
shield = Shield()
 
# Turn 1
prompt1 = "Schedule a call with Sarah Johnson (sarah.j@techcorp.io) for Monday."
sanitized1, token_map = shield.sanitize(prompt1)
 
# Turn 2 — pass the same token_map
prompt2 = "Also invite john@acme.com to the call with Sarah Johnson."
sanitized2, token_map = shield.sanitize(prompt2, token_map=token_map)
# sarah.j@techcorp.io → [EMAIL_0] in both turns
# Sarah Johnson → [PERSON_0] in both turns
# john@acme.com → [EMAIL_1] (new entity, new token)
 
# Desanitize any response using the same token_map
restored = shield.desanitize(llm_response, token_map)

JavaScript

const { Shield } = require('cloakllm');
 
const shield = new Shield();
 
// Turn 1
const [sanitized1, tokenMap] = shield.sanitize(
  'Schedule a call with sarah.j@techcorp.io for Monday.'
);
 
// Turn 2 — pass the same tokenMap
const [sanitized2] = shield.sanitize(
  'Also invite john@acme.com to that call.',
  { tokenMap }
);
 
// Desanitize any response using the same tokenMap
const restored = shield.desanitize(llmResponse, tokenMap);

Custom Patterns

Add your own regex patterns to detect domain-specific PII.

Python

from cloakllm import Shield, ShieldConfig
 
config = ShieldConfig(
    custom_patterns=[
        ("EMPLOYEE_ID", r"EMP-\d{6}"),
        ("CASE_NUMBER", r"CASE-\d{4}-\d{4}"),
    ]
)
shield = Shield(config=config)
 
sanitized, token_map = shield.sanitize("Contact EMP-123456 about CASE-2024-0891")
# → "Contact [EMPLOYEE_ID_0] about [CASE_NUMBER_0]"

JavaScript

const { Shield, ShieldConfig } = require('cloakllm');
 
const config = new ShieldConfig({
  customPatterns: [
    { name: 'EMPLOYEE_ID', pattern: 'EMP-\\d{6}' },
    { name: 'CASE_NUMBER', pattern: 'CASE-\\d{4}-\\d{4}' },
  ],
});
const shield = new Shield(config);
 
const [sanitized, tokenMap] = shield.sanitize('Contact EMP-123456 about CASE-2024-0891');
// → "Contact [EMPLOYEE_ID_0] about [CASE_NUMBER_0]"

LLM-Powered Detection (Ollama)

Both SDKs support an optional local LLM pass via Ollama for detecting PII that requires contextual understanding.

Enabling

# Python
config = ShieldConfig(llm_detection=True)
// JavaScript
const config = new ShieldConfig({ llmDetection: true });

Or via environment variable:

export CLOAKLLM_LLM_DETECTION=true

What It Catches

CategoryExamples
ADDRESS742 Evergreen Terrace, Springfield
DATE_OF_BIRTHborn January 15, 1990
MEDICALdiabetes mellitus, blood type A+
FINANCIALaccount 4521-XXX, routing 021000021
NATIONAL_IDTZ 12345678
BIOMETRICfingerprint hash F3A2...
USERNAME@johndoe42
PASSWORDP@ssw0rd123
VEHICLEplate ABC-1234

In the JS SDK, the LLM pass also detects PERSON, ORG, and GPE (since JS has no spaCy NER).

Configuration

OptionPythonJavaScriptDefault
Modelllm_modelllmModel"llama3.2"
Server URLllm_ollama_urlllmOllamaUrl"http://localhost:11434"
Timeoutllm_timeoutllmTimeout10.0s / 10000ms
Confidencellm_confidencellmConfidence0.85

If Ollama is not running, the LLM pass is silently skipped.


Entity Detection Reference

CategoryExamplesDetection Method
EMAILjohn@acme.comRegex
PHONE+1-555-0142, 050-123-4567Regex
SSN123-45-6789Regex
CREDIT_CARD4111111111111111Regex
IP_ADDRESS192.168.1.100Regex
API_KEYsk-abc123..., AKIA...Regex
AWS_KEYAKIA1234567890ABCDEFRegex
JWTeyJhbGciOi...Regex
IBANDE89370400440532013000Regex
Custom(your patterns)Regex
PERSONJohn Smith, Sarah JohnsonspaCy NER (Python) / Ollama LLM (JS)
ORGAcme Corp, GooglespaCy NER (Python) / Ollama LLM (JS)
GPENew York, IsraelspaCy NER (Python) / Ollama LLM (JS)
ADDRESS742 Evergreen TerraceOllama LLM
DATE_OF_BIRTH1990-01-15Ollama LLM
MEDICALdiabetes mellitusOllama LLM
FINANCIALaccount 4521-XXXOllama LLM
NATIONAL_IDTZ 12345678Ollama LLM
BIOMETRICfingerprint hashOllama LLM
USERNAME@johndoe42Ollama LLM
PASSWORDP@ssw0rd123Ollama LLM
VEHICLEplate ABC-1234Ollama LLM

CLI

Both SDKs include a CLI for scanning text, verifying audit logs, and viewing statistics.

Python

# Scan text for PII
python -m cloakllm scan "Send email to john@acme.com, SSN 123-45-6789"
 
# Scan from stdin
echo "Contact sarah@example.org" | python -m cloakllm scan -
 
# Verify audit chain integrity
python -m cloakllm verify ./cloakllm_audit/
 
# Show audit statistics
python -m cloakllm stats ./cloakllm_audit/

JavaScript

# Scan text for PII
npx cloakllm scan "Send email to john@acme.com, SSN 123-45-6789"
 
# Verify audit chain integrity
npx cloakllm verify ./cloakllm_audit/
 
# Show audit statistics
npx cloakllm stats ./cloakllm_audit/

Example Output

scan:

Found 2 entities:
  [EMAIL]  "john@acme.com"    (confidence: 95%, source: regex)
  [SSN]    "123-45-6789"      (confidence: 95%, source: regex)

Sanitized:
  Send email to [EMAIL_0], SSN [SSN_0]

verify:

Audit chain integrity verified — no tampering detected.

stats:

{
  "total_events": 12,
  "total_entities_detected": 34,
  "categories": { "EMAIL": 10, "PERSON": 8, "SSN": 6, "PHONE": 5, "IP_ADDRESS": 5 }
}

Audit Logs

File Format

Audit logs are stored as JSONL files in the configured log directory:

cloakllm_audit/
  audit_2026-03-01.jsonl
  audit_2026-03-02.jsonl

Entry Structure

Each line is a JSON object with these key fields:

FieldDescription
event_idUnique event ID (UUID4)
seqSequence number within the file
timestampISO 8601 timestamp
event_type"sanitize", "desanitize", "shield_enabled", or "shield_disabled"
entity_countNumber of entities detected
categoriesMap of category → count
prompt_hashSHA-256 hash of the original text
sanitized_hashSHA-256 hash of the sanitized text
modelLLM model name (if provided)
providerLLM provider name (if provided)
tokens_usedList of tokens used (no original values)
latency_msProcessing time in milliseconds
metadataAdditional context (e.g., user_id, session_id)
prev_hashSHA-256 hash of the previous entry
entry_hashSHA-256 hash of this entry

No original PII is stored in audit logs — only hashes, token counts, and categories.

Verification

Python:

shield = Shield()
 
# Programmatic verification
is_valid, errors = shield.verify_audit()
 
# Statistics
stats = shield.audit_stats()

JavaScript:

const shield = new Shield();
 
// Programmatic verification
const { valid, errors } = shield.verifyAudit();
 
// Statistics
const stats = shield.auditStats();

CLI:

# Python
python -m cloakllm verify ./cloakllm_audit/
 
# JavaScript
npx cloakllm verify ./cloakllm_audit/

Tamper Detection

The hash chain makes tampering evident. Each entry's entry_hash is computed from its contents including prev_hash. If any entry is modified, deleted, or reordered, the chain breaks and verify_audit() / verifyAudit() reports the specific entries that fail validation.


Disabling / Re-enabling

Python (LiteLLM)

import cloakllm
 
cloakllm.enable()   # Start protecting LLM calls
cloakllm.disable()  # Stop — LiteLLM calls pass through unchanged
cloakllm.enable()   # Re-enable at any time

JavaScript (OpenAI SDK)

const { enable, disable } = require('cloakllm');
const OpenAI = require('openai');
 
const client = new OpenAI();
 
enable(client);    // Start protecting
disable(client);   // Stop — restore original client behavior
enable(client);    // Re-enable at any time