Skip to main content

Mask PII in LLM tool-call outputs before they reach the model, with role-based access control. Zero dependencies.

Project description

toolmask

Mask PII in LLM tool call outputs before they reach the model with role-based access control. Zero dependencies.

Python 3.9+ License MIT CI zero dependencies

Why

Agents call tools database lookups, internal APIs, CRM queries and those tools often return sensitive data: PAN, Aadhaar, bank details, salaries. That raw output gets stuffed straight into the LLM's context window, where it can leak back out through model responses, logs, or observability traces. toolmask sits at the tool output boundary and masks it before it ever reaches the model, and role-based access control (RBAC) decides what each caller's role is allowed to see unmasked.

# Problem: raw tool output goes straight into the LLM context
def get_employee_record(emp_id):
    return {"pan": "ABCDE1234F", "salary": 2500000, "email": "a@b.com"}

# Solution: mask at the boundary, before it's ever seen by the model
from toolmask import mask_tool_output

@mask_tool_output()
def get_employee_record(emp_id):
    return {"pan": "ABCDE1234F", "salary": 2500000, "email": "a@b.com"}
# -> {"pan": "[PAN]", "salary": "[SENSITIVE]", "email": "[EMAIL]"}

Install

pip install toolmask-llm

The import name is toolmask:

from toolmask import mask

Or install from source:

git clone https://github.com/iiTzAK/toolmask
pip install -e ./toolmask

Quickstart

from toolmask import mask

mask({"pan": "ABCDE1234F", "salary": 2500000, "email": "a@b.com"})
# {"pan": "[PAN]", "salary": "[SENSITIVE]", "email": "[EMAIL]"}

Wrap a tool function so its return value is auto-masked:

from toolmask import mask_tool_output

@mask_tool_output()
def get_employee_record(emp_id: str) -> dict:
    return {"pan": "ABCDE1234F", "email": "a@b.com"}

get_employee_record("E1")  # {"pan": "[PAN]", "email": "[EMAIL]"}

RBAC: let some roles see specific categories unmasked:

from toolmask import mask_tool_output, Policy

analyst_policy = Policy.allow("analyst", {"EMAIL"})
admin_policy = Policy.allow_all("admin")

@mask_tool_output(role="analyst", policy=analyst_policy)
def get_record(emp_id: str) -> dict:
    return {"pan": "ABCDE1234F", "email": "a@b.com"}

get_record("E1")  # {"pan": "[PAN]", "email": "a@b.com"}  <- EMAIL allowed for analyst

See examples/openai_tool_calling.py for a full runnable example.

What it detects

Category What it matches
PAN Indian PAN card format (ABCDE1234F)
AADHAAR 12-digit Aadhaar number, space/hyphen grouped or not
IFSC Indian bank IFSC code (HDFC0001234)
BANK_ACCOUNT 9-18 digit runs (see false-positive note below)
CREDIT_CARD 13-19 digit card numbers, validated with a Luhn checksum
EMAIL Standard email addresses
PHONE Indian mobile numbers, optional +91 prefix

Field-name based (masked regardless of the value's shape, e.g. a numeric salary): keys matching salary, ctc, compensation, account/account_number, ifsc, password, secret, token, api_key, ssn, dob.

RBAC

Deny-by-default: any category not explicitly granted to a role is masked, and a role with no policy entry gets nothing unmasked. Policy is a simple role -> allowed-categories map:

from toolmask import Policy

policy = Policy.allow("hr_admin", {"PAN", "EMAIL", "AADHAAR"})
policy = Policy.allow_all("superadmin")  # sees everything unmasked

Works with

Anything that calls a Python function and gets back a str/dict/list/JSON-serializable value — OpenAI function calling, Anthropic tool use, LangChain tools, or a hand-rolled agent loop. toolmask only touches the tool's return value, so there's nothing to integrate beyond wrapping the function. See examples/.

Design notes

  • Zero runtime dependencies — stdlib re and copy only.
  • Deny-by-default — no policy, or a role not covered by one, means everything is masked.
  • Non-mutating — masking always operates on a deep copy; your original data is untouched.
  • Regex-based, and honest about the ceilingBANK_ACCOUNT (any 9-18 digit run) has a real false-positive rate, and PHONE doesn't validate against the real Indian numbering plan. This is a lightweight boundary guard, not a replacement for Presidio or LLM-Guard — pair it with those for high-stakes pipelines. PRs adding better detectors or locales are welcome.

toolmask vs Presidio vs LLM-Guard

Short version: they're detectors/anonymizers; toolmask is a tool-output boundary guard with RBAC. Use toolmask for the "mask what my agent's tools return, per role" job; reach for Presidio/LLM-Guard when you need ML-grade NER across free text. Full breakdown: docs/comparison.md.

toolmask Presidio LLM-Guard
Focus Tool/function-call outputs General PII detect/anonymize LLM input/output scanners
Role-based unmasking (RBAC) ✅ built-in
India PII (PAN/Aadhaar/IFSC) out of the box partial partial
Dependencies 0 spaCy + models several
Drop-in decorator

Roadmap

  • More locales beyond India-specific formats
  • Per-call allowlist/denylist overrides
  • Streaming (mask incrementally as tokens arrive)
  • Entropy-based generic secret detection

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

toolmask_llm-0.1.0.tar.gz (12.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

toolmask_llm-0.1.0-py3-none-any.whl (10.5 kB view details)

Uploaded Python 3

File details

Details for the file toolmask_llm-0.1.0.tar.gz.

File metadata

  • Download URL: toolmask_llm-0.1.0.tar.gz
  • Upload date:
  • Size: 12.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for toolmask_llm-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6ffd2326a4002f2c5711d9bbd7cb1c4dc4e8f837c422ebf7b2f97b818e68f70f
MD5 d9496272a07885773a5f8d2894fecde0
BLAKE2b-256 f44bfea1d4be960854e008cbb16212635051361d2989eaa0f284049485fd7e05

See more details on using hashes here.

Provenance

The following attestation bundles were made for toolmask_llm-0.1.0.tar.gz:

Publisher: publish.yml on iiTzAK/toolmask

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file toolmask_llm-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: toolmask_llm-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 10.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for toolmask_llm-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c999f33ab42b33be2d897c45d80fa2f9f8587553128829bf77d9b31ac702d7bb
MD5 d73a3dd12ac7ff04e07774db461cf0ea
BLAKE2b-256 ae46ff8e64a48b3aa2f1dd88a3676851ce0c9fb548145776ad6f885b2ebdb6c9

See more details on using hashes here.

Provenance

The following attestation bundles were made for toolmask_llm-0.1.0-py3-none-any.whl:

Publisher: publish.yml on iiTzAK/toolmask

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page