FairqueryBlogToolsAboutSecurityllms.txt

How do I redact PII and secrets before sending data to an LLM?

Fairquery ·

Short answer: run every prompt, document or log through a detector that finds PII (emails, phones, card numbers, SSNs, IBANs, IPs) and secrets (API keys, tokens, private keys), replace each hit with a placeholder like [EMAIL_1], send only the redacted text to the LLM, and put the real values back into the answer afterwards if you need them. You can do this for free on your own machine with the pii_scan_text and pii_redact_text tools in the @fairquery/agent-trust-mcp MCP server (text never leaves your machine), or at batch scale with the LLM Data Guard Apify Actor for $0.0015 per document + $0.0002 per finding, which adds name/address detection, URL and PDF input, and a restore mode.

Why redact before the LLM call

Once text is in a prompt it can end up in provider logs, traces, evaluation datasets, vector stores and, through retrieval, in other users' answers. OWASP lists this as LLM02: Sensitive Information Disclosure in its Top 10 for LLM applications. Redacting at the input side is the cheapest control: the model usually doesn't need the real email address or card number to summarise a support ticket, classify a log line or draft a reply.

What gets detected

CategoryTypesHow
PIIEMAIL, PHONE, SSN, CREDIT_CARD, IBAN, UK_NINO, IP_ADDRESSLuhn + issuer prefix for cards, mod-97 + country length for IBANs, libphonenumber for phones, SSN area/group rules
SecretsPRIVATE_KEY, AWS_ACCESS_KEY, AWS_SECRET_KEY, GITHUB_TOKEN, GITLAB_TOKEN, SLACK_TOKEN, SLACK_WEBHOOK, STRIPE_KEY, GOOGLE_API_KEY, OPENAI_KEY, ANTHROPIC_KEY, HUGGINGFACE_TOKEN, NPM_TOKEN, SENDGRID_KEY, TWILIO_KEY, AZURE_STORAGE_KEY, JWT, URL_CREDENTIALS, GENERIC_SECRETGitleaks-style rules; GENERIC_SECRET (api_key=..., password=...) uses a Shannon-entropy threshold
Opt-inHIGH_ENTROPY_STRING (local + Actor); PERSON, LOCATION, STREET_ADDRESS (Actor only)Entropy scan; open-source NER (compromise, English) and street-address patterns

Overlaps are resolved (secrets > cards/IBAN > SSN > email > IP > phone > address > names), so a card number is never also reported as a phone.

Option 1 (free): redact locally with the Agent Trust MCP server

Add the server to Claude Desktop (claude_desktop_config.json), Cursor (~/.cursor/mcp.json) or any MCP client. No token is needed for the free tools:

{
  "mcpServers": {
    "fairquery-trust": {
      "command": "npx",
      "args": ["-y", "@fairquery/agent-trust-mcp"]
    }
  }
}

Then ask the agent to "redact this ticket before summarising it", or call the tools from your own code with the MCP SDK. This is the script we ran (Node.js 20+, npm i @modelcontextprotocol/sdk):

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

const transport = new StdioClientTransport({ command: 'npx', args: ['-y', '@fairquery/agent-trust-mcp'] });
const client = new Client({ name: 'redactor', version: '1.0.0' });
await client.connect(transport);

const ticket = 'Ticket #4411 from Jane Doe <jane.doe@example.com>, phone +1 415-555-0132. ' +
  'Card 4111 1111 1111 1111 was declined. Debug log: AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE';

const res = await client.callTool({
  name: 'pii_redact_text',
  arguments: { texts: [ticket], returnPlaceholderMap: true },
});
console.log(res.content[0].text);   // JSON: meta.placeholder_map + items[].output_text
await client.close();

