Skip to main content

openadapt-privacy

Build Status PyPI version Python 3.10+ License: MIT

Remove names, email addresses, phone numbers, SSNs, card numbers, medical record numbers, and other personal or health data from GUI recordings before you store or share them. It scrubs typed text, window titles, accessibility trees, event payloads, and screenshots. Detection uses Microsoft Presidio with a local spaCy model, and scrubbing runs on your machine.

Use it if you record desktop or browser sessions for test automation, robotic process automation (RPA), or training data. It also includes openadapt-privacy-scan, a CI check that runs without Presidio and fails when a repository holds health-data identifiers or recording files.

Quick start

pip install "openadapt-privacy[presidio]"
python -m spacy download en_core_web_sm
from openadapt_privacy import PresidioScrubbingProvider

scrubber = PresidioScrubbingProvider()
print(scrubber.scrub_text("Contact John Smith at john.smith@example.com or 555-123-4567"))
# Contact <PERSON> at <EMAIL_ADDRESS> or <PHONE_NUMBER>

If the spaCy model or a Presidio package is missing, the scrubber raises PrivacyModelUnavailable or ImportError instead of returning text. It doesn't fall back to another model, and it never downloads a model at run time.

You need Python 3.10 or later. Screenshot scrubbing also needs the Tesseract OCR binary on your PATH, for example from brew install tesseract or apt-get install tesseract-ocr.

Scrub element trees and event payloads

scrub_dict walks nested dicts and lists and scrubs the string values of known keys:

from openadapt_privacy import PresidioScrubbingProvider, scrub_dict

scrubber = PresidioScrubbingProvider()
scrub_dict(
    {
        "title": "User Profile - John Smith",
        "tooltip": "Click to contact john@example.com",
        "value": "Call 555-123-4567",
        "coordinates": {"x": 100, "y": 200},
    },
    scrubber,
)
# {'title': 'User Profile - <PERSON>',
#  'tooltip': 'Click to contact <EMAIL_ADDRESS>',
#  'value': 'Call <PHONE_NUMBER>',
#  'coordinates': {'x': 100, 'y': 200}}

Only the keys in config.SCRUB_KEYS_HTML are scrubbed, and non-string values pass through unchanged. Pass scrub_all=True to scrub every string.

Recorded keystrokes often arrive one character at a time, as in j-o-h-n-@-e-x-a-m-p-l-e-.-c-o-m, where no detector can see the address. Under the text and canonical_text keys, the scrubber joins a value in that form, scrubs it, and splits it again. Prose under the same keys is scrubbed as prose:

scrub_dict({"text": "Email: john@example.com"}, scrubber)
# {'text': 'Email: <EMAIL_ADDRESS>'}

scrub_dict({"text": "-".join("john@example.com")}, scrubber)
# {'text': '<-E-M-A-I-L-_-A-D-D-R-E-S-S->'}

Pass separated_keys=["keys"] to use your own field name, or separated_keys=[] to turn keystroke handling off.

Scrub screenshots

from PIL import Image
from openadapt_privacy import PresidioScrubbingProvider

scrubber = PresidioScrubbingProvider()
scrubber.scrub_image(Image.open("screenshot.png")).save("screenshot_scrubbed.png")

Tesseract finds the text, Presidio classifies it, and each detected region is filled with solid red. scripts/generate_demo_images.py produced this output:

Before After
Synthetic profile form with a name, email address, phone number, SSN, and card number The same form with the email address, phone number, SSN, card number, and two field labels covered in red; the name in the dark title bar is still visible

The name in the dark title bar is still visible because Tesseract did not read light text on a dark background. See Limits.

Scrub a whole recording

Recording.scrub scrubs the task description, every action, and every screenshot in one call:

from openadapt_privacy import DictRecordingLoader, PresidioScrubbingProvider

loader = DictRecordingLoader()
recording = loader.load_from_dict({
    "task_description": "Send email to John Smith at john@example.com",
    "actions": [
        {"id": 1, "action_type": "click", "text": "Compose", "timestamp": 1000},
        {"id": 2, "action_type": "type", "text": "-".join("john@example.com"), "timestamp": 2000},
    ],
})
scrubbed = recording.scrub(PresidioScrubbingProvider())
scrubbed.task_description
# 'Send email to <PERSON> at <EMAIL_ADDRESS>'

A screenshot must have its pixels loaded in Screenshot.image. One that has only a path raises UnscrubbedScreenshot instead of passing through unredacted; pass scrub_images=False to leave screenshots out on purpose. The scrubbed recording's metadata["_openadapt_privacy"] records the provider, the package version, and a hash of the settings used.

DictRecordingLoader also reads and writes JSON files with load and save. For your own storage, subclass RecordingLoader and implement load and save, and load_and_scrub works too:

from openadapt_privacy import Recording, RecordingLoader

class SQLiteRecordingLoader(RecordingLoader):
    def load(self, source: str) -> Recording: ...
    def save(self, recording: Recording, destination: str) -> None: ...

Block identifiers in CI

openadapt-privacy-scan checks the files in a directory for health-data identifiers (Ontario health card numbers, chart IDs, and MRNs), remote-desktop hostnames, UNC paths, and recording, screenshot, or database files. In a git repository it checks tracked files and untracked files that aren't ignored; it doesn't read the history. It uses only the Python standard library, so the base package is enough:

pip install openadapt-privacy
openadapt-privacy-scan --self-test   # check that every rule fires on planted fixtures
openadapt-privacy-scan --root .      # scan the tree

The scan exits with:

  • 0 when it read every file and no rule fired.
  • 1 when a rule fired or a file couldn't be read.
  • 2 when the scan couldn't run: the root is missing or unreadable, a directory under it can't be listed, or it has no files. Pass --allow-empty if an empty tree is expected.

The same check is available from Python. Importing openadapt_privacy.scan does not load Presidio, spaCy, or Pillow:

from pathlib import Path
from openadapt_privacy.scan import scan_tree

hits = scan_tree(Path("."))   # for example ["mrn-assigned\tnotes.txt:3"]

openadapt_privacy/gitleaks.toml and openadapt_privacy/phi-patterns.txt carry the same text patterns for gitleaks and git-secrets. They don't include the file-type checks.

Configuration

Settings live on the module-level config object. Change them before you scrub:

from openadapt_privacy.config import config

config.SCRUB_PRESIDIO_IGNORE_ENTITIES = ["DATE_TIME"]  # keep dates
config.SCRUB_FILL_COLOR = 0x000000                     # black boxes; 0xBBGGRR, default red
config.SCRUB_KEYS_HTML = [*config.SCRUB_KEYS_HTML, "label"]

The fields are SCRUB_CHAR, SCRUB_LANGUAGE, SCRUB_FILL_COLOR, SCRUB_KEYS_HTML, SCRUB_KEYS_SEPARATED, ACTION_TEXT_NAME_PREFIX, ACTION_TEXT_NAME_SUFFIX, ACTION_TEXT_SEP, SCRUB_CONFIG_TRF, SCRUB_PRESIDIO_IGNORE_ENTITIES, and SPACY_MODEL_NAME. The entity types come from Presidio (PERSON, EMAIL_ADDRESS, PHONE_NUMBER, US_SSN, CREDIT_CARD, DATE_TIME, LOCATION, IP_ADDRESS, URL, and others), plus ORGANIZATION from spaCy and MEDICAL_RECORD_NUMBER and STREET_ADDRESS from this package.

Limits

  • The test set is 27 synthetic identifiers in tests/test_phi_recall.py. Every one must be redacted with the right entity type. The data is synthetic, not clinical, so measure recall on your own data before you rely on it.
  • English only, with the en_core_web_sm spaCy model.
  • Detection depends on context. A card number that fails the Luhn check is not labeled <CREDIT_CARD>, and other detectors decide how much of it is redacted. 4532-1234-5678-9012 becomes <DATE_TIME>, but 4111 1111 1111 1112 keeps its first eight digits. If your code acts on placeholder names, check them against your own data.
  • Street addresses are found only after a label such as Address: or Mail to.
  • In screenshots, text that Tesseract can't read is not redacted.
  • openadapt-privacy-scan matches regular expressions. It does not claim clinical validation.

If you deploy this where recordings can hold protected health information, read docs/COMPLIANCE.md for the fail-closed behavior, the provenance record each scrub writes, and the review steps around it.

Development

git clone https://github.com/OpenAdaptAI/openadapt-privacy
cd openadapt-privacy
uv sync --extra dev --extra presidio
uv pip install pip    # spacy download installs the model with pip
uv run python -m spacy download en_core_web_sm
uv run pytest

License

MIT. openadapt-privacy is part of OpenAdapt and works without it.

Metadata

Release files for openadapt-privacy 1.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for openadapt-privacy 1.1.2
File Size Uploaded
openadapt_privacy-1.1.2.tar.gz 81.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for openadapt-privacy 1.1.2
File Interpreter ABI Platform
openadapt_privacy-1.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 113.6 kB

Release files / openadapt_privacy-1.1.2.tar.gz

Download URL openadapt_privacy-1.1.2.tar.gz
Size 81.1 kB
Tags Source
SHA-256 checksum
How to use checksums
d90287edaa13470955f15d5f6eeb37b075678aeb55de324f8b8207682009bcc2
BLAKE2b-256 checksum
How to use checksums
2c67c5ab780dd460b2830b35ac5ed763f5b3e565ba63d3fcbb5fc2e1a260e256
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / openadapt_privacy-1.1.2-py3-none-any.whl

Download URL openadapt_privacy-1.1.2-py3-none-any.whl
Size 32.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6e542b2fea44bdf8872430687946ec805fef22dc7d228a56a1e65871beebe47a
BLAKE2b-256 checksum
How to use checksums
27ee8ee2ae415bd5364268252ebfdcf672a32f68078f94df70b15bf77d7766c3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.2 This release

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page