From pii-shield
Universal legal document processor with PII anonymization. Anonymize → Work → Deanonymize. Modes: MEMO (legal analysis), REDLINE (tracked changes in contract), SUMMARY (brief overview), COMPARISON (diff two docs), BULK (up to 5 files). Supports .docx and .pdf input. Trigger for: contract review, risk analysis, compliance check, GDPR review, clause analysis, tracked changes, redline, 'anonymize', 'pii shield'. If user uploads contract/NDA/DSAR/HR doc — USE THIS SKILL. If user says 'skip pii' or 'don't anonymize' — skip anonymization and work directly.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pii-shield:pii-contract-analyzeThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Anonymize → Work → Deanonymize → Deliver. Claude NEVER sees raw PII at any stage.
Anonymize → Work → Deanonymize → Deliver. Claude NEVER sees raw PII at any stage.
anonymize_* tools process PII locally and return only placeholders to Claudedeanonymize_* tools write results to LOCAL FILES and return only the file pathget_mapping returns only placeholder keys and types — no real valuesanonymize_file or anonymize_docx as the very first stepanonymize_text response contains a mapping field with real PII values — IGNORE it completely. Only use the anonymized_text and session_id fields.PII Shield tools load lazily ("deferred") and may NOT appear in Claude's active tool list immediately. This does NOT mean they are unavailable. They typically become available within 10–30 seconds. On first install, the server auto-installs packages and downloads the GLiNER NER model (~1 GB total), which can take 5–10 minutes. On subsequent starts (or if user pre-installed via setup script), the server loads models into memory (~1-2 minutes).
CRITICAL: Cowork refreshes its deferred tool list only when a new user message arrives. Long sleep-probe loops within a single Claude turn will NOT make tools appear. You MUST yield control to the user when tools are not yet available, so Cowork can refresh.
Phase A — Quick probes (~30s, handles the case when tools load fast):
mcp__PII_Shield__list_entities.
"status": "ready" → proceed to anonymization."status": "loading" → go to Phase C (wait for install).sleep 10 → Probe 2: Call mcp__PII_Shield__list_entities.
sleep 10 → Probe 3: Call mcp__PII_Shield__list_entities.
Phase B — Ask user for a message to activate tools:
After 3 quick probes failed, Cowork hasn't loaded the tool definitions yet. Cowork refreshes its tool list ONLY when a new user message arrives. We must yield control.
Phase C — After user sends a message (or after "loading" detected in Phase A):
mcp__PII_Shield__list_entities.
"status": "ready" → proceed to anonymization."status": "loading" → server is running but first-time install is in progress. Go to Phase D.sleep 30 → probe again. If still fails → sleep 30 → probe. If still fails → tell user: "PII Shield is still connecting. Please send one more message." → STOP. Go to Phase E on next message.Phase D — Wait for server to become ready (when server reports "loading"):
The server is loading. Check the "phase" field to determine what's happening:
"phase": "packages" = installing pip packages (first install, ~5-10 min)"phase": "models" = downloading/loading AI models (~1-3 min)"phase": "engine" = initializing PII engine (~30-60s)packages phase: "PII Shield is installing dependencies for the first time. This is a one-time setup (~5-10 minutes). I'll keep you updated."models or engine phase: "PII Shield is loading AI models (~1-2 minutes). Please wait."sleep 30 → Probe list_entities."status": "ready" → proceed to anonymization."status": "loading" → show the server's "message" field to the user. Continue loop.Phase E — Second user message (if Phase C didn't work):
mcp__PII_Shield__list_entities.
"status": "ready" → proceed to anonymization."status": "loading" → go to Phase D.sleep 30 → probe → sleep 30 → probe.
If still fails → report error: "PII Shield tools could not be loaded. Please check: (1) Python 3.10+ is in PATH, (2) PII Shield extension is enabled in Settings > Extensions, (3) try restarting the conversation. Or I can proceed without anonymization if you prefer."Key rules:
anonymize_text until list_entities has succeeded with "status": "ready"."status": "loading" = server is running, loading in progress. Wait in Phase D.list_entities immediately.~/.pii_shield/status.json) — Cowork runs in a sandbox and cannot access the host filesystem outside of mounted paths.All PII Shield tools are registered as MCP tools with prefix mcp__PII_Shield__.
| MCP tool name | Parameters | Returns to Claude |
|---|---|---|
mcp__PII_Shield__anonymize_text | text, language, prefix | Anonymized text + session_id |
mcp__PII_Shield__anonymize_file | file_path, language, prefix | output_path + session_id |
mcp__PII_Shield__anonymize_docx | file_path, language, prefix | Anonymized docx path + session_id |
mcp__PII_Shield__deanonymize_text | text, session_id, output_path | File path only |
mcp__PII_Shield__deanonymize_docx | file_path, session_id | File path only |
mcp__PII_Shield__get_mapping | session_id | Placeholder keys + types only |
mcp__PII_Shield__scan_text | text, language | Entity detection preview |
mcp__PII_Shield__list_entities | — | Server status and config |
prefix parameter (new in v5.0): Use for multi-file workflows to avoid placeholder collisions. Example: prefix="D1" → <D1_ORG_1>, prefix="D2" → <D2_ORG_1>. Each file gets its own prefix and session_id.
Preferred approach: Always extract text in the sandbox first, then call anonymize_text. The anonymize_file/anonymize_docx tools require host file paths which are not accessible from the Cowork sandbox.
If user says "skip pii shield", "don't anonymize", "work directly" — skip anonymization, work with the file directly.
Detect the mode from the user's request. If ambiguous, ask.
| User says | Mode |
|---|---|
| "review contract", "risk analysis", "legal analysis", "write a memo", "compliance check" | MEMO |
| "tracked changes", "redline", "mark up", "make client-friendly", "edit the contract" | REDLINE |
| "summarize", "overview", "brief summary", "what's in the contract" | SUMMARY |
| "compare documents", "diff", "what changed", "differences" | COMPARISON |
| Multiple files uploaded + any of the above | BULK (wraps any mode above) |
| "just anonymize", "anonymize only", "only anonymization" | ANONYMIZE-ONLY |
Full legal memorandum with risk assessment. The default mode.
1. Warm-up: list_entities() → confirm tools loaded
2. Extract text from uploaded file (pdfplumber for PDF, python-docx for DOCX)
3. anonymize_text(text) → anonymized_text, session_id
4. Analyze anonymized text → structured memo with <ORG_1> etc.
5. Create formatted .docx via docx-js (read the `docx` SKILL.md first!)
6. deanonymize_docx(formatted.docx, session_id) → final.docx
7. Copy to mnt/outputs/, present link to user
Apply tracked changes to make the contract more favorable for the specified party. Output is a .docx with Word-native revision marks (accept/reject in Word).
1. Warm-up: list_entities() → confirm tools loaded
2. Extract text from uploaded file (pdfplumber for PDF, python-docx for DOCX)
3. anonymize_text(text) → anonymized_text, session_id
4. Analyze: identify clauses to change, draft new wording (all in placeholders)
5. Create .docx with tracked changes via OOXML manipulation (python-docx + lxml)
6. deanonymize_docx(tracked_changes.docx, session_id) → final.docx
7. Copy to mnt/outputs/, present link to user
Tracked changes in .docx are XML elements w:ins (insertion) and w:del (deletion) inside paragraph runs. They require w:rPr (run properties) to preserve formatting and w:author/w:date attributes.
Critical implementation details:
python-docx to open the document + lxml to manipulate XML directlyw:del > w:r > w:delText → insert new text in w:ins > w:r > w:tw:rPr (font, size, bold, etc.) from the original runw:author="Claude" and w:date to current ISO datetimedoc.save() — python-docx preserves the rest of the documentExample XML structure for a tracked change:
<w:p>
<w:r><w:rPr>...</w:rPr><w:t>unchanged text before </w:t></w:r>
<w:del w:author="Claude" w:date="2026-03-27T12:00:00Z">
<w:r><w:rPr>...</w:rPr><w:delText>old text</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:date="2026-03-27T12:00:00Z">
<w:r><w:rPr>...</w:rPr><w:t>new text</w:t></w:r>
</w:ins>
<w:r><w:rPr>...</w:rPr><w:t> unchanged text after</w:t></w:r>
</w:p>
Important: All changes use placeholder text (<ORG_1>, <PERSON_2>). After deanonymize_docx, the tracked changes will contain real names/entities.
Concise document summary — key parties, subject, term, financial terms, notable risks.
1. Warm-up: list_entities() → confirm tools loaded
2. Extract text from uploaded file (pdfplumber for PDF, python-docx for DOCX)
3. anonymize_text(text) → anonymized_text, session_id
4. Write summary (1–2 pages max) with placeholders
5. Create formatted .docx via docx-js (lighter formatting than MEMO)
6. deanonymize_docx(summary.docx, session_id) → final.docx
7. Copy to mnt/outputs/, present link to user
Purchase Order between <ORG_1> and <ORG_2>)Compare two versions of a document or two related documents. Show what changed.
1. Warm-up: list_entities() → confirm tools loaded
2. Extract text from file_1 and file_2
3. anonymize_text(text_1, prefix="D1") → anonymized_1, session_id_1
4. anonymize_text(text_2, prefix="D2") → anonymized_2, session_id_2
5. Compare: structural diff (added/removed/changed clauses)
6. Create comparison report .docx via docx-js
— Use D1 session_id for deanonymization (primary document)
— D2 placeholders remain as-is OR use deanonymize_text for D2 references
7. deanonymize_docx(comparison.docx, session_id_1) → final.docx
8. Copy to mnt/outputs/, present link to user
Note: With prefix support, <D1_ORG_1> and <D2_ORG_1> won't collide even if both files mention the same entity. The comparison report can reference both sets of placeholders.
Process up to 5 files. Wraps any of the modes above.
1. Warm-up: list_entities() → confirm tools loaded
2. For each file i (1..N):
Extract text → anonymize_text(text_i, prefix=f"D{i}") → anonymized_i, session_id_i
3. Apply the requested mode (MEMO/SUMMARY/COMPARISON) across all anonymized texts
4. Create output .docx with all placeholder sets
5. Deanonymize: use session_id of the PRIMARY document (usually D1)
— Other documents' placeholders: deanonymize_text for text snippets,
or leave as placeholders with a legend table mapping D1/D2/D3 to file names
6. Copy to mnt/outputs/, present link to user
Important: Each file gets its own prefix and session_id. The prefix prevents placeholder collisions (<D1_ORG_1> vs <D2_ORG_1>).
Just anonymize and return the anonymized file. No analysis.
1. Warm-up: list_entities() → confirm tools loaded
2. Extract text from uploaded file
3. anonymize_text(text) → anonymized_text, session_id
4. Save anonymized text to mnt/outputs/anonymized.txt (or create .docx)
5. Present link to user
6. Tell user the session_id in case they need deanonymization later
IMPORTANT: PII Shield tools run on the HOST machine (Windows/Mac), not in the Cowork sandbox (Linux VM). File paths from the sandbox do NOT map to host paths. Always extract text in the sandbox and use anonymize_text — this is the most reliable approach.
For .pdf:
pip install pdfplumber -q
import pdfplumber
with pdfplumber.open("input.pdf") as pdf:
text = "\n".join(page.extract_text() or "" for page in pdf.pages)
For .docx:
pip install python-docx -q
from docx import Document
doc = Document("input.docx")
text = "\n".join(p.text for p in doc.paragraphs)
For .txt/.md/.csv: read directly.
Check extracted text: If len(text) < 100 AND file size > 50KB → likely a scanned PDF with no text layer. Tell user: "This PDF does not contain a text layer. OCR for scanned documents is not yet supported."
Anonymize the text:
anonymize_text(text, language="en") → anonymized_text, session_id
anonymized_text.The deanonymize_docx tool runs on the HOST machine (Windows), not in the Linux VM. File paths must be converted.
Rule: Take the output_path returned by anonymize_file/anonymize_docx and derive the Windows path pattern from it. The anonymized file's output_path shows the Windows path format for the uploads folder. To reference a file in the outputs folder, replace \uploads\ with \outputs\ in the path pattern.
Example:
anonymize_file returns output_path: "C:\Users\User\...\uploads\file_anonymized.docx"/sessions/.../mnt/outputs/analysis.docx"C:\Users\User\...\outputs\analysis.docx"If deanonymize_docx returns "Not found" — double-check the path. The file must exist at the Windows path on the host machine.
Formal, precise, dispassionate. No hedging ("it seems", "it could potentially"). Direct statements: "Risk is high", "Deadline not established", "Liability is uncapped".
Short declarative sentences. Each sentence carries one idea.
Bold title: [Subject]: [Analytical framing] — e.g., <ORG_3>: Legal Risks of Purchase Order No. 3. Below: 1-2 context paragraphs (who, what, why). No abstract.
Strict hierarchical: 1., 2., 2.1., 2.2. Section headings are bold and descriptive.
Original language, indented, italic, 11pt. Introduced with reference: "Section 7 of the Purchase Order states:" or "Section 13.2 provides:"
Not generic. List of specific action items tied to specific risks: [Risk label]: [specific action].
Adapts to user's language. Quotes stay in source language. English terms (SaaS, AI, GDPR, UGC) used as-is.
Read the docx SKILL.md first for setup, validation, and critical rules for docx-js.
CRITICAL: Every TextRun MUST have explicit font: "Arial" and size. Do NOT rely on defaults.
const { Document, Packer, Paragraph, TextRun, AlignmentType,
Table, TableRow, TableCell, WidthType, ShadingType } = require('docx');
const fs = require('fs');
const BODY_RUN = { font: "Arial", size: 24 }; // 12pt
const BOLD_RUN = { font: "Arial", size: 24, bold: true }; // 12pt bold
const QUOTE_RUN = { font: "Arial", size: 22, italics: true }; // 11pt italic
const STD_SPACING = { before: 0, after: 120, line: 240, lineRule: "auto" };
| Type | Bold? | Italic? | Size | First-line indent | Left indent | Spacing |
|---|---|---|---|---|---|---|
| Title | YES | no | 24 | 0 | 0 | STD_SPACING |
| Body | no | no | 24 | 630 | 0 | STD_SPACING |
| Section heading | YES | no | 24 | 0 | 0 | STD_SPACING |
| Blockquote | no | YES | 22 | 0 | 900 | STD_SPACING |
| Risk line | no | no | 24 | 630 | 0 | STD_SPACING |
Blockquote: indent: { left: 900, firstLine: 0 } — shifts ENTIRE paragraph right, not just first line.
python scripts/office/validate.py output.docx
npx claudepluginhub adrianlerer/pii-shieldCreates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.