(The card number is the standard Visa test number and the AWS key is the example key from AWS's documentation.)

Real output: pii_redact_text (placeholder style)

{
  "meta": {
    "engine": "local",
    "redaction_style": "placeholder",
    "placeholder_map": {
      "[EMAIL_1]": "jane.doe@example.com",
      "[PHONE_1]": "+1 415-555-0132",
      "[CREDIT_CARD_1]": "4111 1111 1111 1111",
      "[AWS_ACCESS_KEY_1]": "AKIAIOSFODNN7EXAMPLE"
    }
  },
  "items": [
    {
      "type": "redacted_document",
      "finding_count": 4,
      "risk_level": "critical",
      "output_text": "Ticket #4411 from Jane Doe <[EMAIL_1]>, phone [PHONE_1]. Card [CREDIT_CARD_1] was declined. Debug log: AWS_ACCESS_KEY_ID=[AWS_ACCESS_KEY_1]"
    }
  ]
}

Send output_text to the model; keep placeholder_map on your side (it contains the originals) to swap the real values back into the answer. With redactionStyle: "mask" the same input became:

Ticket #4411 from Jane Doe <j***@e***.com>, phone +* ***-***-0132. Card **** **** **** 1111 was declined. Debug log: AWS_ACCESS_KEY_ID=AKIA****************

Real output: pii_scan_text (detect only)

Use the scan tool when you only want to know whether text is safe to send, for example to block a prompt or route it for review. Findings carry offsets and a masked preview, never the raw value:

{
  "count": 4,
  "total": 4,
  "offset": 0,
  "hasMore": false,
  "nextOffset": null,
  "meta": {
    "documents": [
      {
        "doc_index": 0,
        "chars": 162,
        "finding_count": 4,
        "counts_by_type": {
          "EMAIL": 1,
          "PHONE": 1,
          "CREDIT_CARD": 1,
          "AWS_ACCESS_KEY": 1
        },
        "risk_level": "critical"
      }
    ],
    "engine": "local"
  },
  "items": [
    {
      "type": "EMAIL",
      "doc_index": 0,
      "category": "pii",
      "rule": "email",
      "start": 28,
      "end": 48,
      "confidence": "high",
      "masked": "j***@e***.com"
    },
    {
      "type": "PHONE",
      "doc_index": 0,
      "category": "pii",
      "rule": "libphonenumber",
      "start": 57,
      "end": 72,
      "confidence": "high",
      "masked": "+* ***-***-0132"
    },
    {
      "type": "CREDIT_CARD",
      "doc_index": 0,
      "category": "pii",
      "rule": "card-luhn",
      "start": 79,
      "end": 98,
      "confidence": "high",
      "masked": "**** **** **** 1111"
    },
    {
      "type": "AWS_ACCESS_KEY",
      "doc_index": 0,
      "category": "secret",
      "rule": "aws-access-key-id",
      "start": 142,
      "end": 162,
      "confidence": "high",
      "masked": "AKIA****************"
    }
  ]
}

In the local tools, risk_level is critical if any secret is found, high for card/IBAN/SSN/NI numbers, medium for other PII and none otherwise. In our test the scan returned in 14 ms and the redaction in 5 ms (server already running). Limits per call: up to 50 texts, 200,000 characters each, 500,000 in total.

Option 2 (API, batch): LLM Data Guard Actor

Use the Actor when you need to process many documents, fetch public URLs or PDFs (text layer, no OCR, up to 10 MB each), detect person names and street addresses, use consistent placeholders across a whole dataset (for RAG or fine-tuning), or restore values server-side.

curl -X POST \
  "https://api.apify.com/v2/acts/fairquery~llm-data-guard-pii-secrets/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "texts": ["Ticket #4411 from Jane Doe <jane.doe@example.com>, phone +1 415-555-0132. Card 4111 1111 1111 1111 was declined. Debug log: AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE"],
    "mode": "redact",
    "detectNames": true
  }'

Real output from that request

Abridged (first two of five findings shown). With detectNames on, the Actor also caught the name Jane Doe as a low-confidence PERSON:

{
  "type": "document",
  "id": "text-1",
  "chars": 162,
  "mode": "redact",
  "finding_count": 5,
  "counts_by_type": {
    "PERSON": 1,
    "EMAIL": 1,
    "PHONE": 1,
    "CREDIT_CARD": 1,
    "AWS_ACCESS_KEY": 1
  },
  "risk_level": "high",
  "findings": [
    {
      "type": "PERSON",
      "category": "pii",
      "rule": "ner-compromise",
      "confidence": "low",
      "start": 18,
      "end": 26,
      "length": 8,
      "preview": "**** ***",
      "replacement": "[PERSON_1]"
    },
    {
      "type": "EMAIL",
      "category": "pii",
      "rule": "email",
      "confidence": "high",
      "start": 28,
      "end": 48,
      "length": 20,
      "preview": "j***@e***.com",
      "replacement": "[EMAIL_1]"
    },
    "… 3 more (PHONE, CREDIT_CARD, AWS_ACCESS_KEY)"
  ],
  "output_text": "Ticket #4411 from [PERSON_1] <[EMAIL_1]>, phone [PHONE_1]. Card [CREDIT_CARD_1] was declined. Debug log: AWS_ACCESS_KEY_ID=[AWS_ACCESS_KEY_1]"
}

That run took 2.5 seconds end to end on Apify (mostly container start) and would cost $0.0015 + 5 × $0.0002 = $0.0025. The README documents roughly 1–5 MB of text per second for the regex detectors; NER is slower. In the Actor, risk_level is high for any secret or card/SSN/IBAN/NI number, medium for other confident PII and low for low-confidence hits only.

Restore after the model answers

{ "mode": "restore",
  "texts": ["Dear [PERSON_1], we emailed [EMAIL_1] about the declined card."],
  "placeholderMap": { "[PERSON_1]": "Jane Doe", "[EMAIL_1]": "jane.doe@example.com" } }

Get the map by setting returnPlaceholderMap: true in redact mode. Treat it like the original data, because it is.

Data handling

The Actor is designed not to log document content. It writes only result rows and a counts-only RUN_SUMMARY, deletes the run's INPUT record at the end by default (purgeInputAfterRun), and omits raw values from findings unless you set includeFindingValues. The free local tools are designed to make no network calls.

Pricing

OptionPrice
pii_scan_text, pii_redact_text, pii_list_detectors (local)Free, no account
LLM Data Guard Actor / pii_guard_documents MCP tool$0.0015 per document + $0.0002 per finding (+ Apify's $0.00005 Actor-start event); error rows never charged

How it compares with alternatives

OptionGood forTrade-offs
Microsoft Presidio (open source)Self-hosted PII detection and anonymisation with NER models and custom recognizers, many languages via spaCy/transformers modelsYou run and tune it yourself (Python services, models); secrets/API-key detection is not its focus
Cloud DLP / PII APIs (Google Cloud Sensitive Data Protection, Amazon Comprehend, Azure AI Language)Broad, managed detection with many info types and languagesThe text is sent to that cloud provider; pricing and setup vary by provider
Secret scanners (gitleaks, TruffleHog)Finding leaked credentials in repositories and git historyBuilt for code and CI, not for redacting PII in prompts
Fairquery (local MCP tools + Actor)PII and secrets in one pass, reversible placeholders, free local mode, MCP-native for agentsPattern-based: English-only NER, no OCR, unusual formats can slip through

FAQ

How do I remove personal data from a prompt before sending it to ChatGPT, Claude or another LLM?

Run the text through a PII and secret detector, replace each hit with a placeholder such as [EMAIL_1], send the redacted text, and restore the originals in the answer if needed. Fairquery's free pii_redact_text tool in @fairquery/agent-trust-mcp does this locally; the LLM Data Guard Actor does it at batch scale via API.

Is there a free way to redact PII locally?

Yes. The pii_scan_text and pii_redact_text tools in the @fairquery/agent-trust-mcp MCP server are free, need no account and make no network calls. Limits: 50 texts per call, 200,000 characters each, 500,000 in total.

Which secrets can it detect?

Private keys, AWS access and secret keys, GitHub, GitLab, Slack, Stripe, Google, OpenAI, Anthropic, Hugging Face, npm, SendGrid, Twilio and Azure storage keys, JWTs, credentials in URLs, and generic api_key=/password= assignments above an entropy threshold.

Can I put the original values back after the LLM answers?

Yes. Placeholder redaction is reversible: request the placeholder map (returnPlaceholderMap) and swap values back yourself, or call the Actor with mode 'restore' and the map.

How much does the LLM Data Guard API cost?

$0.0015 per document plus $0.0002 per finding on Apify (pay per event); error rows are never charged. A document with 5 findings costs $0.0025.

Does it detect names and addresses?

The Actor can, opt-in (detectNames, detectAddresses), using English open-source NER and street-address patterns; these hits are marked low confidence. The free local tools do not detect names or addresses.

Honest limits: pattern-based detection finds most but not all sensitive data. Non-English names, unusual formats, images and scanned PDFs can slip through, and some matches are false positives. Use it as one layer of a data-protection program, not as a compliance certification, and only process data you are allowed to process.

Source and docs: github.com/fairquery/llm-data-guard-pii-secrets · Apify Store · Agent Trust MCP on GitHub · npm · all Fairquery tools