Install
$ agentstack add skill-gregmos-pii-shield-pii-contract-analyze ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
⚡ YOUR FIRST ACTION
When the user invokes /pii-contract-analyze , you respond in TWO turns.
Turn 1 — acknowledge and wait
Do NOT call any tool. Do NOT read files. Do NOT run Bash. Reply with one short line and stop:
> Ready to start. Type go or continue to proceed.
Wait for the user's next turn. The MCP deferred-tools registry is often not populated yet on turn 1; it lazy-loads between turns.
Turn 2 — discover and proceed
When the user replies with any continue signal (go, continue, yes, ok, proceed, or equivalents in their language), silently run this discovery sequence:
ToolSearch(query: "select:mcp__PII_Shield_v2__list_entities", max_results: 1)- If that returns "No matching deferred tools found":
ToolSearch(query: "select:mcp__pii-shield__list_entities", max_results: 1) - If that also fails:
ToolSearch(query: "select:mcp__plugin_pii-shield_pii-shield__list_entities", max_results: 1)
If any of them loads a schema → immediately call list_entities and continue with the Startup procedure, mode detection, and pipeline below. Do NOT surface these attempts to the user.
If all three fail → show the user:
> PII Shield MCP tools are installed on your host (Claude Desktop) but this session can't reach them. Known Anthropic bridging bug on Windows. Fixes: (1) restart Claude Desktop and start a fresh session, or (2) install pii-shield-v2.0.1-plugin.zip directly into this Cowork session. Meanwhile I can proceed without PII anonymization — OK?
Rules
- Never call ToolSearch on turn 1. The prompt "type go" is the whole turn-1 response.
- Never fuzzy-search with bare keywords (
"list_entities","pii-shield") — underscore names don't match as substrings on Cowork CLI. - Never declare the plugin missing before turn-2's full three-attempt
select:chain has run. - Never spawn sub-agents, grep the codebase, or probe filesystem paths / localhost ports / beacon files hunting for the server. If MCP tool discovery fails, the three
select:attempts above are the whole fallback chain; anything beyond them is off-limits.
PII Shield — Universal Legal Document Processor
Anonymize → Work → Deanonymize → Deliver. Claude NEVER sees raw PII at any stage.
CRITICAL: PII never flows through Claude
File handling: The user must connect a folder (not attach the file directly to the message). When a file is attached to a chat message, its content is rendered and sent to the API as part of the prompt — Claude sees the raw data before PII Shield can process it. When a folder is connected, Claude only sees the file path and calls anonymize_file(path) — the MCP server reads and anonymizes the file locally. PII never enters Claude's context.
If the user attaches a file directly: Warn them politely: "For full PII protection, please connect the folder containing your document instead of attaching it directly. When a file is attached to a message, its content is included in the API request before PII Shield can anonymize it. I can still process it, but the privacy guarantee is stronger when you connect the folder."
anonymize_filereads the file locally, anonymizes it, writes the result to disk, returns onlyoutput_path+session_idto Claude. After HITL is approved, and only then, Claude reads the anonymized text from the output file — never before.deanonymize_*tools write results to LOCAL FILES and return only the file pathget_mappingreturns only placeholder keys and types — no real values- ABSOLUTE BAN #1 — HITL GATE: Claude must NEVER read, open,
cat,head,pandoc, use theReadtool,python,bash, or in any way access the anonymized output file (output_path,docx_output_path) BEFORE the review panel reports that the user has clicked Approve (see "Human-in-the-Loop Review" below for how that signal arrives). Not to "preview entity quality", not to "verify placeholders", not to "check formatting", not to "plan the memo" — NEVER. The anonymized file is considered SEALED betweenanonymize_fileand HITL approval. The HITL reviewer is the human, not Claude. - ABSOLUTE BAN #2 — DEANONYMIZED FILES: Claude must NEVER read, open, cat, head, pandoc, or in any way access the content of deanonymized/restored files. Not to "verify", not to "check formatting", not to "validate" — NEVER. These files contain real PII. Just give the user the file path and STOP. Any "verification" of deanonymized output is a PII leak.
- Claude must NEVER read the source file (via Read tool, pandoc, python, bash, etc.) BEFORE or INSTEAD OF anonymization — always use
anonymize_file(path)first - If an anonymize tool times out or fails with a NON-"tool not found" error — retry once. If it still fails, tell the user PII Shield is unavailable and ask whether to proceed without anonymization or abort. NEVER fall back to reading the raw file.
- NEVER use
anonymize_textorscan_text— these take raw text as input which means PII passes through the API. The ONLY exception is if the user explicitly pastes text into the chat (PII is already in the conversation).
Startup
PII Shield is a pure-Node.js MCP server — no Python dependency, instant startup. On first run, the NER model (~665 MB fp32 ONNX GLiNER) and its runtime deps (onnxruntime-node, @xenova/transformers, gliner) download into ${CLAUDE_PLUGIN_DATA}/models and ${CLAUDE_PLUGIN_DATA}/deps. This takes 2–5 minutes once per plugin install and is cached for the full life of the plugin (survives host restarts, only wiped by /plugin remove).
⛔ ABSOLUTE RULE — NO SUB-AGENT DELEGATION
NEVER delegate PII Shield tool calls to a sub-agent. Not to a general-purpose agent, not to a Task agent, not to an Explore agent — NEVER. Sub-agents do not stream text to the user; they return one final message only when they exit. If PII Shield is initializing, a sub-agent will poll silently for minutes while the user sees nothing. This is the single worst UX failure mode. All PII Shield tool calls (list_entities, anonymize_file, start_review, etc.) MUST happen in the MAIN conversation.
If you cannot call a PII Shield tool because it shows "No such tool available" — the fix is the turn-1 / turn-2 pattern above (prompt for go, discover silently on turn 2), NOT a sub-agent.
Startup procedure
- Call
list_entities— this happens on turn 2 after the user sends a continue signal. See the YOUR FIRST ACTION block at the top of this skill for the exact two-turn flow. You MUST havelist_entitiesresponding before proceeding. - Identify the file(s) to process and determine the mode (MEMO, REDLINE, etc.)
- Read the
list_entitiesresponse to check NER status
- If
"ner_ready": true— proceed toanonymize_file - If
"ner_ready": false— NER is still initializing. The response includesphase(installing_deps/downloading_model/loading_model),progress_pct, a humanmessage, and a pre-formatteduser_messagefield. If the response ALSO contains afirst_run_noticefield (only present on the very first loading response per server process), printfirst_run_noticeVERBATIM to the user as a plain chat message BEFORE anything else. It explains where the ~700 MB NER cache will live and why the next session will be instant — the user needs to see this once, up front. Subsequent polls will NOT containfirst_run_notice. On every poll (including the first), print theuser_messagefield VERBATIM to the user as a plain chat message BEFORE callinglist_entitiesagain. This is the ONLY thing the user sees during the wait — do not paraphrase, do not summarize, do not skip it, do not batch it silently. Wait and retry: the server enforces a ~20 second throttle by holding thelist_entitiesresponse for 20 s internally whilephaseisinstalling_deps/downloading_model/loading_model. First run may take 2–5 minutes. Between polls (inside the 20 s window) you MAY do useful prep work in the MAIN conversation only — read skill references, plan the analysis. Do NOT delegate any of this to a sub-agent (see the ABSOLUTE RULE above). Do NOT callanonymize_fileuntilner_ready: true— without NER, only regex patterns work, missing PERSON/ORGANIZATION/LOCATION entities. - If
"ner_error"field present — show it to the user. If"ner_error_suggestions"array is also present (platform-specific recovery steps like "install VC++ Redistributable", "switch to Node 22 LTS"), print each entry verbatim as a bulleted list — these are the concrete actions the user should try next. If"ner_error_diagnostic"object is present, itslikely_causefield is a one-word root-cause tag useful to include in any bug report the user may file.
Long document handling (chunked processing)
For documents >15K characters, anonymize_file returns "status": "chunked". Chunked processing flow:
anonymize_file(path)returnssession_id,total_chunks,processed_chunks: 1- Loop: call
anonymize_next_chunk(session_id)untilstatusis"complete"— show "Anonymizing... [chunk X/Y]" - Call
get_full_anonymized_text(session_id)to finalize — returnsoutput_path,session_id,output_dir - Continue with the normal pipeline using the returned values
For short documents (")` directly — no ceremony. The server auto-resolves:
- The path as-given (if it's a valid absolute host path it just works)
$PII_WORK_DIR/if that env is set- BFS (depth 4) of
~/Downloads,~/Documents,~/Desktop,$PII_WORK_DIRfor an unambiguous match
If the response is status: "error" with a "file not found" or "ambiguous filename" hint — the file is in a non-standard location. Fall back to:
# create a marker next to the target file
touch "/path/visible/to/you/.pii_marker_abc"
# then:
resolve_path(filename: "", marker: ".pii_marker_abc")
# take host_path from the response and retry:
anonymize_file(file_path: "")
The marker+resolve_path tools stay available as a reliability net — the auto-BFS handles ~95% of cases, marker covers the rest.
Available MCP tools
| Tool name (suffix) | Parameters | Returns to Claude | |---|---|---| | anonymize_file | filepath, language, prefix, sessionid, reviewsessionid | outputpath (.txt) + sessionid + docid + poolsize + documentsinsession + outputdir + docxoutputpath (.docx, for .docx input only). For long docs: returns status: "chunked" with sessionid and totalchunks. | | anonymize_next_chunk | sessionid | Progress: processedchunks, totalchunks, progresspct, entitiessofar | | get_full_anonymized_text | sessionid | outputpath, sessionid, outputdir, docxoutputpath (same as anonymizefile) | | resolve_path | filename, marker | absolute path + parent dir (fallback when auto-BFS in anonymize_file can't find the file — user-drops-marker-next-to-file ritual) | | deanonymize_text | text, sessionid, outputpath | File path only (takes anonymized text, writes deanonymized file) | | deanonymize_docx | filepath, sessionid? | File path only. If session_id is omitted, server reads pii_shield.session_id from the input .docx's docProps/custom.xml — works across chats/sessions without needing to pass sessionid manually. | | get_mapping | sessionid | Placeholder keys + types only | | list_entities | — | Server status and config | | find_file | filename | Full host path(s) — searches configured workdir only (fallback) | | start_review | sessionid | Opens the review panel in the chat (MCP Apps iframe). No URL, no browser. | | apply_review_overrides | sessionid, overrides | Called automatically by the review panel when the user clicks Approve. Claude does NOT call this directly. | | apply_tracked_changes | filepath, changes (JSON), author | Output .docx with Word-native w:del/w:ins revision marks | | export_session | sessionid, passphrase, outputpath | {archive_path, archive_size_bytes} — encrypted .pii-session archive for team handoff. | | import_session | archive_path, passphrase, overwrite? | {session_id, overwritten, document_count, had_review} — restores a session's mapping locally after receiving an archive from a colleague. |
DO NOT USE these tools (they exist on the server but must not be called for file workflows):
anonymize_text— sends raw text through the API. Only acceptable if user pasted text into chat.scan_text— sends raw text through the API.anonymize_docx— useanonymize_fileinstead (handles .docx automatically).
prefix parameter: Optional per-doc label WITHIN a shared session. Example: prefix="D1" prepends to placeholders as ``. Use it only when the user explicitly wants to visually distinguish placeholders from different documents inside the SAME matter (power-user case: "party A track" vs "party B track"). The default behaviour — no prefix — is recommended; identical entities across files in one session will coalesce into the same placeholder automatically.
session_id parameter (multi-file workflow): Pass the session_id from a previous anonymize_file call to ADD the new document to the same session. Identical entities across files in the session share the same placeholder (e.g. Acme Corp. becomes ` in every file). The response includes the same sessionid, a fresh docid, and pool_size (running count of unique entities). **This is the default in ALL modes (MEMO, REDLINE, SUMMARY, COMPARISON, BULK, ANONYMIZE-ONLY) when the user uploads N≥2 files and confirms they're part of one matter** — see references/bulk-mode.md "One matter" pipeline for the full step list. For unrelated files across separate matters, omit this parameter and use prefix="D{i}"` instead.
review_session_id parameter: Pass the session_id from a previous anonymize_file call after HITL review. The server fetches the user's overrides internally and re-anonymizes. PII never passes through Claude.
Preferred approach: Always use anonymize_file(file_path) — only the file path (not content) passes through the API. The server auto-resolves paths via BFS of common user dirs, so passing a filename or the absolute path the user mentioned is fine. Fall back to resolve_path(filename, marker) or find_file(filename) only if the auto-resolve returns a not_found / ambiguous error.
Skip mode
If user says "skip pii shield", "don't anonymize", "work directly" — skip anonymization, work with the file directly.
Continuing in a later session (cross-chat deanonymize)
PII Shield v2.1+ embeds pii_shield.session_id into the docProps/custom.xml of every emitted _anonymized.docx. This makes the file self-describing: the server can recover the sessionid without Claude holding it in context. If the session has multiple documents (a "one matter" multi-file session), EVERY file in the session carries the SAME sessionid in custom.xml, and the shared mapping covers all of them — deanonymize_docx on any one file restores every placeholder in it from that matter's pool.
When the user returns in a new chat with an anonymized document and asks to restore PII (e.g. "deanonymize this", "give me the PII version", "restore my memo"):
- Ask for (or accept) the file:
.docxfiles carry their sessionid internally..txt/.pdffiles don't — for those, the user must either pass the sessionid or show you a parent anonymized.docx. - Call
deanonymize_docx(file_path: "")— nosession_idargument needed for .docx with embedded metadata. - Server reads
docProps/custom.xml→ finds session_id → loads mapping from~/.pii-shield/mappings/→ returnsrestored_path. - If response contains
"session_id_source": "custom_xml"— tell the user the file was self-identifying (bonus clarity). - If response is an error like
Mapping not found for session 'X', the mapping was cleaned up (TTL, or it was created on another machine). Ask t
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: gregmos
- Source: gregmos/PII-Shield
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.