Created
January 30, 2026 16:05
-
-
Save jstockdi/aeaae7f5d01bd3f18d2e40f63633a930 to your computer and use it in GitHub Desktop.
Implementing PII Validation Hooks in Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Implementing PII Validation Hooks in Claude Code | |
| Hooks are shell commands that run at specific points in Claude Code's workflow. For PII protection, you'll use PreToolUse hooks to inspect and block operations before they execute. | |
| ## Step 1: Create the PII Validation Script | |
| Create a file at `.claude/hooks/pii_validator.py`: | |
| ```python | |
| #!/usr/bin/env python3 | |
| """ | |
| PII Validator Hook for Claude Code | |
| Blocks file writes and commands that contain potential PII. | |
| """ | |
| import json | |
| import sys | |
| import re | |
| # PII patterns to detect | |
| PII_PATTERNS = { | |
| 'ssn': r'\b\d{3}-\d{2}-\d{4}\b', | |
| 'credit_card': r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b', | |
| 'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', | |
| 'phone': r'\b(\+1[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b', | |
| 'api_key': r'\b(sk-[a-zA-Z0-9]{20,}|api[_-]?key[=:]\s*["\']?[a-zA-Z0-9]{20,})\b', | |
| 'aws_key': r'\b(AKIA[0-9A-Z]{16})\b', | |
| 'password': r'(password|passwd|pwd)\s*[=:]\s*["\']?[^\s"\']+', | |
| } | |
| def detect_pii(text: str) -> list[tuple[str, str]]: | |
| """Scan text for PII patterns. Returns list of (pii_type, matched_value).""" | |
| findings = [] | |
| for pii_type, pattern in PII_PATTERNS.items(): | |
| matches = re.findall(pattern, text, re.IGNORECASE) | |
| for match in matches: | |
| # Redact the actual value for safety | |
| redacted = match[:4] + '***' if len(match) > 4 else '***' | |
| findings.append((pii_type, redacted)) | |
| return findings | |
| def main(): | |
| # Read hook input from stdin | |
| input_data = json.load(sys.stdin) | |
| tool_name = input_data.get('tool_name', '') | |
| tool_input = input_data.get('tool_input', {}) | |
| content_to_check = "" | |
| context = "" | |
| # Extract content based on tool type | |
| if tool_name in ('Write', 'Edit', 'MultiEdit'): | |
| content_to_check = tool_input.get('content', '') | |
| context = f"file: {tool_input.get('file_path', 'unknown')}" | |
| elif tool_name == 'Bash': | |
| content_to_check = tool_input.get('command', '') | |
| context = "bash command" | |
| elif tool_name == 'WebFetch': | |
| # Check if URL contains sensitive data | |
| content_to_check = tool_input.get('url', '') | |
| context = "web request URL" | |
| # Scan for PII | |
| findings = detect_pii(content_to_check) | |
| if findings: | |
| # Block the operation and provide feedback | |
| pii_types = ', '.join(set(f[0] for f in findings)) | |
| output = { | |
| "hookSpecificOutput": { | |
| "hookEventName": "PreToolUse", | |
| "permissionDecision": "deny", | |
| "userFacingText": f"🛑 PII BLOCKED: Detected {pii_types} in {context}", | |
| "assistantFacingText": ( | |
| f"SECURITY BLOCK: Your {tool_name} operation was blocked because it " | |
| f"contains potential PII ({pii_types}). Please remove or redact " | |
| f"sensitive information before proceeding. Detected patterns: " | |
| f"{[f[1] for f in findings]}" | |
| ) | |
| } | |
| } | |
| print(json.dumps(output)) | |
| sys.exit(2) # Exit code 2 = block with feedback | |
| # Allow the operation | |
| sys.exit(0) | |
| if __name__ == "__main__": | |
| main() | |
| ``` | |
| ## Step 2: Make it Executable | |
| ```bash | |
| chmod +x .claude/hooks/pii_validator.py | |
| ``` | |
| ## Step 3: Configure the Hook in Settings | |
| Add to your `.claude/settings.json`: | |
| ```json | |
| { | |
| "hooks": { | |
| "PreToolUse": [ | |
| { | |
| "matcher": "Write|Edit|MultiEdit|Bash|WebFetch", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/pii_validator.py\"", | |
| "timeout": 10 | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } | |
| ``` | |
| ## Step 4: Register the Hook | |
| In Claude Code, run: | |
| ``` | |
| /hooks | |
| ``` | |
| Then review and approve the new hook configuration. | |
| ## How It Works | |
| | Exit Code | Behavior | | |
| |-----------|----------| | |
| | 0 | Allow the operation to proceed | | |
| | 2 | Block and provide feedback to Claude | | |
| | Non-zero (other) | Block silently | | |
| The hook receives JSON on stdin with the tool details: | |
| ```json | |
| { | |
| "tool_name": "Write", | |
| "tool_input": { | |
| "file_path": "/path/to/file.txt", | |
| "content": "file content here..." | |
| } | |
| } | |
| ``` | |
| ## Extended Example: Adding Custom Patterns | |
| For your KYC/fintech context, you might want to add patterns like: | |
| ```python | |
| PII_PATTERNS = { | |
| # ... existing patterns ... | |
| 'sin': r'\b\d{3}[- ]?\d{3}[- ]?\d{3}\b', # Canadian SIN | |
| 'passport': r'\b[A-Z]{2}\d{6}\b', | |
| 'account_number': r'\baccount[_\s]?(num|number|#)?[:\s]*\d{8,12}\b', | |
| 'routing_number': r'\brouting[_\s]?(num|number|#)?[:\s]*\d{9}\b', | |
| } | |
| ``` | |
| --- | |
| Would you like me to expand this with additional validation logic, like checking for PII in file paths or adding logging/alerting capabilities? |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment