piilint
Local-first PII scanner for the files developers actually commit and send — notebooks, CSV, JSON, Parquet, Excel/Word/PDF (optional), and source code. Nothing leaves your machine.
Disclaimer: piilint helps you find sensitive data before it leaks. It is a detection aid, not a compliance certification, and cannot guarantee that all sensitive data is found. It does not make anyone GDPR/HIPAA/PCI compliant.
Package: piilint (PyPI name piiscan was already taken) · License: Apache-2.0 · Repo: thelonewander3r/PIIScanner
Not a secrets scanner. Pair with gitleaks or trufflehog for API keys and tokens.
What's new in 0.2.0 (prep)
This checkout is 0.2.0 — not tagged and not on PyPI yet. PyPI still serves 0.1.0 until Emanuel goes. Install from PyPI remains primary: pip install piilint.
- Office formats via optional
piilint[office]: Excel.xlsx/.xlsm, Word.docx, PDF embedded text (no OCR / no legacy.doc) - Redact:
piilint redact PATH -o OUTwrites cleaned copies (text, json/jsonl, csv/tsv, notebooks, parquet, xlsx, docx) - Locales:
SIN_CA/NINO_UK/BSN_NL+scan.phone_region/scan.phone_regions - Policy packs:
examples/policies/(strict-ci,data-eng,open-source-lib) - Optional NER still via
piilint[ner] - Local metadata history (Slice B MVP, already on
main):report --metadata-only,history --since,sync --metadata --dry-run— no network / no SaaS
Further team-layer / hosted sync is paused. Tag v0.2.0 waits on Emanuel.
Five-minute path
1. Install
From PyPI (piilint — 0.1.0 published; 0.2.0 not on PyPI yet):
# recommended for CLI use
pipx install piilint
# or one-off
uvx piilint --version
# or classic
pip install piilint
From git / local checkout (contribute or run unreleased main):
pipx install git+https://github.com/thelonewander3r/PIIScanner.git
# or
uv sync --extra dev
uv run piilint --version
2. Scan
piilint . # scan the current directory
piilint . --fail-on high # fail CI/pre-commit on high-severity findings
Exit codes: 0 clean / nothing staged · 1 findings at or above --fail-on · 2 usage/config/git error.
3. Wire into git / CI (optional)
Pre-commit — add to .pre-commit-config.yaml:
repos:
- repo: https://github.com/thelonewander3r/PIIScanner
rev: v0.1.0 # pin to a release tag
hooks:
- id: piilint
# Default: --staged --fail-on medium
GitHub Action + SARIF — drop into a workflow:
- uses: actions/checkout@v4
- name: Run piilint
id: piilint
uses: thelonewander3r/PIIScanner@v0.1.0 # or @main for latest
with:
path: .
fail-on: high
format: sarif
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ${{ steps.piilint.outputs.sarif-path }}
4. Adopt without boiling the ocean
piilint baseline . -o piilint-baseline.json # fingerprints only — never raw PII
piilint . --baseline piilint-baseline.json # report NEW findings only
piilint . --staged # scan only git-staged files
Demo
The classic leak: a notebook runs df.head() and the output cell still holds customer rows when you commit the .ipynb.
Synthetic demo (no real PII): tests/corpus/notebook/leak_demo.ipynb
piilint tests/corpus/notebook
See also examples/README.md for a short pointer and expected story.
All of tests/corpus/ is 100% synthetic labeled data generated for tests.
Configuration
Precedence (highest wins): CLI flags → piilint.toml at the scan root → [tool.piilint] in pyproject.toml → built-in defaults.
# piilint.toml
[scan]
fail_on = "high"
min_confidence = 0.6
exclude = ["tests/fixtures/**"]
phone_region = "US"
phone_regions = ["CA", "GB"]
[entities]
ip_address = false
# Locale IDs: SIN_CA on by default; enable NINO_UK / BSN_NL when needed
nino_uk = true
bsn_nl = true
[entities.email]
severity = "medium"
[allowlist]
values = ["support@mycompany.com"]
domains = ["example.com", "mycompany.dev"]
Phone regions
scan.phone_region— primary default region forphonenumbersparsing (default"US").scan.phone_regions— optional extra ISO regions tried after the primary (default[]). Candidates are validated withphonenumbers.is_valid_number; E.164 /+countrynumbers still match without listing every region. US hard-negative corpora must stay clean.
Locale national IDs
| Entity | Default | Notes |
|---|---|---|
SIN_CA |
on | Canadian SIN format + Luhn |
NINO_UK |
off | Strict NI format; requires context (NI / NINO / National Insurance) |
BSN_NL |
off | Dutch BSN + 11-proef checksum |
Enable via [entities] nino_uk = true / bsn_nl = true (or NINO_UK / BSN_NL).
Disclaimer (locale IDs): These recognizers are a detection aid only. They are not legal identity verification and do not make anyone GDPR, HIPAA, or PCI compliant.
.piiignore— gitignore-syntax path excludes (combined with.gitignore).- Inline suppressions (text/code lines):
# piilint: ignoreor# piilint: ignore[EMAIL](comma-list). Not applied to tabular/column-aggregated findings in v0. - Allowlists — exact normalized values and email domains drop matching findings.
- Test-data downweight — obvious fixtures (example.com, 555-01xx, 4111…, RFC5737 IPs) get −0.4 confidence and severity capped at low, then
min_confidenceis re-applied.
Baseline + staged
Adopt without fixing history first, and scan only what is about to land in git.
piilint baseline . -o piilint-baseline.json
piilint . --baseline piilint-baseline.json
piilint . --staged
Fingerprint design: SHA-256(relative path, entity, normalized-value hash, occurrence index). Line numbers are excluded so ordinary edits do not resurrect old findings.
Tradeoff: an edit that only moves a value to a different line will not reappear as "new." Moved or duplicated values may still match by occurrence index. Commit a fresh baseline when you intentionally accept a new set of findings.
Output formats
Default output is a Rich console report (grouped by file → severity-colored table → totals).
piilint . --format json
piilint . --format sarif > piilint.sarif
piilint . --format json --baseline piilint-baseline.json --fail-on high
JSON includes a config_hash: SHA-256 of a canonical JSON snapshot of the effective
scan config fields that affect detection/policy (fail_on, min_confidence, exclude,
entity_enabled, severity_overrides, allowlists, phone_region, phone_regions). Paths and timestamps
are excluded so the hash is stable across identical policy runs.
--show-matches unmasks the console Sample column for local triage only. It is refused
when CI=true (exit 2) and does not apply to JSON/SARIF (those formats never emit raw PII).
Local metadata history (Sprint 15 Slice B)
Opt-in metadata-only history for “what’s new since T,” stored in a local SQLite DB.
No network upload. Default piilint . / scan / baseline / redact never write history
and never dial out.
Trust boundary: records may include entity, severity, fingerprints
(finding_fingerprint, path_fingerprint, value_fingerprint), config_hash,
scanned_at, and optional repo_id / tool_version / schema_version. They must never
include raw paths, line/row/column/cell, masked_sample, match values, or file bytes.
DB path (stdlib only; Windows-first):
- Windows:
%LOCALAPPDATA%\piilint\history.sqlite3 - else:
$XDG_DATA_HOME/piilint/history.sqlite3(fallback~/.local/share/piilint/history.sqlite3) - Override:
PIILINT_HISTORY_PATH(full file) orPIILINT_DATA_DIR(directory)
History queries are scoped to a deterministic local workspace id (repo_id) derived
from the git root (or scan directory when not in a git repo). Pass an optional
workspace path to history / sync to match the tree used by report (default .).
Naive ISO datetimes in --since (no Z or offset) are interpreted as UTC.
# Scan target, emit metadata-only JSON, and auto-record into local history (no network)
piilint report --metadata-only
piilint report PATH --metadata-only -o meta.json
# New finding_fingerprints first seen since T (relative or ISO; naive ISO = UTC)
piilint history --since 7d
piilint history --since 2026-08-01T00:00:00Z --json
# Dry-run sync summary only — counts, payload bytes, destination <not configured>; sends nothing
piilint sync --metadata --dry-run
report --metadata-only does append a run to the local history DB (documented intentional
side effect). Status lines from report, history, and sync --metadata --dry-run print
counts and fingerprints only — never raw paths or match values. Real cloud upload is out of
scope; sync --metadata without --dry-run exits with guidance and opens no sockets.
See docs/TEAM_LAYER.md for the broader team-layer design.
Pre-commit hook
This repo ships a pre-commit hook definition in .pre-commit-hooks.yaml.
Add to your consuming project's .pre-commit-config.yaml:
repos:
- repo: https://github.com/thelonewander3r/PIIScanner
rev: v0.1.0 # pin to a release tag
hooks:
- id: piilint
# Default args from the hook repo: --staged --fail-on medium
# Override fail-on (or drop --staged) by replacing args:
# args: ["--staged", "--fail-on", "high"]
Notes:
- The hook runs
piilint --stagedand setspass_filenames: falseso pre-commit does not append paths (staged mode reads the git index). fail-ondefaults to medium in the hook; change viaargsas shown above.- Requires a git repository at hook time (same as CLI
--staged).
GitHub Action
Composite action at action.yml. Example workflow:
name: piilint
on:
pull_request:
push:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
# needed only if you upload SARIF:
# security-events: write
steps:
- uses: actions/checkout@v4
- name: Run piilint
id: piilint
uses: thelonewander3r/PIIScanner@main # pin to a tag when available
with:
path: .
fail-on: high
format: sarif # console | json | sarif
# baseline: piilint-baseline.json
# staged: "false"
# version: "0.1.0" # install from PyPI; omit to pip-install action checkout
# extra-args: "--exclude 'vendor/**'"
# SARIF upload is the caller's job — the action only writes the file.
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ${{ steps.piilint.outputs.sarif-path }}
Action inputs
| Input | Default | Description |
|---|---|---|
path |
. |
Path to scan |
fail-on |
(empty) | high / medium / low / never (empty → config/default) |
format |
console |
console / json / sarif |
baseline |
(empty) | Optional baseline JSON path |
staged |
false |
Scan only git-staged files |
extra-args |
(empty) | Extra CLI args (space-separated) |
version |
(empty) | PyPI version; empty installs from github.action_path |
sarif-file |
piilint.sarif |
Output path when format=sarif |
python-version |
3.12 |
Python for the composite runner |
Action outputs
| Output | Description |
|---|---|
sarif-path |
Path to written SARIF when format=sarif; empty otherwise |
CI & release
- CI:
.github/workflows/ci.yml—{ubuntu, windows, macos} × {3.10, 3.13}with ruff, mypy (files=src/piilint), pytest (incl. benchmark gate), andpiilint --version. - Release:
.github/workflows/release.yml— on tagv*/ GitHub Release published: build withuv build(hatchling), publish viapypa/gh-action-pypi-publishusing OIDC trusted publishing (no long-lived PyPI token).
PyPI trusted publisher checklist (maintainer)
Full tag-day steps: docs/RELEASE.md.
Hard stop: tag v0.2.0 only after Emanuel’s explicit go (v0.1.0 already published). No long-lived PyPI API tokens.
Emanuel-only (PyPI UI)
- Create project
piilintor register a pending trusted publisher (preferred; creates the project on first upload). - PyPI → Publishing → Trusted publishers → GitHub:
- Owner:
thelonewander3r - Repository:
PIIScanner - Workflow name:
release.yml(filename only) - Environment name:
pypi(must matchrelease.yml)
- Owner:
Emanuel-only (GitHub UI)
- Repo Settings → Environments → create
pypi(recommend required reviewers / wait timer).
Then (after Emanuel go)
- Do not store a PyPI API token in Actions secrets for this flow.
- From release commit on
main:git tag v0.2.0 && git push origin v0.2.0→ watch Actions Release → verifyuvx piilint --version/pipx install piilint.
Optional NER (names & addresses)
PERSON/ADDRESS detection is off by default and lives behind an optional extra so the base install stays lean and scan-time stays offline.
pip install "piilint[ner]" # or: uv sync --extra ner
piilint setup-ner # downloads en_core_web_sm (network; once)
piilint . --ner # enable PERSON + ADDRESS for this run
- Without
[ner]installed, normal scans are unchanged;piilint . --nerexits 2 with an install hint. - With
[ner]but no model,--nerexits 2 asking you to runsetup-ner. - Config toggles
entities.person/entities.addressdefault to false;--nerenables both for the run. - Enabling PERSON/ADDRESS via config alone still requires the
[ner]extra + model (same clear exit 2 guidance as--nerif they are missing). - Only English (
en_core_web_sm) is supported in this phase. No scan-time network — model download is setup-only.
Optional office formats (Excel, Word, PDF)
Excel (.xlsx/.xlsm), Word (.docx), and PDF embedded text scanning live behind an optional extra so the base install stays lean:
pip install "piilint[office]" # or: uv sync --extra office
piilint scan ./docs
- Without
[office], those files are skipped with a one-time stderr install hint; other formats keep scanning. - No OCR (image-only PDFs yield nothing). No legacy
.doc(Word 97–2003 binary). piilint redact -ocan write cleaned.xlsxand.docxcopies when[office]is installed. PDF redact is deferred.
Redact (cleaned copies)
Write copies with PII spans replaced by the same masks as findings (no in-place overwrite in v1):
piilint redact ./data -o ./data-clean
Supported today: text + json/jsonl + csv/tsv + notebooks + parquet (string columns) + xlsx/xlsm + docx + PDF embedded text (via optional piilint[office]; no OCR / no legacy .doc). PDF redact deferred. Uses the base wheel only (no new deps / no presidio-anonymizer). Honors the same config/policy as scan (allowlists, # piilint: ignore, entity toggles, min_confidence, excludes). Sources under the input path are never modified.
Example policy packs
Starter piilint.toml templates (detection aid only ? not compliance certifications):
examples/policies/strict-ci.tomlexamples/policies/data-eng.tomlexamples/policies/open-source-lib.toml
See examples/policies/README.md for copy instructions and disclaimer.
Status
0.1.0 is published on PyPI (tag v0.1.0, OIDC trusted publisher). This tree is 0.2.0 prep — not tagged and not on PyPI yet.
What's in 0.2.0: office extras ([office] for xlsx/xlsm/docx/PDF text), piilint redact, locale IDs + phone_regions, example policy packs, optional NER ([ner]), and local metadata history (report --metadata-only / history --since / sync --metadata --dry-run; no network). Further team-layer / hosted sync is paused.
Install from PyPI remains primary: pip install piilint (currently 0.1.0). Future releases: docs/RELEASE.md.
Contributing & security
- Contributors: see
CONTRIBUTING.md - Vulnerability reports: see
SECURITY.md - Changelog:
CHANGELOG.md - Maintainer release runbook:
docs/RELEASE.md
License
Apache-2.0
Release files for piilint 0.2.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 | |
|---|---|---|---|
| piilint-0.2.0.tar.gz | 106.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| piilint-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 186.4 kB
Release files / piilint-0.2.0.tar.gz
| Download URL | piilint-0.2.0.tar.gz |
|---|---|
| Size | 106.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d039b5d8716f5a8446df073498edc403ff84ac1ad338a519e2f7a386ddd1b15e
|
|
BLAKE2b-256 checksum How to use checksums |
2e8debef60a8755b67ab2ce22dc6d6be5a3d33bfea1b5744b641f9bf4912d934
|
| 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 Aug 17, 2026.
Transparency logRelease files / piilint-0.2.0-py3-none-any.whl
| Download URL | piilint-0.2.0-py3-none-any.whl |
|---|---|
| Size | 79.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a19309eaaf1ae83720aefe5a683df293b4e0e8f7bc55851b0d7d5b8bca2fb781
|
|
BLAKE2b-256 checksum How to use checksums |
d28616a6e9eda64ed27811132eb4da274b2024498683fece87ac5294aa81c39e
|
| 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 Aug 17, 2026.
Transparency log