[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 — OpenAI SDK

One line to wrap your OpenAI client:

from cloakllm import enable_openai, ShieldConfig
from openai import OpenAI
 
client = OpenAI()
enable_openai(
    client,
    config=ShieldConfig(
        skip_models=["ollama/"],
        log_dir="./audit_logs",
    ),
)
 
# Use OpenAI normally — CloakLLM works transparently
response = client.chat.completions.create(
    model="gpt-4o-mini",
    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."
            ),
        }
    ],
)
 
# Response is automatically desanitized — original names/emails restored
print(response.choices[0].message.content)
 
# Disable when done
from cloakllm import disable_openai
disable_openai(client)

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": [...] }
 
# Per-entity metadata (no original text — PII-safe)
token_map.entity_details
# [{"category": "PERSON", "start": 0, "end": 10, ...}, ...]
 
# Full report for dashboards
token_map.to_report()
# {"entity_count": 5, "categories": {...}, "tokens": [...], "mode": "tokenize", "entity_details": [...]}

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

Important: MCP tools are called by the LLM, not before it. Your prompt is sent to the LLM provider first, then the LLM decides to call CloakLLM's tools. This means the MCP server cannot prevent PII in your prompt from reaching the provider. It is useful for sanitizing data the LLM works with during a conversation (documents, files, tool outputs). To protect prompts before they leave your infrastructure, use the SDK middleware instead (enable_openai / cloakllm.enable()).

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 6 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 },
  "entity_details": [
    { "category": "EMAIL", "start": 6, "end": 19, "length": 13, "confidence": 0.95, "source": "regex", "token": "[EMAIL_0]" },
    { "category": "PERSON", "start": 42, "end": 56, "length": 14, "confidence": 0.85, "source": "spacy", "token": "[PERSON_0]" }
  ]
}

sanitize_batch — Sanitize multiple texts with a shared token map.

// Tool call
{ "texts": ["Email john@acme.com", "SSN 123-45-6789"] }
 
// Response
{
  "sanitized": ["Email [EMAIL_0]", "SSN [SSN_0]"],
  "token_map_id": "a1b2c3d4-...",
  "entity_count": 2,
  "categories": { "EMAIL": 1, "SSN": 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." }

desanitize_batch — Restore original values in multiple texts using a shared token map.

// Tool call
{ "texts": ["Reply to [EMAIL_0]", "SSN is [SSN_0]"], "token_map_id": "a1b2c3d4-..." }
 
// Response
{ "restored": ["Reply to john@acme.com", "SSN is 123-45-6789"] }

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" }
  ]
}

analyze_batch — Analyze multiple texts for PII without cloaking.

// Tool call
{ "texts": ["Email john@acme.com", "SSN 123-45-6789"] }
 
// Response
{
  "results": [
    { "entity_count": 1, "entities": [{ "text": "john@acme.com", "category": "EMAIL", ... }] },
    { "entity_count": 1, "entities": [{ "text": "123-45-6789", "category": "SSN", ... }] }
  ],
  "total_entity_count": 2
}

How It Works

CloakLLM uses a multi-pass detection pipeline to find PII before it reaches an LLM provider. The pipeline is built from pluggable backends — you can replace or extend any stage (see Pluggable Detection Backends).

Default 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.

The TokenMap also exposes entity_details (Python) / entityDetails (JS) — per-entity metadata (category, offsets, confidence, source, token) without original text. Use to_report() / toReport() for a full summary suitable for compliance dashboards.

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
custom_llm_categorieslist[tuple[str, str]][]Custom (name, description) categories for LLM detection
llm_allow_remoteboolFalseCLOAKLLM_LLM_ALLOW_REMOTEAllow non-localhost Ollama URLs (SSRF prevention)
localestr""Locale for country-specific PII patterns (e.g., "de", "fr")
entity_hashingboolFalseCLOAKLLM_ENTITY_HASHINGEnable per-entity HMAC-SHA256 hashing
entity_hash_keystrNoneCLOAKLLM_ENTITY_HASH_KEYHMAC key (auto-generated if omitted)
context_analysisboolFalseCLOAKLLM_CONTEXT_ANALYSISEnable automatic context risk analysis
context_risk_thresholdfloat0.7Risk score threshold for warnings
descriptive_tokensboolTrue[PERSON_0] vs [TKN_A3F2]
audit_enabledboolTrueEnable audit logging
log_dirPath./cloakllm_auditCLOAKLLM_LOG_DIRAudit log directory
otel_enabledboolFalseCLOAKLLM_OTEL_ENABLEDEnable OpenTelemetry
otel_service_namestr"cloakllm"OTEL_SERVICE_NAMEOTel service name
auto_modeboolTrueAuto-sanitize in middleware
modestr"tokenize""tokenize" (reversible) or "redact" (irreversible)
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
customLlmCategoriesArray<{name, description?}>[]Custom categories for LLM detection
llmAllowRemotebooleanfalseCLOAKLLM_LLM_ALLOW_REMOTEAllow non-localhost Ollama URLs (SSRF prevention)
localestring""Locale for country-specific PII patterns (e.g., "de", "fr")
entityHashingbooleanfalseCLOAKLLM_ENTITY_HASHINGEnable per-entity HMAC-SHA256 hashing
entityHashKeystringundefinedCLOAKLLM_ENTITY_HASH_KEYHMAC key (auto-generated if omitted)
contextAnalysisbooleanfalseCLOAKLLM_CONTEXT_ANALYSISEnable automatic context risk analysis
contextRiskThresholdnumber0.7Risk score threshold for warnings
descriptiveTokensbooleantrue[PERSON_0] vs opaque tokens
auditEnabledbooleantrueEnable audit logging
logDirstring"./cloakllm_audit"CLOAKLLM_LOG_DIRAudit log directory
autoModebooleantrueAuto-sanitize in middleware
modestring"tokenize""tokenize" (reversible) or "redact" (irreversible)
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_LLM_ALLOW_REMOTEfalseAllow non-localhost Ollama URLs
CLOAKLLM_SPACY_MODELen_core_web_smspaCy model (Python only)
CLOAKLLM_ENTITY_HASHINGfalseEnable per-entity HMAC-SHA256 hashing
CLOAKLLM_ENTITY_HASH_KEY(auto-generated)HMAC key for entity hashing
CLOAKLLM_CONTEXT_ANALYSISfalseEnable automatic context risk analysis
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);

Batch Processing

Sanitize multiple texts at once with a shared token map and a single audit entry. Same entities across texts get the same token.

Python

from cloakllm import Shield
 
shield = Shield()
 
texts = [
    "Email john@acme.com about the project",
    "Also notify jane@acme.com and john@acme.com",
]
sanitized_texts, token_map = shield.sanitize_batch(texts)
# sanitized_texts[0]: "Email [EMAIL_0] about the project"
# sanitized_texts[1]: "Also notify [EMAIL_1] and [EMAIL_0]"
# john@acme.com → [EMAIL_0] in both texts (shared token map)
 
# Desanitize batch
responses = ["Reply to [EMAIL_0]", "CC [EMAIL_1]"]
restored = shield.desanitize_batch(responses, token_map)

JavaScript

const { Shield } = require('cloakllm');
 
const shield = new Shield();
 
const [sanitizedTexts, tokenMap] = shield.sanitizeBatch([
  'Email john@acme.com about the project',
  'Also notify jane@acme.com and john@acme.com',
]);
 
// Desanitize batch
const restored = shield.desanitizeBatch(
  ['Reply to [EMAIL_0]', 'CC [EMAIL_1]'],
  tokenMap
);

MCP

Use the sanitize_batch tool:

// Tool call
{ "texts": ["Email john@acme.com", "SSN 123-45-6789"] }
 
// Response
{
  "sanitized": ["Email [EMAIL_0]", "SSN [SSN_0]"],
  "token_map_id": "a1b2c3d4-...",
  "entity_count": 2,
  "categories": { "EMAIL": 1, "SSN": 1 }
}

Key Behaviors

  • Shared token map: Same entity in different texts gets the same token
  • Single audit entry: One sanitize_batch event instead of N separate sanitize events
  • Per-text entity tracking: Each entity detail includes a text_index field indicating which text it came from
  • Reusable token map: Pass token_map / tokenMap from a previous call for multi-turn batch conversations

Performance Metrics

Track detection performance with per-pass timing breakdowns in audit logs and accumulated metrics via the metrics() API.

Per-Pass Timing in Audit Logs

Every audit entry includes a timing object with per-pass breakdowns:

{
  "timing": {
    "total_ms": 12.5,
    "detection_ms": 8.2,
    "regex_ms": 1.1,
    "ner_ms": 6.8,
    "llm_ms": 0.0,
    "tokenization_ms": 4.3
  }
}

Accumulated Metrics API

Use metrics() to get accumulated performance stats across all calls, and reset_metrics() / resetMetrics() to clear them.

Python

from cloakllm import Shield
 
shield = Shield()
 
# ... perform sanitize/desanitize calls ...
 
stats = shield.metrics()
# {
#   "calls": { "sanitize": 5, "desanitize": 3, "sanitize_batch": 1, "desanitize_batch": 0 },
#   "total_ms": 62.4,
#   "avg_ms": 6.9,
#   "detection": { "regex_ms": 5.5, "ner_ms": 34.0, "llm_ms": 0.0 },
#   "tokenization_ms": 22.9,
#   "entities_detected": 18,
#   "categories": { "EMAIL": 7, "PERSON": 5, "SSN": 3, "PHONE": 3 }
# }
 
shield.reset_metrics()  # Clear accumulated stats

JavaScript

const { Shield } = require('cloakllm');
 
const shield = new Shield();
 
// ... perform sanitize/desanitize calls ...
 
const stats = shield.metrics();
// {
//   calls: { sanitize: 5, desanitize: 3, sanitizeBatch: 1, desanitizeBatch: 0 },
//   total_ms: 45.2,
//   avg_ms: 5.0,
//   detection: { regex_ms: 5.5, llm_ms: 0.0 },
//   tokenization_ms: 39.7,
//   entities_detected: 18,
//   categories: { EMAIL: 7, SSN: 3, PHONE: 3 }
// }
 
