PII Masking

Captain can strip personally identifiable information (PII) from content at index time. Set mask_pii: true on any indexing request and Captain detects and masks PII in the parsed text, the layout regions, and the pixels of images, before anything is embedded or written to the search index.

It is opt-in and per-job: the flag defaults to false, so existing indexing is unchanged, and only the files you index with mask_pii: true are masked.

What gets masked

Masking covers three layers, so PII cannot leak through any of them:

  • Text: the parsed content of documents, media transcripts, and the text Captain reads out of images. Detected PII is replaced with an entity tag, for example:

    Contact Jane Doe at jane.doe@example.com, SSN 123-45-6789, phone (415) 555-0142.

    becomes

    Contact <PERSON> at <EMAIL_ADDRESS>, SSN <US_SSN>, phone <PHONE_NUMBER>.
  • Images: for standalone image files and images embedded in PDFs, PII text that is visible in the pixels is covered with solid boxes (shown above). This applies to both the stored, retrievable image and the visual embedding.

  • Regions: when you query with include.regions, the per-region text is masked as well, so the layout view carries no raw PII either.

What Captain detects

Captain masks a curated set of high-value identifiers. The same detection rules run on text and on the text Captain reads out of image pixels. Each category is listed by the tag that replaces it in masked output.

Identity

CategoryWhat it matchesMasked in textMasked in images
<PERSON>Names of people, detected by a named-entity model rather than a fixed list.
<LOCATION>Place names such as cities, states, countries, and addresses.

Contact

CategoryWhat it matchesMasked in textMasked in images
<EMAIL_ADDRESS>Email addresses.
<PHONE_NUMBER>Phone numbers in common national and international formats.

Financial

CategoryWhat it matchesMasked in textMasked in images
<CREDIT_CARD>Credit and debit card numbers from the major networks.
<IBAN_CODE>International Bank Account Numbers (IBAN).
<US_BANK_NUMBER>US bank account numbers.
<CRYPTO>Cryptocurrency wallet addresses.

Government and professional IDs

CategoryWhat it matchesMasked in textMasked in images
<US_SSN>US Social Security numbers in any common format (dashed, spaced, or unseparated).
<US_PASSPORT>US passport numbers.
<US_DRIVER_LICENSE>US driver’s license numbers.
<US_ITIN>US Individual Taxpayer Identification Numbers.
<MEDICAL_LICENSE>Medical license numbers.

Compliance

CategoryWhat it matchesMasked in textMasked in images
<US_NMLS_ID>NMLS unique identifiers with their label, in every common rendering: NMLS #123456, NMLSR ID, the Loan Estimate’s NMLS/__ LICENSE ID, and Closing Disclosure table rows with multiple IDs per row.
<US_SURETY_BOND_NUMBER>Surety bond numbers anchored to their label (Bond No. 106648000), including alpha prefixes, leading zeros, and rider suffixes such as 14862-A.
<SURETY_COMPANY>Admitted surety carrier names (the Treasury Circular 570 set) plus carrier-shaped legal names such as Pacific Coast Surety Company, across line breaks and in all-caps.
<SURETY_BOND_PRINCIPAL>The bond principal’s name where the document labels it: Name of Principal: Ficus Bank LLC or Ficus Bank, as Principal.

The compliance entities are label-anchored: a bare number is never masked on its own, so loan numbers, NAIC codes, MIC numbers, and dollar amounts on the same form stay readable. Company names in ordinary prose (a header or a letterhead) are only masked when a bond label identifies them.

Network

CategoryWhat it matchesMasked in textMasked in images
<IP_ADDRESS>IPv4 and IPv6 addresses.

In masked output, each detected value in chunk text (and in region text) is replaced with its tag, for example <US_SSN> where the number appeared. In images, the pixels of each detected value are covered with a solid black box.

If a PII category matters to your use case and is missing here, tell us at support@runcaptain.com and we will look at adding it.

What is not masked

Captain deliberately does not mask dates or generic numbers. Masking those would corrupt ordinary content (a revenue figure, a row count, a timestamp) without meaningfully protecting anyone.

Image masking covers OCR-readable text in images. Detecting and blurring faces, handwriting, and signatures is on the roadmap but not available yet.

Detection engine

Microsoft Presidio icon

Captain’s PII detection is built on Microsoft Presidio (source on GitHub) with Captain-tuned recognizers: the curated 18-entity allowlist above, a custom high-confidence US SSN recognizer, custom compliance recognizers validated against the CFPB TRID model forms and state licensing bond forms, and the spaCy en_core_web_trf transformer model backing <PERSON> and <LOCATION> detection. Every tag listed follows Presidio’s entity semantics.

Masked responses

The examples below are real (trimmed) responses from a collection indexed with mask_pii: true.

1{
2 "query": "SSN",
3 "results": [
4 {
5 "chunk_id": "133671d8fe07301cc1c8c1e52419b447:0",
6 "score": 0.142857,
7 "rerank_score": 0.72265625,
8 "text": "Uploaded image:\n\n# EMPLOYEE ID CARD\n\nName: <PERSON>\n\nSSN: <US_SSN>\n\n<EMAIL_ADDRESS>\n\nPhone: <PHONE_NUMBER>",
9 "modality": "pdf",
10 "match_sources": ["content_embedding", "keyword", "ocr"],
11 "document": {
12 "id": "133671d8fe07301cc1c8c1e52419b447",
13 "filename": "jane-card.png.pdf"
14 }
15 }
16 ],
17 "total_results": 10
18}

The figure region’s image_url serves the redacted crop: the pixels behind each detected value are covered with solid black boxes, so the retrievable image carries no raw PII either.

Enable it

Add "mask_pii": true to any indexing request. It works on every indexing endpoint (S3, GCS, Azure, R2, Supabase, Backblaze, Dropbox, Google Drive, SharePoint, OneDrive, URL, YouTube, and text/file upload), across the bucket, directory, and single-file variants.

$curl -X POST "https://api.captain.dev/v2/collections/{collection_name}/index/s3" \
> -H "Authorization: Bearer $CAPTAIN_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "bucket_name": "my-documents",
> "aws_access_key_id": "AKIA...",
> "aws_secret_access_key": "...",
> "processing_type": "advanced",
> "mask_pii": true
> }'

Query the collection afterwards and the returned results[].text (and regions, if requested) contains the entity tags, never the original values.

Masking report

Every job indexed with mask_pii: true keeps a record of what was masked, so the original values can be handed to a system that needs them without ever entering the index. Captain writes it while the job runs, as one JSON object per file (doc.json) or per text chunk (chunk-{n}.json):

1{
2 "schema_version": "1.0",
3 "file_id": "f05d93c3da314ab4fa24077afc08bd04",
4 "job_id": "job_01HZP5G8N3",
5 "category": "DOC",
6 "entity_count": 24,
7 "truncated": false,
8 "entities": [
9 {
10 "type": "US_SSN",
11 "value": "123-45-6789",
12 "replacement": "<US_SSN>",
13 "score": 1.0,
14 "page": 1,
15 "bbox": { "top": 0.1076, "left": 0.0693, "width": 0.7279, "height": 0.0715 },
16 "block_type": "Text"
17 }
18 ]
19}

value is the original text; replacement is the tag that took its place in the stored content. bbox uses the same 0 to 1 page-normalized coordinates as document regions. page and bbox are null for text-lane objects and for documents so large that the parser returned no block layout.

Two endpoints manage the report, available to any key with the index permission:

  • GET /v2/jobs/{job_id}/pii lists the objects with signed download URLs (valid for at most 300 seconds) and entity counts. The response itself never contains a value.
  • DELETE /v2/jobs/{job_id}/pii destroys the stored objects. Afterwards GET answers 410 with deleted_at, forever; the report is never regenerated even though the source files still exist.

The job status response points at the report while it exists:

1"pii_report": { "state": "retained", "url": "/v2/jobs/job_01HZP5G8N3/pii" }

Reports are kept for 90 days, then expire (GET answers 410 expired). Jobs indexed before 2026-08-28 have no report (409 predates_feature); re-index them to obtain one. Every GET and DELETE is audited.

Notes

  • Opt-in, per job. Files indexed without mask_pii: true are stored as-is; the flag does not retroactively mask previously indexed data.
  • Source files are untouched. Captain only masks the copies it generates and stores (parsed text, region data, figure crops). Your original file in your own bucket is never modified.
  • Fail-safe. If masking cannot run for a file, that file fails to index rather than being stored unmasked.
© 2026 Captain