privacy-linter
Skill0 starsChanged 4 months agoMITView on GitHub
Deterministic pre-disclosure privacy scanner for git-staged changes — flags Direct PII (email, phone, SSN, Luhn-valid credit card, IP address), Secrets (AWS/GitHub/Slack/Stripe/Google/Anthropic tokens, private key blocks, hardcoded-credential assignments), and Metadata risk (image/document file types that commonly carry EXIF/embedded properties, with a confirmed EXIF GPS check when Pillow is available) before a commit lands. `--scan-history` walks the full commit history for PII/secrets ever introduced, even if later removed from HEAD. Can also strip EXIF metadata from a JPEG/TIFF outright with `--strip-metadata`, and track findings over time with `--log-dir` + `scan_log_history.py` (bridges into agent-eval's score_eval.py for trend/regression reporting, same pattern broadcast's qa_gate_history.py uses). Runs entirely locally with no model or network call — pattern-matching only. Use when the user wants to check a commit, diff, or file for leaked PII or secrets before it's committed or shared, asks "will this diff leak anything," "did I commit an API key," "did I already leak something in an old commit," "is my leak rate getting better or worse over time," wants a pre-commit privacy check, wants to scan a specific file or pasted text for personal information, or wants to strip GPS/EXIF data from a photo before sharing it. Triggers on "check this diff for PII," "will committing this leak anything," "scan this file for personal info," "did I leak a secret/API key," "check git history for leaked secrets," "strip metadata from this photo," "track privacy findings over time," "set up a privacy pre-commit hook," and the script `scan_diff.py`. Does not cover inference-cue or stylometric leaks, or text redaction — see ${CLAUDE_PLUGIN_ROOT}/references/leak-taxonomy.md for why those are deliberately out of scope for this version.
In this repository19 other files it gives its agents.
- agent-evalSkill
- agent-redteamSkill
- broadcastSkill
- deid-reid-harnessSkill
- fiction-workshopSkill
- issue-reconcilerSkill
- privacy-threat-oracleSkill
- repo-pincerSkill
- research-ledgerSkill
- review-and-planSkill
- style-obfuscatorSkill
- wiki-governorSkill
- wiki-librarianSkill
- wiki-operatorSkill
- wiki-privacy-auditSkill
- wiki-synthesizerSkill
- wiki-teacherSkill
- wiki-warehouseSkill
- my-skill-nameSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 3 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 50
Or scan something specific: instead of the staged diff:
- Distinctive: line 63
Gate or advise: Default behavior always exits 0 — advisory only, matching the source design's "advisory, not blocking by default." Add a …
- Distinctive: line 100
Track findings over time: instead of only ever seeing one run's snapshot — off by default, opt in per invocation:
- Very specific: line 114
If python3 is not on PATH where you commit (NixOS, some version-manager shells), the installer refuses to write a hook that would exit 127 …
- Very specific: line 187
Never claim a pass/no-findings result means "definitely clean" — say what was and wasn't checked (e.g., no findings from pattern-matching …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Privacy Linter
Catches leakage at the one cheap intervention point — the moment of creation — rather than after content is already committed or posted. Advisory by default: a low false-positive rate matters more than catching everything, because an ignored linter is useless.
Why this is deterministic-only, not model-based
The design this implements (Projects/Privacy OS - Pre-Disclosure Privacy Linter in the
source vault) requires local-only analysis: sending content to an external LLM API for
leak detection is itself a potential disclosure. Of the four leak classes in that
design — Direct PII, Metadata, Inference cues, Stylometric fingerprint — only the first
two are mechanically detectable without a model. This version implements those two, end
to end, with real tests, the same order deid-reid-harness used for its own tracks
(ship the model-independent slice first) — plus Secrets/credentials, a class added
afterward that isn't part of the source design's original four but is mechanically
detectable the exact same way (see ${CLAUDE_PLUGIN_ROOT}/references/leak-taxonomy.md for why it's called
out as an addition, not silently folded into "Direct PII"). Inference cues and
stylometric fingerprinting are documented, not built — see ${CLAUDE_PLUGIN_ROOT}/references/leak-taxonomy.md
— because building them today would mean either standing up a local-model pipeline
(real setup work, not yet done) or using an external LLM for exactly the analysis this
project exists to keep local. Don't silently reach for an external model to "complete"
those two classes. Text redaction for Direct PII/Secrets is deliberately not built for
a related reason — auto-rewriting is a judgment call (did it redact too much/too
little), unlike --strip-metadata's EXIF removal, which has no partial-credit version.
How this works
- Scan staged changes (the default, no arguments): reads
git diff --cached -U0for added lines andgit diff --cached --name-onlyfor the staged file list.- Direct PII — regex-matched against added lines only (not removed or context
lines): email, phone, SSN (strict
###-##-####form), credit card (13-19 digit run, Luhn-validated to cut false positives), IPv4 (octet-range validated). - Secrets — same added-lines-only scope: AWS/GitHub/Slack/Stripe/Google/
Anthropic token prefixes, private key blocks, and a conservative catch-all for a
secret in an unrecognized format (a quoted string literal assigned to a
api_key/secret/token/password-shaped name — bare/unquoted values are a documented gap, too false-positive-prone to catch reliably). - Metadata — any staged file whose extension commonly carries embedded metadata
(jpg/png/heic/tiff/bmp/gif/pdf/docx/xlsx/pptx) is flagged as an advisory. If
Pillow is installed, JPEG/TIFF files additionally get a real EXIF GPS check —
a confirmed
highfinding if GPS data is actually present, not just a type-based guess.
- Direct PII — regex-matched against added lines only (not removed or context
lines): email, phone, SSN (strict
- Or scan something specific instead of the staged diff:
python ${CLAUDE_PLUGIN_ROOT}/scripts/scan_diff.py --file path/to/thing.txt python ${CLAUDE_PLUGIN_ROOT}/scripts/scan_diff.py --commit-msg .git/COMMIT_EDITMSG echo "some text" | python ${CLAUDE_PLUGIN_ROOT}/scripts/scan_diff.py --text - - Report findings in the format the source project specified:
[severity] [class] [finding] reason (location). Add--jsonfor machine-readable output. - Suppress deliberately, two ways — both are for real exceptions the user has
actually reviewed, not a way to make warnings go away:
.privacy-linter-ignoreat the repo root — one glob pattern per line (like.gitignore), for whole files/paths (test fixtures, sample data).- An inline
privacy-linter: ignoremarker on the specific line.
- Gate or advise. Default behavior always exits 0 — advisory only, matching the
source design's "advisory, not blocking by default." Add a gate when the caller
wants one:
Samepython ${CLAUDE_PLUGIN_ROOT}/scripts/scan_diff.py --block-on high # exit 1 if any 'high' finding exists--block-on-as-CI-gate patternagent-eval'sscore_eval.pyalready uses — reused here rather than inventing a new convention. - Strip EXIF metadata outright, instead of just flagging it (JPEG/TIFF only,
requires Pillow):
Writes a separate copy by default — never overwrites the original unlesspython ${CLAUDE_PLUGIN_ROOT}/scripts/scan_diff.py --strip-metadata photo.jpg # -> photo.stripped.jpg python ${CLAUDE_PLUGIN_ROOT}/scripts/scan_diff.py --strip-metadata photo.jpg --out clean.jpg python ${CLAUDE_PLUGIN_ROOT}/scripts/scan_diff.py --strip-metadata photo.jpg --in-place # overwrites photo.jpg--in-placeis passed explicitly — and re-reads the output file's EXIF afterward to confirm the strip actually worked before reporting success. Refuses to silently clobber an existing.strippedfile from a previous run; pass--outfor a different path. - Check whether something was already leaked before this tool existed, not just
what's staged right now:
Walks every non-merge commit on the current branch (oldest first, so findings print in the order a leak was actually introduced), checking each commit's own added lines for Direct PII/Secrets — the same regex scanners as everything above, just pointed at history instead of the staged diff. This is the one check the staged/working-tree view structurally cannot do: a secret committed and later removed frompython ${CLAUDE_PLUGIN_ROOT}/scripts/scan_diff.py --scan-history # entire branch history python ${CLAUDE_PLUGIN_ROOT}/scripts/scan_diff.py --scan-history --max-commits 50 # just the N most recent commitsHEADis still sitting in an old commit unless history itself was rewritten, and--scan- historyis what actually looks there instead of assuming a deleted line is a solved problem. Each finding's location is<short-hash> <path>:<line>—git show <short-hash>to see the exact commit. Does not cover Metadata (would need reading each historical blob's file content, not just its diff — a heavier operation left for a future pass) or merge commits (their content was already introduced by a non-merge ancestor commit, so nothing is missed by skipping them). - Track findings over time instead of only ever seeing one run's snapshot — off by
default, opt in per invocation:
To have the git pre-commit hook log every commit-time scan, install it with the opt-in flag (absolute path; a leadingpython ${CLAUDE_PLUGIN_ROOT}/scripts/scan_diff.py --log-dir ~/.privacy-linter-log # add to any normal run python ${CLAUDE_PLUGIN_ROOT}/scripts/scan_log_history.py --log-dir ~/.privacy-linter-log --out trend.jsonl python ../agent-eval/scripts/score_eval.py trend.jsonl --fail-under 0.9 python ../agent-eval/scripts/score_eval.py trend.jsonl --baseline last_month.jsonl --fail-on-regression~/is expanded once at install time):
Without it the hook does not log, so no run data accumulates for the trend bridge. Ifbash ${CLAUDE_PLUGIN_ROOT}/scripts/install_hook.sh /path/to/repo --log-dir ~/.privacy-linter-logpython3is not onPATHwhere you commit (NixOS, some version-manager shells), the installer refuses to write a hook that would exit 127 and block every commit; pass--runnerwith a command that provides it, for example--runner "nix-shell -p python3 git --run". The runner is written into the hook as-is, so only pass one you trust.--log-dirwrites one timestamped JSON record per run (the findings only, never the scanned content — this log can't become a second copy of whatever triggered a finding).scan_log_history.pyis the bridge, not a second aggregation engine — it reads a window of those records and flattens them intoagent-eval's own{id, score, category, rationale}schema (same "bridge, not reimplementation" patternbroadcast/scripts/qa_gate_history.pyalready established for its own QA gate), soscore_eval.pydoes the actual pass-rate/regression/gate work rather than a second copy of that logic growing here. A scan run has no fixed checklist the way a QA gate does — it's zero or more findings, not a pass/fail per named check — so the "categories"score_eval.pyreports on are derived per run:clean(zero findings of any kind),no_secrets,no_direct_pii,no_high_severity. Over enough runs,score_eval.py's per-category pass rate answers "is my secret-leak rate actually improving," not just "did today's commit have a leak."--scan-historyruns are never logged — they aggregate many historical commits into one call, not comparable to a single pre-commit hook run, and mixing the two would make "clean rate over time" meaningless.
Installing as a git pre-commit hook
${CLAUDE_PLUGIN_ROOT}/scripts/install_hook.sh /path/to/your/repo # blocks on 'high' (default)
${CLAUDE_PLUGIN_ROOT}/scripts/install_hook.sh /path/to/your/repo --block-on medium # blocks on medium or high
${CLAUDE_PLUGIN_ROOT}/scripts/install_hook.sh /path/to/your/repo --block-on none # advisory only, never blocksWrites a pre-commit hook that runs scan_diff.py --block-on high by default —
confirmed as of 2026-09-16: a report-only linter that never actually stops anything is
close to a suggestion box, not a control layer, and the tools this default applies to
(Luhn-validated credit card, matched secret patterns, confirmed EXIF GPS) are exactly
the low-false-positive-rate findings this project's own philosophy says are worth acting
on. Medium/low findings still print but never block, regardless of threshold. Pass
--block-on none for the old fully-advisory behavior. See the script for exactly what
it writes; it never overwrites an existing hook without confirmation.
What's NOT built here
- MCP tool surface. The source design names this as a second surface alongside the git hook. Not implemented in this version — the git-hook surface covers the "highest daily value... moment of creation" case the project itself prioritized. A fast follow, not a silent scope cut.
- Inference cues and stylometric fingerprinting. Genuinely need judgment, not
pattern-matching. See
${CLAUDE_PLUGIN_ROOT}/references/leak-taxonomy.mdfor what "genuinely need a model" means here and what the local-model path would require. - Free-text name detection. Regex can't reliably distinguish a name from an ordinary capitalized word without NER/a model — deliberately not attempted rather than shipped with a high false-positive rate.
- Text redaction / auto-fix for Direct PII or Secrets. Detection only reports;
nothing here rewrites a file or diff. Auto-redacting is a judgment call (did it
redact too much, too little, or corrupt surrounding content) that this tool's own
"low false-positive rate matters more than coverage" philosophy argues against
automating — a human should apply the actual fix.
--strip-metadatais the one exception, because EXIF removal has no such judgment call to get wrong. - Metadata stripping for non-EXIF formats (PNG, HEIC, PDF, DOCX, XLSX, PPTX).
--strip-metadataonly covers JPEG/TIFF via Pillow; the others need a different metadata-writing library each — detection still flags all of them, just not remediation. - Metadata in
--scan-history, and merge commits in--scan-history. History scanning covers Direct PII/Secrets only — a historical metadata check would need each commit's actual file content, not just its diff, real scope beyond this pass. Merge commits are skipped (their content already arrived via a scanned non-merge ancestor). See${CLAUDE_PLUGIN_ROOT}/references/leak-taxonomy.md. - Any presence at the actual point of disclosure (a GitHub PR box or LinkedIn post
composer typed directly in-browser, never touching a file this hook or CLI can see).
A structural property of this whole stack's file-based architecture, not something
fixable in this skill alone — see
privacy-threat-oracle/SKILL.md's "What's NOT built here" for the full writeup and what was considered and deferred.
Output discipline
- Never claim a
pass/no-findings result means "definitely clean" — say what was and wasn't checked (e.g., no findings from pattern-matching doesn't mean no PII; it means no PII shaped like the patterns this version knows). - Report the Pillow-unavailable case explicitly when it affects a result (a flagged JPEG got the weaker file-type heuristic, not the stronger EXIF check) — don't let a degraded check look identical to a full one.
- Findings are advisory by default for a reason (false-positive tolerance matters more
than coverage) — don't recommend
--block-onas a default without the user asking for blocking behavior specifically.
Pairing
- agent-eval —
scan_log_history.pybridges--log-dir's run records intoscore_eval.py's schema for trend tracking (step 8);agent-evaldoes all the actual pass-rate/regression/gate computation, never duplicated here. - wiki-privacy-audit — reuses this skill's
scan_diff.pydirectly (as a subprocess, per vault note) rather than reimplementing detection for a vault instead of a git diff. This skill stays the single source of truth for what counts as PII or a secret.
Files
| Path | What it is |
scripts/scan_diff.py | The scanner — PII/secret regex + metadata heuristics, EXIF stripping, commit-history scanning, run logging, git integration, suppression, CLI gate |
scripts/test_scan_diff.py | Unit + CLI + real-temp-git-repo test suite (stdlib unittest) |
scripts/scan_log_history.py | Bridges --log-dir's run records into agent-eval's JSONL schema for trend tracking |
scripts/test_scan_log_history.py | Unit + CLI test suite, including a real end-to-end run into score_eval.py |
scripts/install_hook.sh | Installs scan_diff.py as a repo's pre-commit hook (blocks on high by default) |
scripts/test_install_hook.py | End-to-end tests: real temp git repo, real installed hook, actually run against staged content |
references/leak-taxonomy.md | Every leak class (the source design's four, plus Secrets), severity rubric and rationale, what's built vs. deferred and why |
examples/ | A worked example: a synthetic diff with seeded PII, and the scan output it produces |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

