Skip to main content

maskflow

Mask PII before it reaches an LLM. Unmask the response. Works with any provider.

from maskflow import mask_and_call


def call_claude(masked_prompt: str) -> str:
    return (
        anthropic_client.messages.create(
            model="claude-sonnet-5",
            max_tokens=1024,
            messages=[{"role": "user", "content": masked_prompt}],
        )
        .content[0]
        .text
    )


response = mask_and_call(
    "Hi, I'm Jane Doe (jane@example.com). My order shipped to 123 Main St but never arrived.",
    call_claude,
)
# Claude only ever sees "Hi, I'm <PERSON_NAME_1> (<EMAIL_1>). My order shipped to
# <ADDRESS_1> but never arrived." -- response comes back with the real values restored.

Install

pip install maskflow-sdk
python -m spacy download en_core_web_sm

Why this shape

mask_and_call takes a plain function, not a specific provider's client. You write the one line that actually calls your LLM (Claude, OpenAI, Gemini, a local model, anything) -- maskflow never parses or depends on any provider's SDK, so it doesn't break when a provider changes their API and works with providers it's never heard of.

response = mask_and_call(prompt, lambda masked: my_llm_client.generate(masked))

Lower-level API

For more control than the wrapper gives, mask/unmask are available directly. Persisting the mapping between calls is your responsibility -- neither function uses a database, but mask() does read a .maskflowrc if one is discovered (see "Configuration" below); pass config= to skip that lookup entirely.

from maskflow import mask, unmask

result = mask("Email me at alice@example.com.")
result.masked_text  # "Email me at <EMAIL_1>."
result.mapping  # {"<EMAIL_1>": "alice@example.com"}

unmask(result.masked_text, result.mapping)  # original text, restored

Session-scoped masking

mask() restarts its token numbering on every call, so two separate calls can each hand out <PHONE_1> for two different phone numbers -- fine for one-shot use, wrong for a multi-turn agent that needs the same value to keep the same token across calls. session() fixes that by keeping value->token identity stable for as long as it's open:

import maskflow

with maskflow.session() as s:
    prompt = s.mask(user_input)
    args = s.mask_json(tool_call_arguments)  # masks string leaves only, keys untouched
    reply = s.unmask(llm_response)

Sessions are closeable (with ... as s: or s.close()) and TTL-bounded (ttl_seconds, default 3600 seconds); either purges the mapping. maskflow.async_session() is the asyncio counterpart. Neither is thread-safe. See docs/agent-sessions.md for the concrete before/after this fixes.

Two things a session-based server needs (added in 0.7.0, all additive):

  • s.snapshot() -> bytes / s.restore(blob) — serialize a session's full masking state (mapping + every identity cache) and rebuild it in another process, so it keeps minting the same tokens for the same values. s.mapping exposes the live Mapping. The blob holds raw PII — encrypt it before it touches disk or a shared store.
  • session(patterns_only=True) — skip the spaCy NER pass entirely (routes through detect_patterns_only): a large latency/throughput win in exchange for missing bare names and addresses, the same tradeoff docs/logging.md makes for the log filter.

maskflow-gateway (a drop-in OpenAI/Anthropic proxy) is built on exactly these — see docs/gateway.md.

Configuration (.maskflowrc)

mask(), mask_and_call(), and session()/async_session() all read a .maskflowrc file automatically if one is discovered on disk -- no code change needed to adjust entity thresholds, disable an entity, add a custom regex-based entity, exclude specific values, or change the substitution strategy (replace/redact/mask/hash/surrogate). Pass config= to bypass discovery and supply one explicitly:

import maskflow
from maskflow_core.config import RootConfig, EntityConfig
from maskflow_core.strategies import Strategy

result = maskflow.mask(
    "Reach me at alice@example.com.",
    config=RootConfig(entities={"EMAIL": EntityConfig(strategy=Strategy.MASK)}),
)

With no .maskflowrc anywhere and no config= passed, behavior is unchanged from before this feature existed. See docs/configuration.md for the full schema, precedence rules, and maskflow.reload_config() (forces a fresh discovery in a long-running process, which otherwise caches the discovered config once per process).

What gets detected

Email, phone, SSN, credit card, IP address, AWS access key, API key / generic secret, JWT, IBAN, street address, person name, date of birth -- via regex + structural validation (Luhn, mod-97, etc.) plus spaCy NER for names and dates, with keyword-context confidence boosting. These 12 recognizers ship in maskflow-pack-intl, which maskflow-sdk depends on automatically; the engine itself lives in maskflow-core and ships with none built in.

Tests

uv run pytest

Release files for maskflow-sdk 0.9.0

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

Source distribution (sdist)

Source distribution for maskflow-sdk 0.9.0
File Size Uploaded
maskflow_sdk-0.9.0.tar.gz 22.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for maskflow-sdk 0.9.0
File Interpreter ABI Platform
maskflow_sdk-0.9.0-py3-none-any.whl Python 3 none any Details

Total release size: 38.8 kB

Release files / maskflow_sdk-0.9.0.tar.gz

Download URL maskflow_sdk-0.9.0.tar.gz
Size 22.4 kB
Tags Source
SHA-256 checksum
How to use checksums
263c4f190b4d4c7e12fada69b3cda2fd8567da7110793ac6a91d6262f1632f1c
BLAKE2b-256 checksum
How to use checksums
90398f8a13d8c4b74a5cd4da2ffda221860592180cb01c60f44ef357ccc698db
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 Sep 8, 2026.

Transparency log

Release files / maskflow_sdk-0.9.0-py3-none-any.whl

Download URL maskflow_sdk-0.9.0-py3-none-any.whl
Size 16.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
655689b39b80c4b41061fed718c83b2aff08e50bf4aece8a10414e4a2fbd8133
BLAKE2b-256 checksum
How to use checksums
522e4d514c7e9186d30003a565fb7447d303495f01e80feb21a1f1779efa84db
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 Sep 8, 2026.

Transparency log

Release history Release notifications | RSS feed

0.9.1

2 release files

This release

0.9.0 This release

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

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