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.
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ffd2326a4002f2c5711d9bbd7cb1c4dc4e8f837c422ebf7b2f97b818e68f70f
|
|
| MD5 |
d9496272a07885773a5f8d2894fecde0
|
|
| BLAKE2b-256 |
f44bfea1d4be960854e008cbb16212635051361d2989eaa0f284049485fd7e05
|
Provenance
The following attestation bundles were made for toolmask_llm-0.1.0.tar.gz:
Publisher:
publish.yml on iiTzAK/toolmask
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
toolmask_llm-0.1.0.tar.gz -
Subject digest:
6ffd2326a4002f2c5711d9bbd7cb1c4dc4e8f837c422ebf7b2f97b818e68f70f - Sigstore transparency entry: 2128381138
- Sigstore integration time:
-
Permalink:
iiTzAK/toolmask@f80f74d7826708fde908c8715edb6732d1412cb0 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/iiTzAK
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f80f74d7826708fde908c8715edb6732d1412cb0 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c999f33ab42b33be2d897c45d80fa2f9f8587553128829bf77d9b31ac702d7bb
|
|
| MD5 |
d73a3dd12ac7ff04e07774db461cf0ea
|
|
| BLAKE2b-256 |
ae46ff8e64a48b3aa2f1dd88a3676851ce0c9fb548145776ad6f885b2ebdb6c9
|
Provenance
The following attestation bundles were made for toolmask_llm-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on iiTzAK/toolmask
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
toolmask_llm-0.1.0-py3-none-any.whl -
Subject digest:
c999f33ab42b33be2d897c45d80fa2f9f8587553128829bf77d9b31ac702d7bb - Sigstore transparency entry: 2128381181
- Sigstore integration time:
-
Permalink:
iiTzAK/toolmask@f80f74d7826708fde908c8715edb6732d1412cb0 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/iiTzAK
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f80f74d7826708fde908c8715edb6732d1412cb0 -
Trigger Event:
push
-
Statement type: