Zerf home

block_guest_data.py

hooks/block_guest_data.py

#!/usr/bin/env python3
"""PreToolUse hook: block any write that contains guest emails or phone numbers.

Claude Code sends the tool call as JSON on stdin. Exit code 2 blocks the call
and shows stderr to the agent. Owner addresses listed in config/owners.json are
allowed, because the outbox needs them.
"""
import json
import os
import re
import sys

EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
# US style (305-555-0142, (305) 555-0142, 305.555.0142) or international with a + prefix.
PHONE = re.compile(
    r"\(?\b\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b"
    r"|\+\d{1,3}[\s.-]?\d{1,4}(?:[\s.-]?\d{2,4}){2,3}\b"
)


def allowed_emails() -> set[str]:
    path = os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", "."), "config", "owners.json")
    try:
        with open(path, encoding="utf-8") as f:
            owners = json.load(f)
    except (OSError, json.JSONDecodeError) as err:
        print(f"Hook could not read owners.json: {err}", file=sys.stderr)
        return set()
    return {o["email"].lower() for o in owners.get("owners", []) if "email" in o}


def main() -> int:
    call = json.load(sys.stdin)
    text = json.dumps(call.get("tool_input", {}))
    emails = {e.lower() for e in EMAIL.findall(text)} - allowed_emails()
    if emails or PHONE.search(text):
        print(
            "Blocked: guest contact data found (email or phone). "
            "Remove names, emails and phone numbers before writing.",
            file=sys.stderr,
        )
        return 2
    return 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as err:  # fail closed: a broken hook must not let the write through
        print(f"Hook error, write blocked: {err}", file=sys.stderr)
        sys.exit(2)