Skip to main content

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 ceiling — BANK_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

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)

Source distribution for toolmask-llm 0.1.0
File Size Uploaded
toolmask_llm-0.1.0.tar.gz 12.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for toolmask-llm 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

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