shield.resetMetrics();  // Clear accumulated stats

Redaction Mode

Redaction mode provides irreversible PII removal — entities are replaced with [CATEGORY_REDACTED] placeholders instead of numbered tokens. No token map is stored, so the original values cannot be recovered. This is designed for GDPR right-to-erasure and scenarios where you must guarantee PII is permanently destroyed.

Python

from cloakllm import Shield, ShieldConfig
 
shield = Shield(ShieldConfig(mode="redact"))
 
redacted, token_map = shield.sanitize("Email john@acme.com about Sarah Johnson")
# redacted: "Email [EMAIL_REDACTED] about [PERSON_REDACTED]"
# token_map.entity_count == 0 (no forward mappings in redact mode)
 
# Desanitize is a no-op in redact mode — original values are gone
restored = shield.desanitize(redacted, token_map)
# restored == redacted (unchanged)

JavaScript

const { Shield, ShieldConfig } = require('cloakllm');
 
const shield = new Shield(new ShieldConfig({ mode: 'redact' }));
 
const [redacted, tokenMap] = shield.sanitize('Email john@acme.com about Sarah Johnson');
// redacted: "Email [EMAIL_REDACTED] about [PERSON_REDACTED]"
// tokenMap.entityCount == 0 (no forward mappings in redact mode)
 
// Desanitize is a no-op in redact mode
const restored = shield.desanitize(redacted, tokenMap);
// restored === redacted (unchanged)

MCP

Pass mode: "redact" to the sanitize tool. No token_map_id is returned in redact mode.

Key Behaviors

  • Token format: [CATEGORY_REDACTED] (e.g., [EMAIL_REDACTED], [PERSON_REDACTED])
  • Token map is empty — no bidirectional mappings stored
  • desanitize() returns the input unchanged (no-op)
  • Audit log entries include "mode": "redact" for traceability

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.


Custom LLM Categories

Define domain-specific PII types that the Ollama LLM pass should detect. This extends the built-in LLM categories (ADDRESS, MEDICAL, etc.) with your own semantic types.

Python

from cloakllm import Shield, ShieldConfig
 
config = ShieldConfig(
    llm_detection=True,
    custom_llm_categories=[
        ("PATIENT_ID", "Hospital patient ID, format PAT-XXXXX"),
        ("EMPLOYEE_NUMBER", "Internal employee number"),
    ],
)
shield = Shield(config=config)
 
sanitized, token_map = shield.sanitize("Patient PAT-12345 was seen by Dr. Smith")
# If LLM detects "PAT-12345" as PATIENT_ID → "[PATIENT_ID_0]"

JavaScript

const { Shield, ShieldConfig } = require('cloakllm');
 
const config = new ShieldConfig({
  llmDetection: true,
  customLlmCategories: [
    { name: 'PATIENT_ID', description: 'Hospital patient ID, format PAT-XXXXX' },
    { name: 'EMPLOYEE_NUMBER', description: 'Internal employee number' },
  ],
});
const shield = new Shield(config);
 
const [sanitized, tokenMap] = shield.sanitize('Patient PAT-12345 was seen by Dr. Smith');
// If LLM detects "PAT-12345" as PATIENT_ID → "[PATIENT_ID_0]"

MCP

Pass custom_llm_categories as a JSON string of [name, description] pairs:

// Tool call
{
  "text": "Patient PAT-12345 was seen by Dr. Smith",
  "custom_llm_categories": "[[\"PATIENT_ID\", \"Hospital patient ID\"]]"
}

Key Behaviors

BehaviorDetails
Name validationMust match ^[A-Z][A-Z0-9_]*$ (Python enforces at config time)
Excluded categoriesCategories handled by regex/NER (EMAIL, PHONE, SSN, etc.) are skipped with a warning
Description hintsDescriptions are injected into the Ollama system prompt to guide detection
Requires LLM detectionllm_detection / llmDetection must be enabled for custom categories to take effect

Multi-Language Detection

CloakLLM supports locale-specific PII detection for 13 non-US locales. Setting a locale activates country-specific regex patterns for SSNs, phone numbers, IBANs, tax IDs, and national IDs. In Python, it also auto-selects the appropriate spaCy NER model for that language.

Supported Locales

LocaleCountryExample Patterns
deGermanySteuer-IdNr, Personalausweis, DE phone, DE IBAN
frFranceNIR (INSEE), carte d'identite, FR phone, FR IBAN
esSpainDNI/NIE, ES phone, ES IBAN
itItalyCodice Fiscale, IT phone, IT IBAN
ptPortugalNIF, PT phone, PT IBAN
nlNetherlandsBSN, NL phone, NL IBAN
plPolandPESEL, NIP, PL phone, PL IBAN
seSwedenPersonnummer, SE phone, SE IBAN
noNorwayFodselsnummer, NO phone, NO IBAN
dkDenmarkCPR-nummer, DK phone, DK IBAN
fiFinlandHenkilotunnus, FI phone, FI IBAN
gbUnited KingdomNINO, GB phone, GB IBAN
auAustraliaTFN, AU phone

Python

from cloakllm import Shield, ShieldConfig
 
# German locale — activates DE-specific patterns and de_core_news_sm spaCy model
shield = Shield(ShieldConfig(locale="de"))
 
sanitized, token_map = shield.sanitize("Steuer-IdNr: 12345678901, Tel: +49 30 1234567")
# → "Steuer-IdNr: [SSN_0], Tel: [PHONE_0]"

JavaScript

const { Shield, ShieldConfig } = require('cloakllm');
 
// German locale — activates DE-specific patterns
const shield = new Shield(new ShieldConfig({ locale: 'de' }));
 
const [sanitized, tokenMap] = shield.sanitize('Steuer-IdNr: 12345678901, Tel: +49 30 1234567');
// → "Steuer-IdNr: [SSN_0], Tel: [PHONE_0]"

Key Behaviors

  • spaCy model auto-selection (Python only): Each locale maps to the appropriate spaCy language model (e.g., de uses de_core_news_sm, fr uses fr_core_news_sm). Install the model with python -m spacy download <model_name>.
  • Pattern replacement: Locale-specific patterns replace the default US-centric patterns for SSN, phone, and similar categories.
  • Composable: Locale patterns work alongside custom patterns, LLM detection, and entity hashing.
  • Default: When no locale is set (empty string), US patterns are used.

Entity Hashing

Per-entity HMAC-SHA256 hashing enables cross-request entity correlation without storing PII. Each detected entity gets a deterministic, keyed hash — the same entity always produces the same hash, allowing you to track "the same person appeared in 47 requests" without knowing who.

Python

from cloakllm import Shield, ShieldConfig
 
config = ShieldConfig(
    entity_hashing=True,
    entity_hash_key="my-secret-key-hex",  # optional — auto-generated if omitted
)
shield = Shield(config=config)
 
sanitized, token_map = shield.sanitize("Email john@acme.com about Sarah Johnson")
 
# entity_details now includes entity_hash
for detail in token_map.entity_details:
    print(detail["category"], detail["token"], detail["entity_hash"])
    # EMAIL  [EMAIL_0]  a3f2...  (64-char hex)
    # PERSON [PERSON_0] b7c1...

JavaScript

const { Shield, ShieldConfig } = require('cloakllm');
 
const config = new ShieldConfig({
  entityHashing: true,
  entityHashKey: 'my-secret-key-hex',  // optional — auto-generated if omitted
});
const shield = new Shield(config);
 
const [sanitized, tokenMap] = shield.sanitize('Email john@acme.com about Sarah Johnson');
 
// entityDetails now includes entity_hash
for (const detail of tokenMap.entityDetails) {
  console.log(detail.category, detail.token, detail.entity_hash);
}

MCP

Pass entity_hashing and optionally entity_hash_key to the sanitize tool:

// Tool call
{ "text": "Email john@acme.com", "entity_hashing": true, "entity_hash_key": "my-key" }
 
// Response — entity_details includes entity_hash
{
  "entity_details": [
    { "category": "EMAIL", "token": "[EMAIL_0]", "entity_hash": "a3f2..." }
  ]
}

How It Works

  • HMAC-SHA256: HMAC(key, "CATEGORY:normalized_text") — keyed hash prevents rainbow table attacks
  • Category prefix: EMAIL:john@acme.com and PERSON:john@acme.com produce different hashes, preventing cross-type collisions
  • Normalization: Input is lowercased and stripped of whitespace for consistency (John Smith and john smith produce the same hash)
  • Auto-key: If entity_hashing=True but no key is provided, a random 32-byte hex key is generated per Shield instance
  • Deterministic: Same entity + same key = same hash, across requests and SDK languages
  • Works everywhere: Compatible with tokenize mode, redact mode, sanitize_batch, and multi-turn conversations

Security Notes

  • The HMAC key is a deployment secret — never share it or log it
  • Entity hashes are one-way — you cannot recover the original PII from a hash
  • Use a consistent key across requests to enable correlation; rotate the key to break correlation

Incremental Streaming

When using streaming LLM responses, CloakLLM desanitizes tokens incrementally as chunks arrive — no buffering of the full response. The StreamDesanitizer state machine replaces [CATEGORY_N] tokens as soon as the closing ] arrives, passing plain text through immediately.

All middleware integrations (OpenAI SDK, LiteLLM, Vercel AI SDK) use StreamDesanitizer automatically. You only need the standalone API if you're building a custom streaming pipeline.

Python

from cloakllm import Shield, StreamDesanitizer
 
shield = Shield()
sanitized, token_map = shield.sanitize("Email john@acme.com about Sarah Johnson")
 
# Simulate streaming chunks from an LLM
chunks = ["Hi ", "[PER", "SON_0]", ", your email is ", "[EMAIL_0]", "."]
 
desan = StreamDesanitizer(token_map)
for chunk in chunks:
    output = desan.feed(chunk)
    if output:
        print(output, end="")  # prints incrementally
# Flush any remaining buffer at end of stream
remaining = desan.flush()
if remaining:
    print(remaining, end="")

JavaScript

const { Shield, StreamDesanitizer } = require('cloakllm');
 
const shield = new Shield();
const [sanitized, tokenMap] = shield.sanitize('Email john@acme.com about Sarah Johnson');
 
// Simulate streaming chunks from an LLM
const chunks = ['Hi ', '[PER', 'SON_0]', ', your email is ', '[EMAIL_0]', '.'];
 
const desan = new StreamDesanitizer(tokenMap);
for (const chunk of chunks) {
  const output = desan.feed(chunk);
  if (output) process.stdout.write(output);
}
const remaining = desan.flush();
if (remaining) process.stdout.write(remaining);

How It Works

  • Plain text passes through feed() immediately — no latency added
  • [ bracket triggers internal buffering of a potential token
  • ] bracket resolves the buffer against the token map (case-insensitive) and emits the original value, or the literal text if not a known token
  • Buffer overflow — if the buffer exceeds 40 characters without a ], it flushes incrementally to prevent unbounded memory use
  • flush() — call at end-of-stream to emit any remaining buffered text

Middleware Integration

All middleware paths use StreamDesanitizer internally:

MiddlewareStreaming Support
Python OpenAI SDK (enable_openai)Incremental desanitization
Python LiteLLM (cloakllm.enable)Incremental desanitization
JS OpenAI SDK (enable)Incremental desanitization
JS Vercel AI SDK (createCloakLLMMiddleware)Incremental desanitization

No configuration needed — streaming desanitization is automatic when stream: true / stream=True is used.


Cryptographic Attestation

Ed25519 digital signatures prove that sanitization occurred. Each sanitize() call produces a signed certificate containing input/output hashes, entity count, categories, and detection passes. Batch operations use Merkle trees for efficient multi-text proofs.

Setup

# Python — generate and save a signing key
from cloakllm import Shield, ShieldConfig, DeploymentKeyPair
 
keypair = DeploymentKeyPair.generate()
keypair.save("./keys/signing_key.json")
 
shield = Shield(ShieldConfig(attestation_key=keypair))
// JavaScript
const { Shield, ShieldConfig, DeploymentKeyPair } = require('cloakllm');
 
const keypair = DeploymentKeyPair.generate();
keypair.save('./keys/signing_key.json');
 
const shield = new Shield(new ShieldConfig({ attestationKey: keypair }));

Or load from file / environment variable:

shield = Shield(ShieldConfig(attestation_key_path="./keys/signing_key.json"))
# Or: export CLOAKLLM_SIGNING_KEY_PATH=./keys/signing_key.json

Using Certificates

sanitized, token_map = shield.sanitize("Email john@acme.com")
cert = token_map.certificate
 
# Certificate fields: version, timestamp, nonce, input_hash, output_hash,
# entity_count, categories, detection_passes, mode, key_id, signature
# The nonce field contains a random value for replay prevention
 
# Verify the certificate
assert cert.verify(keypair.public_key)
assert shield.verify_certificate(cert)
 
# Serialize for storage or transmission
cert_dict = cert.to_dict()

Batch Attestation with Merkle Trees

texts = ["Email john@acme.com", "SSN 123-45-6789", "Call 555-0100"]
sanitized_texts, token_map = shield.sanitize_batch(texts)
 
# Batch certificate uses Merkle roots instead of individual hashes
cert = token_map.certificate
merkle_tree = token_map.merkle_tree
 
# Verify individual text inclusion in the batch
from cloakllm import MerkleTree
import hashlib
 
leaf = hashlib.sha256(texts[0].encode()).hexdigest()
proof = merkle_tree["input"].proof(0)
assert MerkleTree.verify_proof(leaf, proof, merkle_tree["input"].root)

Cross-Language Compatibility

Certificates are fully cross-language compatible. A certificate signed in Python verifies in JavaScript and vice versa, using identical canonical JSON serialization and Ed25519 signatures.

Configuration

OptionPythonJavaScriptDefault
Signing keypairattestation_keyattestationKeyNone
Key file pathattestation_key_pathattestationKeyPathNone
Environment variableCLOAKLLM_SIGNING_KEY_PATHCLOAKLLM_SIGNING_KEY_PATH

Python Optional Dependencies

pip install cloakllm[attestation]  # installs pynacl
# or: pip install cryptography     # also works

JavaScript uses Node.js built-in crypto module — no extra dependencies.


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
Custom LLM(your categories)Ollama LLM (via custom_llm_categories)

CLI

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

Python

# Scan text for PII (PII values redacted by default)
python -m cloakllm scan "Send email to john@acme.com, SSN 123-45-6789"
 
# Show original PII values in output
python -m cloakllm scan --show-pii "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", "sanitize_batch", "desanitize_batch", "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)
mode"tokenize" or "redact"
entity_detailsPer-entity metadata array (PII-safe: category, offsets, confidence, source, token, and entity_hash when hashing is enabled)
timingPer-pass breakdown: total_ms, detection_ms, regex_ms, ner_ms, llm_ms, tokenization_ms
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 — returns (valid, errors, final_seq)
# final_seq is the last sequence number, useful for truncation detection
is_valid, errors, final_seq = shield.verify_audit()
 
# Statistics
stats = shield.audit_stats()

JavaScript:

const shield = new Shield();
 
