security-pii
Load when reviewing code that handles personal data in logs, test fixtures, error responses, serialized output, URLs, telemetry, or git history.
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
Load when reviewing code that handles personal data in logs, test fixtures, error responses, serialized output, URLs, telemetry, or git history.
Agent definition
security-pii.mdPII Exposure Patterns
Load when reviewing code that handles personal data in logs, test fixtures, error responses, serialized output, URLs, telemetry, or git history.
PII exposure: personally identifiable information appears in a context with broader visibility, longer retention, or lower trust than intended. Use synthetic data in tests, structured identifiers in logs, explicit field selection in API responses.
---
Use Synthetic Data in Test Fixtures
Tests, fixtures, snapshots, cassettes, and seed files must use obviously synthetic identifiers.
Correct Pattern
**Python:**
fixture = {
"email": "user@example.com",
"org_slug": "org-slug",
"name": "Jane Doe",
"ip": "198.51.100.23", # RFC 5737
"arr_usd": 120000,
"renewal_date": "2026-01-01",
}**TypeScript:**
const fixture = {
email: 'user@example.com',
orgSlug: 'org-slug',
name: 'John Doe',
ip: '203.0.113.42', // RFC 5737 TEST-NET-3
monthlySpend: 1000,
seatCount: 25,
};Safe Domains and IP Ranges
| Type | Safe Values | Standard | |------|-------------|----------| | Email domains | `example.com`, `example.org`, `example.net`, `.invalid` | RFC 2606 | | IPv4 | `192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24` | RFC 5737 | | IPv6 | `2001:db8::/32` | RFC 3849 | | Names | `Jane Doe`, `John Doe`, `Alice`, `Bob`, `Acme Corp` | Convention |
Why This Matters
Real data in git history cannot be fully purged. A single real email is a GDPR data subject access request waiting to happen.
Detection
rg -n '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' tests/ fixtures/ --type py --type ts | \
rg -v 'example\.(com|org|net)|noreply|test@|foo@|user@'
rg -n '\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b' tests/ fixtures/ | \
rg -v '192\.0\.2\.|198\.51\.100\.|203\.0\.113\.|127\.0\.0\.|0\.0\.0\.0|10\.\|172\.(1[6-9]|2|3[01])\.|192\.168\.'
rg -n 'customer|account.*slug|org.*slug|tenant' tests/ fixtures/ --type py --type ts---
Log Structured Identifiers Instead of Raw PII
Log internal ID, salted hash, or correlation ID — never raw email, IP, phone, or request body.
Correct Pattern
**Python:**
import hashlib
def hash_email(email: str) -> str:
return hashlib.sha256(f"salt:{email}".encode()).hexdigest()[:12]
logger.warning(
"identity lookup failed",
extra={"user_id": user.id, "email_hash": hash_email(user.email)},
)**TypeScript:**
logger.warn('signup failed', {
userId: user.id,
reason: 'validation_failed',
});**Go:**
slog.Warn("identity lookup failed",
"user_id", user.ID,
"error", err,
)Why This Matters
PII in logs creates a secondary data store bypassing access controls. Log aggregators retain data for months, index for full-text search, and expose to broad teams.
Detection
rg -n 'logger\.\w+\(.*email|logger\.\w+\(.*REMOTE_ADDR|logger\.\w+\(.*request\.(body|data|META)' --type py
rg -n 'logger\.\w+\(.*email|logger\.\w+\(.*req\.(body|ip)|console\.\w+\(.*email' --type ts
rg -n 'set_user\(|set_tag\(.*email|set_extra\(.*email' --type py
rg -n 'slog\.\w+\(.*email|log\.\w+\(.*email|zap\.\w+\(.*email' --type go
---
Keep PII Out of URL Query Strings and Path Segments
Emails, phone numbers, names in URLs appear in browser history, server logs, CDN logs, referrer headers, analytics.
Correct Pattern
**Python:**
request.session["login_error"] = "invalid_magic_code"
return redirect("/login/error")**TypeScript:**
return redirect('/oauth/error?reason=invalid_code');Detection
rg -n 'redirect.*email=|redirect.*user=|redirect.*phone=' --type py --type ts
rg -n 'f".*\?.*email|`.*\?.*email|\?.*email=' --type py --type ts
---
Exclude PII Fields from Serialized API Responses
Declare explicit field lists excluding PII not required by the caller.
Correct Pattern
**Python (DRF):**
class UserPublicSerializer(ModelSerializer):
class Meta:
model = User
fields = ['id', 'display_name', 'avatar_url']**TypeScript (Prisma):**
const user = await prisma.user.findUnique({
where: { id },
select: { id: true, displayName: true, avatarUrl: true },
});
return res.json(user);**GraphQL:**
type UserPublic {
id: ID!
displayName: String!
avatarUrl: String
}Why This Matters
`fields = '__all__'` exposes every column including fields added by future migrations. Full-row Prisma queries return password hashes, 2FA secrets.
Detection
rg -n "fields = '__all__'" --type py
rg -n 'email|phone|ipAddress|password' --type graphql --type ts | rg 'type |interface '
rg -n 'findUnique\(|findFirst\(' --type ts | rg -v 'select:'---
Scrub PII from Error Responses and Exception Handlers
Error responses: generic message + correlation ID. Stack traces, SQL, request bodies stay server-side.
Correct Pattern
**Python:**
import uuid
@app.errorhandler(Exception)
def handle_error(error):
correlation_id = str(uuid.uuid4())
app.logger.error("unhandled exception",
extra={"correlation_id": correlation_id, "error": str(error)}, exc_info=True)
return {"error": "internal server error", "reference": correlation_id}, 500**TypeScript:**
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const correlationId = crypto.randomUUID();
console.error({ correlationId, error: err.stack });
res.status(500).json({ error: 'internal server error', reference: correlationId });
});Detection
rg -n 'err\.stack|error\.stack|traceback|exc_info' --type py --type ts | rg -v 'logger\.|console\.'
rg -n 'request\.(body|data|form)|req\.body' --type py --type ts | rg 'return |res\.(json|send)'
rg -n 'set_user\(|set_context\(|set_extra\(' --type py | rg 'email|phone|ip|address'---
Detect Common PII Patterns in Code
# Email addresses (excluding safe domains)
rg -n '[a-zA-Z0-9._%+-]+@
Read more
PII Exposure Patterns
Load when reviewing code that handles personal data in logs, test fixtures, error responses, serialized output, URLs, telemetry, or git history.
PII exposure: personally identifiable information appears in a context with broader visibility, longer retention, or lower trust than intended. Use synthetic data in tests, structured identifiers in logs, explicit field selection in API responses.
---
Use Synthetic Data in Test Fixtures
Tests, fixtures, snapshots, cassettes, and seed files must use obviously synthetic identifiers.
Correct Pattern
**Python:**
fixture = {
"email": "user@example.com",
"org_slug": "org-slug",
"name": "Jane Doe",
"ip": "198.51.100.23", # RFC 5737
"arr_usd": 120000,
"renewal_date": "2026-01-01",
}**TypeScript:**
const fixture = {
email: 'user@example.com',
orgSlug: 'org-slug',
name: 'John Doe',
ip: '203.0.113.42', // RFC 5737 TEST-NET-3
monthlySpend: 1000,
seatCount: 25,
};Safe Domains and IP Ranges
| Type | Safe Values | Standard | |------|-------------|----------| | Email domains | `example.com`, `example.org`, `example.net`, `.invalid` | RFC 2606 | | IPv4 | `192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24` | RFC 5737 | | IPv6 | `2001:db8::/32` | RFC 3849 | | Names | `Jane Doe`, `John Doe`, `Alice`, `Bob`, `Acme Corp` | Convention |
Why This Matters
Real data in git history cannot be fully purged. A single real email is a GDPR data subject access request waiting to happen.
Detection
rg -n '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' tests/ fixtures/ --type py --type ts | \
rg -v 'example\.(com|org|net)|noreply|test@|foo@|user@'
rg -n '\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b' tests/ fixtures/ | \
rg -v '192\.0\.2\.|198\.51\.100\.|203\.0\.113\.|127\.0\.0\.|0\.0\.0\.0|10\.\|172\.(1[6-9]|2|3[01])\.|192\.168\.'
rg -n 'customer|account.*slug|org.*slug|tenant' tests/ fixtures/ --type py --type ts---
Log Structured Identifiers Instead of Raw PII
Log internal ID, salted hash, or correlation ID — never raw email, IP, phone, or request body.
Correct Pattern
**Python:**
import hashlib
def hash_email(email: str) -> str:
return hashlib.sha256(f"salt:{email}".encode()).hexdigest()[:12]
logger.warning(
"identity lookup failed",
extra={"user_id": user.id, "email_hash": hash_email(user.email)},
)**TypeScript:**
logger.warn('signup failed', {
userId: user.id,
reason: 'validation_failed',
});**Go:**
slog.Warn("identity lookup failed",
"user_id", user.ID,
"error", err,
)Why This Matters
PII in logs creates a secondary data store bypassing access controls. Log aggregators retain data for months, index for full-text search, and expose to broad teams.
Detection
rg -n 'logger\.\w+\(.*email|logger\.\w+\(.*REMOTE_ADDR|logger\.\w+\(.*request\.(body|data|META)' --type py rg -n 'logger\.\w+\(.*email|logger\.\w+\(.*req\.(body|ip)|console\.\w+\(.*email' --type ts rg -n 'set_user\(|set_tag\(.*email|set_extra\(.*email' --type py rg -n 'slog\.\w+\(.*email|log\.\w+\(.*email|zap\.\w+\(.*email' --type go
---
Keep PII Out of URL Query Strings and Path Segments
Emails, phone numbers, names in URLs appear in browser history, server logs, CDN logs, referrer headers, analytics.
Correct Pattern
**Python:**
request.session["login_error"] = "invalid_magic_code"
return redirect("/login/error")**TypeScript:**
return redirect('/oauth/error?reason=invalid_code');Detection
rg -n 'redirect.*email=|redirect.*user=|redirect.*phone=' --type py --type ts rg -n 'f".*\?.*email|`.*\?.*email|\?.*email=' --type py --type ts
---
Exclude PII Fields from Serialized API Responses
Declare explicit field lists excluding PII not required by the caller.
Correct Pattern
**Python (DRF):**
class UserPublicSerializer(ModelSerializer):
class Meta:
model = User
fields = ['id', 'display_name', 'avatar_url']**TypeScript (Prisma):**
const user = await prisma.user.findUnique({
where: { id },
select: { id: true, displayName: true, avatarUrl: true },
});
return res.json(user);**GraphQL:**
type UserPublic {
id: ID!
displayName: String!
avatarUrl: String
}Why This Matters
`fields = '__all__'` exposes every column including fields added by future migrations. Full-row Prisma queries return password hashes, 2FA secrets.
Detection
rg -n "fields = '__all__'" --type py
rg -n 'email|phone|ipAddress|password' --type graphql --type ts | rg 'type |interface '
rg -n 'findUnique\(|findFirst\(' --type ts | rg -v 'select:'---
Scrub PII from Error Responses and Exception Handlers
Error responses: generic message + correlation ID. Stack traces, SQL, request bodies stay server-side.
Correct Pattern
**Python:**
import uuid
@app.errorhandler(Exception)
def handle_error(error):
correlation_id = str(uuid.uuid4())
app.logger.error("unhandled exception",
extra={"correlation_id": correlation_id, "error": str(error)}, exc_info=True)
return {"error": "internal server error", "reference": correlation_id}, 500**TypeScript:**
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const correlationId = crypto.randomUUID();
console.error({ correlationId, error: err.stack });
res.status(500).json({ error: 'internal server error', reference: correlationId });
});Detection
rg -n 'err\.stack|error\.stack|traceback|exc_info' --type py --type ts | rg -v 'logger\.|console\.'
rg -n 'request\.(body|data|form)|req\.body' --type py --type ts | rg 'return |res\.(json|send)'
rg -n 'set_user\(|set_context\(|set_extra\(' --type py | rg 'email|phone|ip|address'---
Detect Common PII Patterns in Code
# Email addresses (excluding safe domains) rg -n '[a-zA-Z0-9._%+-]+@
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

