This guide walks you through installing ShieldPrompt, wiring it into Claude Code, and using it from the CLI or Python.
Contents
- Setup Claude Code
- Install the package
- CLI usage
- Python library
- FastAPI middleware
- What ShieldPrompt detects
- FAQ & gotchas
ShieldPrompt includes an MCP helper, not a pre-send privacy filter. A model sees
its prompt before it invokes a tool. Do not paste secrets into Claude and expect
shield_mask to hide them retroactively. Use the local CLI or Python library
before text enters any model context; review masked output before sending.
| What | Check with | If missing |
|---|---|---|
| Python 3.9 or newer | python3 --version |
Install from https://www.python.org/downloads |
| Claude Code CLI | claude --version |
Install from https://docs.claude.com/en/docs/claude-code |
pip install shieldpromptInstall the spaCy model explicitly before enabling contextual detection:
python -m spacy download en_core_web_smShieldPrompt does not install model packages during a request. Missing models cause a clear failure rather than silently reducing privacy protection.
Run this one command — it writes the required JSON into Claude Code's config for you, no manual editing:
claude mcp add shieldprompt -- python -m shieldprompt.mcp_serverThat's it. Verify it was added:
claude mcp listYou should see shieldprompt in the output.
Prefer to edit JSON yourself? Add this block to your Claude Code settings (
~/.claude.jsonfor user-scope, or.mcp.jsonin your project for project-scope):{ "mcpServers": { "shieldprompt": { "command": "python", "args": ["-m", "shieldprompt.mcp_server"] } } }
Close and reopen your Claude Code session. The ShieldPrompt tools will
now be available: shield_mask, shield_inspect, shield_clear.
Try MCP only with synthetic, non-sensitive examples. It does not automatically protect a conversation, intercept files, or mask prompts before Claude receives them. Inspection returns entity types and positions, never original values. The MCP server deliberately does not expose unmask or vault-export tools.
For real data, run masking in a local terminal first:
shieldprompt mask --file input.txt --save-vault private.vault.json > masked.txtReview masked.txt, then send only that file's contents to the model. Keep
input.txt and private.vault.json out of the model's accessible files, prompts,
and tools. Restore the response locally, not through a model-visible tool:
shieldprompt unmask --file response.txt --vault private.vault.jsonDetection is best effort: even a functioning detector can miss sensitive values. Do not treat masked output as a guarantee that no personal data remains.
pip install shieldpromptOptional extras (install only if you need them):
| Extra | Command | What it adds |
|---|---|---|
ner |
pip install "shieldprompt[ner]" |
HuggingFace transformer NER as a fallback/alternative to Presidio. |
fastapi |
pip install "shieldprompt[fastapi]" |
The FastAPI / Starlette middleware. |
all |
pip install "shieldprompt[all]" |
Everything. |
The core install already includes Presidio + spaCy, so name/organization/ location detection works out of the box.
Three commands are installed: shieldprompt (full), pii (shortcut for
masking), retrace (shortcut for unmasking).
# --- masking ---
pii "My email is alice@example.com" # one-liner
echo "Call +1-415-555-1234" | pii # from stdin
pii --file notes.txt --in-place # file in place (+ sidecar vault)
pii "text" --save-vault my_vault.json # explicit vault path
pii "text" --entities EMAIL_ADDRESS CREDIT_CARD # only these types
pii "text" --no-ner # regex-only, skip Presidio
# --- unmasking ---
retrace "Hi [EMAIL_ADDRESS_1]" --vault my_vault.json # explicit vault
retrace --file notes.txt --in-place # uses sidecar automatically
# --- inspect what would be detected (no masking) ---
shieldprompt inspect "Alice works at alice@acme.com"
# --- dump a vault file for debugging ---
shieldprompt map --vault my_vault.json
shieldprompt map --file notes.txt --jsonExample: mask a file and unmask it back.
# starting file
echo "Email alice@example.com about the invoice." > notes.txt
# mask in place (writes notes.txt + notes.txt.shieldprompt.vault.json)
pii --file notes.txt --in-place
cat notes.txt
# → Email [EMAIL_ADDRESS_1] about the invoice.
# unmask using the sidecar vault automatically
retrace --file notes.txt --in-place
cat notes.txt
# → Email alice@example.com about the invoice.Vault files are written with 0600 permissions (owner-only). Treat
them like password files — anyone who reads the vault can re-identify
every redaction.
from shieldprompt import Shield
shield = Shield()
masked = shield.mask("Email alice@example.com or call +1-415-555-1234")
# → "Email [EMAIL_ADDRESS_1] or call [PHONE_NUMBER_1]"
original = shield.unmask(masked)
# → "Email alice@example.com or call +1-415-555-1234"Pick specific entity types:
from shieldprompt import Shield
from shieldprompt.entities import EntityType
shield = Shield(entities={EntityType.EMAIL_ADDRESS, EntityType.CREDIT_CARD})Wrap an LLM call with the decorator:
from shieldprompt import mask_pii
from openai import OpenAI
client = OpenAI()
@mask_pii(entities=["EMAIL_ADDRESS", "PHONE_NUMBER", "PERSON"])
def ask_llm(prompt: str) -> str:
reply = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
)
return reply.choices[0].message.content
answer = ask_llm("Draft a reply to alice@example.com about Friday.")
# `answer` still mentions alice@example.com — the model never saw it.Works with async def too — the decorator detects sync vs async
automatically.
pip install "shieldprompt[fastapi]"from fastapi import FastAPI
from shieldprompt.middleware import ShieldPromptMiddleware
app = FastAPI()
app.add_middleware(
ShieldPromptMiddleware,
sensitivity="high", # low | medium | high
exclude_paths=["/health"],
)
@app.post("/chat")
async def chat(payload: dict):
# payload["prompt"] is already masked here
reply = call_your_llm(payload["prompt"])
return {"response": reply}
# the JSON response is unmasked before the client sees itEach request gets its own vault, so data never leaks across users.
Regex-based (fast, deterministic, always on):
EMAIL_ADDRESSPHONE_NUMBERCREDIT_CARD(Luhn-validated)SSNIP_ADDRESSIBANURLAWS_KEYAPI_KEY(OpenAI, GitHub tokens)DATE_OF_BIRTH(common numeric date forms; not contextual birth-date inference)MONEY(common currency-symbol/code numeric amounts)
NER-based (Presidio + spaCy, contextual):
PERSONORGANIZATIONLOCATIONDATE
Q: Is the vault safe?
The vault is plaintext JSON on your disk. ShieldPrompt writes it with
0600 permissions, but anyone with file access can re-identify every
redaction. Never commit it to git, never send it to the LLM, clean it up
when you're done.
Q: Do placeholder numbers stay consistent across runs?
No. Each Shield instance starts counting from 1. Always unmask with the
vault produced by the same masking run — don't stitch vaults from
different processes together.
Q: Will the LLM get confused by placeholders?
Modern models handle [EMAIL_ADDRESS_1]-style tokens well. For best
results add a system prompt hint: "Placeholders like [EMAIL_ADDRESS_1]
refer to real values; keep them unchanged in your response."
Q: What if the model hallucinates a placeholder I never gave it?
That placeholder isn't in the vault, so unmask leaves it as text.
Nothing crashes — but your user will see a literal [EMAIL_ADDRESS_99].
Worth post-processing if it matters.
Q: Does it slow things down? Regex is microseconds per KB. Presidio + spaCy loads once (a couple of seconds) then runs fast. Network calls to the LLM dwarf both.
- NER initialization or inference failure raises an error instead of returning
apparently protected text.
--no-ner/use_ner=Falseexplicitly selects regex-only protection; names and locations are not covered in that mode. - Vault outputs are created owner-only and existing vault files are not silently overwritten. Choose a new vault path for each masking operation. In-place masking saves the recovery vault before replacing the source; repeated masking of tokenized files is refused. Keep the vault until restoration is verified.
- Never use the source path as the vault path. Vaults contain plaintext secrets: filesystem permissions do not replace encryption or appropriate file isolation.
- Streaming responses are forwarded with placeholders intact. Restore them in a trusted local client; automatic restoration applies only to bounded, complete, uncompressed JSON/text responses (default limit 1 MiB). Oversized and other response types retain tokens. Requests over the configurable 1 MiB default limit are rejected with HTTP 413. JSON responses are restored as structured values, then serialized.
- The decorator supports string arguments, including variadic wrappers, and recursively restores strings in list/dict responses. SDK-specific objects require a caller-owned adapter.