// Programmatic verification — returns { valid, errors, finalSeq }
// finalSeq is the last sequence number, useful for truncation detection
const { valid, errors, finalSeq } = 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. The returned final_seq / finalSeq value indicates the last sequence number seen, which can be compared against expected counts to detect log truncation.


Security

Ollama SSRF Prevention

By default, the Ollama LLM detection pass only connects to localhost URLs. This prevents server-side request forgery (SSRF) if an attacker controls the llm_ollama_url / llmOllamaUrl configuration. To allow connections to remote Ollama servers, explicitly opt in:

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

Or via environment variable:

export CLOAKLLM_LLM_ALLOW_REMOTE=true

CLI PII Redaction

The CLI scan command redacts detected PII values by default. To display original values in the output, use the --show-pii flag:

# Default — PII values are redacted in output
python -m cloakllm scan "Email john@acme.com"
# → [EMAIL] "j***@***.com"
 
# Show original PII values
python -m cloakllm scan --show-pii "Email john@acme.com"
# → [EMAIL] "john@acme.com"

Thread Safety

CloakLLM is designed for concurrent use:

  • TokenMap: Thread-safe. Multiple threads can read/write tokens concurrently.
  • AuditLogger: Thread-safe. Concurrent sanitize calls produce correctly ordered, hash-chained audit entries.
  • LLM cache: Thread-safe. The Ollama detection cache handles concurrent access without corruption.

Redacted Analysis

The analyze() method supports redacting PII values in its output:

# Python — redact values in analysis output
analysis = shield.analyze("Email john@acme.com", redact_values=True)
# entities[0]["text"] → "[REDACTED]" instead of "john@acme.com"
// JavaScript — redact values in analysis output
const analysis = shield.analyze('Email john@acme.com', { redactValues: true });
// entities[0].text → "[REDACTED]" instead of "john@acme.com"

Context Risk Analysis

Even after tokenization, surrounding context can reveal identity. CloakLLM's ContextAnalyzer scores this re-identification risk.

Standalone Analysis

Python:

from cloakllm import Shield
 
shield = Shield()
sanitized, _ = shield.sanitize("The CEO of Acme Corp works at their office")
 
risk = shield.analyze_context_risk(sanitized)
print(risk)
# {'token_density': 0.375, 'identifying_descriptors': 1, 'relationship_edges': 1,
#  'risk_score': 0.513, 'risk_level': 'medium', 'warnings': [...]}

JavaScript:

const { Shield } = require('cloakllm');
 
const shield = new Shield();
const [sanitized] = shield.sanitize('The CEO of Acme Corp works at their office');
 
const risk = shield.analyzeContextRisk(sanitized);
console.log(risk);
// { token_density: 0.375, identifying_descriptors: 1, relationship_edges: 1,
//   risk_score: 0.513, risk_level: 'medium', warnings: [...] }

Automatic Analysis

Enable context_analysis to automatically analyze every sanitize() call:

Python:

shield = Shield(ShieldConfig(context_analysis=True, context_risk_threshold=0.5))
sanitized, token_map = shield.sanitize("The CEO of Acme Corp lives in New York")
 
print(token_map.risk_assessment)
# {'risk_score': 0.65, 'risk_level': 'medium', ...}
# Warning logged if risk_score > context_risk_threshold

JavaScript:

const shield = new Shield(new ShieldConfig({
  contextAnalysis: true,
  contextRiskThreshold: 0.5,
}));
const [sanitized, tokenMap] = shield.sanitize('The CEO of Acme Corp lives in New York');
 
console.log(tokenMap.riskAssessment);
// { risk_score: 0.65, risk_level: 'medium', ... }

Three Signals

SignalDescriptionWeight
Token densityRatio of tokens to total wordsx1.5
Identifying descriptorsWords like "CEO", "founder", "only" near tokensx0.15 each
Relationship edgesPhrases like "works at", "lives in" connecting two tokensx0.20 each

Risk levels: low (0–0.3), medium (0.3–0.7), high (above 0.7). Score capped at 1.0.

CLI

# Python
python -m cloakllm scan "The CEO of Acme Corp works in NYC" --context-risk
 
# JavaScript
npx cloakllm scan "The CEO of Acme Corp works in NYC" --context-risk

Token Specification

CloakLLM v0.5.1 introduces a formal token standard. The full spec is in TOKEN_SPEC.md.

Token Format

All tokens follow the grammar [CATEGORY_N] in tokenize mode or [CATEGORY_REDACTED] in redact mode:

  • Category: uppercase letters, digits, and underscores (e.g., EMAIL, CREDIT_CARD, DATE_OF_BIRTH)
  • Suffix: zero-based counter (e.g., 0, 1, 42) or REDACTED
  • Maximum token length: 40 characters (including brackets)

Validation Utilities

Both SDKs export functions to validate and parse tokens:

Python:

from cloakllm import validate_token, parse_token, is_redacted_token, validate_category_name
from cloakllm import BUILTIN_CATEGORIES, MAX_TOKEN_LENGTH
 
