toolmask
Mask PII in LLM tool call outputs before they reach the model with role-based access control. 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
reandcopyonly. - 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 ceiling —
BANK_ACCOUNT(any 9-18 digit run) has a real false-positive rate, andPHONEdoesn'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
Metadata
Release files for toolmask-llm 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| toolmask_llm-0.1.0.tar.gz | 12.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| toolmask_llm-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 23.3 kB
Release files / toolmask_llm-0.1.0.tar.gz
| Download URL | toolmask_llm-0.1.0.tar.gz |
|---|---|
| Size | 12.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6ffd2326a4002f2c5711d9bbd7cb1c4dc4e8f837c422ebf7b2f97b818e68f70f
|
|
BLAKE2b-256 checksum How to use checksums |
f44bfea1d4be960854e008cbb16212635051361d2989eaa0f284049485fd7e05
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 Jul 9, 2026.
Transparency logRelease files / toolmask_llm-0.1.0-py3-none-any.whl
| Download URL | toolmask_llm-0.1.0-py3-none-any.whl |
|---|---|
| Size | 10.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c999f33ab42b33be2d897c45d80fa2f9f8587553128829bf77d9b31ac702d7bb
|
|
BLAKE2b-256 checksum How to use checksums |
ae46ff8e64a48b3aa2f1dd88a3676851ce0c9fb548145776ad6f885b2ebdb6c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 Jul 9, 2026.
Transparency log