And all of this cost 2 hours of dialogue with Hermes and tokens worth 20 cents.
name: pii-guard
description: PII Guard Plugin Usage — prevents accidental leakage of personal data to LLM providers
version: 1.0.0
author: Hermes Agent
tags: [security, privacy, pii, datenschutz]
PII Guard Skill
Overview
The PII Guard system prevents accidental transmission of Personally Identifiable Information (PII) to the LLM provider (DeepSeek).
Two components:
- pii_guard plugin (
~/.hermes/plugins/pii_guard/) — active Python code that runs every session
- This skill — documentation of how to use it
Architecture
| Layer |
Mechanism |
Strength |
| Memory Rule |
Permanent instruction in system prompt |
Behavioral — agent follows it |
| Plugin: pre_llm_call |
Privacy reminder injected every turn |
Reminder — not enforced |
| Plugin: pii_approve |
Auditable release tool |
Audit trail — not enforced |
| PII Scanner + Redactor |
Regex detection + local sanitization |
Local functions, agent can use |
What the Plugin Does (Invisible)
- On every turn, injects a privacy reminder into the conversation context
- Provides the
pii_approve tool for documented data release
- Maintains an audit log of all PII decisions
How to Use
Normal operation (PII is blocked)
When the agent reads files or data containing PII, it must:
- Redact or anonymize before including in responses
- Only provide aggregates, placeholders, or anonymized summaries
- Refuse to quote raw PII values
Explicit approval (PII may be sent)
If the user explicitly requests PII transmission:
- User says “send it anyway” or similar
- Agent calls
pii_approve with PII types, values, and reason
- Tool logs to audit trail and confirms
- Agent may now include PII in its response
PII Detection
| Type |
Example |
Pattern note |
| Email |
user@domain.com |
[a-zA-Z0-9._%+-]+@... |
| Phone |
+49 170 1234567 |
Requires separator between digit groups; avoids matching pure digit runs |
| Birthdate (DE) |
01.01.1990 |
Dot, slash, or dash separated |
| Birthdate (US) |
January 15, 1990 |
Long and short month names |
| Address (DE) |
Musterstraße 42, 10115 Berlin |
Street + Nr + optional PLZ + multi-word city |
| PLZ/Ort (standalone) |
10115 Berlin |
Detects PLZ+city separately when not attached to a street |
| Name with Title |
Herr Müller, Dr. Weber |
Herr/Frau/Dr./Prof + capitalized surname |
| IBAN |
DE89 3704 0044 0532 0130 00 |
2 letters + 20+ digits, with or without spaces |
| German Insurance |
T123456789 |
Format: 2-3 digits + 1-2 letters + 8-12 digits |
Complete Regex Patterns
Below are the actual regex patterns used in PII_PATTERNS (from __init__.py):
Email
("email", "E-Mail-Adresse", re.compile(
r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', re.IGNORECASE
)),
Phone
("phone", "Telefonnummer", re.compile(
r'(?:\+\d{1,3}[\s-]?)?(?:\(?\d{2,5}\)?[\s-]?\d{2,7}[\s-]?\d{2,7}(?:[\s-]?\d{1,4})?|\(?\d{2,5}\)?[\s-]\d{2,7}[\s-]?\d{2,7}(?:[\s-]?\d{1,4})?)\b',
)),
Birthdate (DE)
("birthdate_de", "Geburtsdatum (DE)", re.compile(
r'\b\d{2}[./-]\d{2}[./-]\d{2,4}\b'
)),
Birthdate (US)
("birthdate_us", "Geburtsdatum (US)", re.compile(
r'\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|'
r'Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)'
r'\s+\d{1,2},?\s+\d{4}\b'
)),
Address (DE) — with optional PLZ + city
("address_de", "Adresse (DE)", re.compile(
r'\b[A-Za-zäöüßÄÖÜ][a-zäöüß]+(?:str(?:aße|\.)?|weg|platz|allee|ring|damm|gasse|'
r'graben|hof|park|steig|winkel)\s+\d+[a-z]?(?:\s*[,/]?\s*\d{5}\s+(?:[A-Z][a-zäöüß]+[\s-]?)+)?\b',
re.IGNORECASE
)),
PLZ/Ort (standalone — no street context)
("zip_city", "PLZ/Ort", re.compile(
r'\b\d{5}\s+(?:[A-Z][a-zäöüß]+|[a-zäöüß]{2,5}\s)+(?:[A-Z][a-zäöüß]+[\s-]?)*\b',
)),
Name with Title
("name_prefix", "Name (Vor-/Nachname)", re.compile(
r'\b(?:Herr|Frau|Doktor|Prof(?:essor)?|Dr\.?)\s+[A-Z][a-zäöüß]+\b',
re.IGNORECASE
)),
German Insurance Number
("german_insurance", "Sozialversicherungsnummer", re.compile(
r'(?<!DE)\b\d{2,3}[A-Z]{1,2}\d{8,12}\b'
)),
IBAN
("iban", "IBAN", re.compile(
r'\b[A-Z]{2}\d{2}\s?(?:\d{4}\s?){4,7}\d{0,2}\b'
)),
Redaction Examples
Input: "Hallo Herr Mustermann, max@example.com, Musterstraße 42, +49 170 1234567"
Output: "Hallo [Name - GESCHWÄRZT], [E-Mail - GESCHWÄRZT], [Adresse - GESCHWÄRZT], +[Telefonnummer - GESCHWÄRZT]"
Input: "Musterstraße 42, 10115 Berlin"
Output: "[Adresse (DE) - GESCHWÄRZT]"
Input: "60313 Frankfurt am Main"
Output: "[PLZ/Ort - GESCHWÄRZT] am Main"
Pattern Ordering Pitfall
PII_PATTERNS list order matters — sub() is applied sequentially. Known interactions:
- address_de must come before zip_city (address_de captures longer matches including street + PLZ; zip_city catches standalone PLZ+city)
- The phone pattern can over-match on IBAN digit blocks — acceptable since IBAN is also detected and all PII is redacted
- Short numeric sequences like
60313 (PLZ) can match phone first if zip_city is listed after phone — the fix is early zip_city placement
Heuristic: Over-detection is acceptable. The only relevant metric is whether the redacted output has 0 remaining PII hits.
Verification
from plugins.pii_guard import scan_for_pii, redact_pii
# Quick test
hits = scan_for_pii("Herr Schmidt, 01.01.1990, Musterstraße 42, 10115 Berlin")
print(f"Hits: {len(hits)}")
for h in hits:
print(f" [{h['type']}] {h['label']}: '{h['match']}'")
redacted = redact_pii("Herr Schmidt, 01.01.1990, Musterstraße 42, 10115 Berlin")
remaining = scan_for_pii(redacted)
print(f"Remaining after redaction: {len(remaining)}") # Must be 0