validate_token("[EMAIL_0]")          # True
validate_token("[email_0]")          # False (lowercase)
parse_token("[PERSON_3]")           # ("PERSON", "3")
parse_token("[SSN_REDACTED]")       # ("SSN", "REDACTED")
is_redacted_token("[SSN_REDACTED]") # True
validate_category_name("MY_TYPE")   # True
len(BUILTIN_CATEGORIES)             # 62 built-in categories

JavaScript:

const {
  validateToken, parseToken, isRedactedToken, validateCategoryName,
  BUILTIN_CATEGORIES, MAX_TOKEN_LENGTH,
} = require('cloakllm');
 
validateToken('[EMAIL_0]');          // true
validateToken('[email_0]');          // false
parseToken('[PERSON_3]');           // { category: 'PERSON', suffix: '3' }
isRedactedToken('[SSN_REDACTED]');  // true
validateCategoryName('MY_TYPE');    // true
BUILTIN_CATEGORIES.size;            // 62

Custom Category Names

Custom categories (via custom_patterns or custom_llm_categories) must:

  • Match the pattern ^[A-Z][A-Z0-9_]*$
  • Not collide with any of the 62 built-in category names

Both SDKs enforce these rules at config creation time.


Pluggable Detection Backends

v0.5.2 introduces a DetectorBackend base class that lets you replace or extend the default detection pipeline. The built-in pipeline runs regex → NER → LLM (opt-in). With pluggable backends, you can swap any stage, add custom detectors, or build an entirely custom pipeline.

Writing a Custom Backend

Python:

from cloakllm import DetectorBackend, Shield
 
class ProfanityBackend(DetectorBackend):
    @property
    def name(self):
        return "profanity"
 
    def detect(self, text, covered_spans):
        from cloakllm.detector import Detection
        detections = []
        bad_words = {"badword1", "badword2"}
        for word in bad_words:
            idx = text.lower().find(word)
            if idx != -1:
                span = (idx, idx + len(word))
                if not any(s <= idx and idx + len(word) <= e for s, e in covered_spans):
                    detections.append(Detection("PROFANITY", idx, idx + len(word), word, 1.0, "profanity"))
                    covered_spans.append(span)
        return detections

JavaScript:

const { DetectorBackend, Shield } = require('cloakllm');
 
class ProfanityBackend extends DetectorBackend {
  get name() { return 'profanity'; }
 
  detect(text, coveredSpans) {
    const detections = [];
    const badWords = ['badword1', 'badword2'];
    for (const word of badWords) {
      const idx = text.toLowerCase().indexOf(word);
      if (idx !== -1) {
        const end = idx + word.length;
        const overlaps = coveredSpans.some(([s, e]) => s <= idx && end <= e);
        if (!overlaps) {
          detections.push({ category: 'PROFANITY', start: idx, end, text: word, confidence: 1.0, source: 'profanity' });
          coveredSpans.push([idx, end]);
        }
      }
    }
    return detections;
  }
}

Using Custom Backends

Pass a backends array to Shield to replace the default pipeline:

Python:

from cloakllm import Shield, RegexBackend, ShieldConfig
 
config = ShieldConfig()
profanity = ProfanityBackend()
regex = RegexBackend(config)
 
# Custom pipeline: regex first, then profanity
shield = Shield(config, backends=[regex, profanity])
sanitized, token_map = shield.sanitize("Email john@acme.com, badword1 detected")

JavaScript:

const { Shield, ShieldConfig, RegexBackend } = require('cloakllm');
 
const config = new ShieldConfig();
const profanity = new ProfanityBackend();
const regex = new RegexBackend(config);
 
const shield = new Shield(config, { backends: [regex, profanity] });
const [sanitized, tokenMap] = shield.sanitize('Email john@acme.com, badword1 detected');

Built-In Backends

Both SDKs export three built-in backend classes:

BackendNameDescription
RegexBackend"regex"Pattern matching for structured PII (emails, SSNs, etc.)
NerBackend"ner"Named entity recognition (spaCy in Python, compromise in JS)
LlmBackend"llm"Ollama-based semantic detection for contextual PII

When no backends parameter is provided, Shield builds the default pipeline automatically (regex → NER → LLM if enabled).

Dynamic Metrics

When custom backends are used, timing keys in shield.metrics() and audit log entries are derived from each backend's name property (e.g., profanity_ms), instead of the hardcoded regex_ms/ner_ms/llm_ms.


Article 12 Compliance Mode

CloakLLM v0.6.0 introduces a formal EU AI Act Article 12 compliance profile. Activating it adds tamper-detectable compliance metadata to every audit entry, enables a runtime guard against PII leakage in logs, and unlocks structured compliance reporting for auditors.

For the regulatory rationale, see The Article 12 Paradox whitepaper.

Activation

Python:

from cloakllm import Shield, ShieldConfig
 
shield = Shield(ShieldConfig(
    compliance_mode="eu_ai_act_article12",
    retention_hint_days=180,  # default; Article 12 minimum for deployers
))

JavaScript:

const { Shield, ShieldConfig } = require('cloakllm');
 
const shield = new Shield(new ShieldConfig({
  complianceMode: 'eu_ai_act_article12',
  retentionHintDays: 180,
}));

When activated, every audit entry gains four fields, all included in the SHA-256 hash chain:

FieldValuePurpose
compliance_version"eu_ai_act_article12_v1"Schema version for regulator-facing tooling
article_ref["EU_AI_Act_Art_12", "EU_AI_Act_Art_19"]Articles satisfied by this entry
retention_hint_days180 (default)Recommended retention for downstream log-rotation systems
pii_in_logfalseAsserted at runtime — never true

compliance_summary()

Returns a structured map of EU AI Act and GDPR articles addressed by the current configuration.

Python: shield.compliance_summary() JavaScript: shield.complianceSummary()

export_compliance_config()

Writes the compliance summary to a JSON file. This is the artifact you hand to an auditor.

Python:

shield.export_compliance_config("./compliance_snapshot.json")

JavaScript:

shield.exportComplianceConfig('./compliance_snapshot.json');

verify_audit() compliance report

Returns a structured compliance report with a verdict of "COMPLIANT" or "NON_COMPLIANT".

Python:

report = shield.verify_audit(output_format="compliance_report")

JavaScript:

const report = shield.verifyAudit({ outputFormat: 'compliance_report' });

CLI

cloakllm verify ./cloakllm_audit/ --format compliance_report

Emits the report as JSON to stdout. Exit code 0 for COMPLIANT, 1 for NON_COMPLIANT.

The PII guard

In compliance mode, every audit entry passes through a runtime guard before being hashed. Any field of entity_details containing forbidden keys (original_value, original_text, raw_text, plain_text, value) causes the write to be rejected. This is the structural enforcement of CloakLLM's core invariant: audit logs contain zero original PII.


Enterprise Key Management

⚠ EXPERIMENTAL — disabled in v0.6.1. The KMS providers shipped in v0.6.0 had bugs that produced unverifiable signatures. They now raise NotImplementedError. Use LocalKeyProvider (the default) for production attestation. Full rebuild planned for v0.7.0.

The scaffolding for HSM-backed signing keys is in place but not production-usable in v0.6.1.

pip install cloakllm[kms]

Supported providers

ProviderConfig value
AWS KMSattestation_key_provider="aws_kms"
GCP KMSattestation_key_provider="gcp_kms"
Azure Key Vaultattestation_key_provider="azure_keyvault"
HashiCorp Vaultattestation_key_provider="hashicorp_vault"

Usage

from cloakllm import Shield, ShieldConfig
 
shield = Shield(ShieldConfig(
    attestation_key_provider="aws_kms",
    attestation_key_id="arn:aws:kms:eu-west-1:123:key/abc-...",
    key_rotation_enabled=True,
))

When key_rotation_enabled=True, a key_rotation_event audit entry is logged at session init recording key_id, key_provider, and key_version. No PII is included.


Disabling / Re-enabling

Python (OpenAI SDK)

from cloakllm import enable_openai, disable_openai
from openai import OpenAI
 
client = OpenAI()
 
enable_openai(client)   # Start protecting
disable_openai(client)  # Stop — restore original client behavior
enable_openai(client)   # Re-enable at any time

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

On this page

Table of ContentsInstallationPythonJavaScriptMCP ServerQuick StartPython — OpenAI SDKPython — LiteLLMPython — Standalone ShieldJavaScript — OpenAI SDKJavaScript — Vercel AI SDKJavaScript — Standalone ShieldMCP — Claude DesktopHow It WorksDefault 3-Pass DetectionTokenizationAudit LogsConfiguration ReferencePython ShieldConfigJavaScript ShieldConfigEnvironment VariablesMulti-Turn ConversationsPythonJavaScriptBatch ProcessingPythonJavaScriptMCPKey BehaviorsPerformance MetricsPer-Pass Timing in Audit LogsAccumulated Metrics APIPythonJavaScriptRedaction ModePythonJavaScriptMCPKey BehaviorsCustom PatternsPythonJavaScriptLLM-Powered Detection (Ollama)EnablingWhat It CatchesConfigurationCustom LLM CategoriesPythonJavaScriptMCPKey BehaviorsMulti-Language DetectionSupported LocalesPythonJavaScriptKey BehaviorsEntity HashingPythonJavaScriptMCPHow It WorksSecurity NotesIncremental StreamingPythonJavaScriptHow It WorksMiddleware IntegrationCryptographic AttestationSetupUsing CertificatesBatch Attestation with Merkle TreesCross-Language CompatibilityConfigurationPython Optional DependenciesEntity Detection ReferenceCLIPythonJavaScriptExample OutputAudit LogsFile FormatEntry StructureVerificationTamper DetectionSecurityOllama SSRF PreventionCLI PII RedactionThread SafetyRedacted AnalysisContext Risk AnalysisStandalone AnalysisAutomatic AnalysisThree SignalsCLIToken SpecificationToken FormatValidation UtilitiesCustom Category NamesPluggable Detection BackendsWriting a Custom BackendUsing Custom BackendsBuilt-In BackendsDynamic MetricsArticle 12 Compliance ModeActivationcompliance_summary()export_compliance_config()verify_audit() compliance reportCLIThe PII guardEnterprise Key ManagementSupported providersUsageDisabling / Re-enablingPython (OpenAI SDK)Python (LiteLLM)JavaScript (OpenAI SDK)