unbroker

Autonomously remove your info from data-broker sites.

  • privacy
  • data-broker
  • opt-out
  • ccpa
  • gdpr
  • security
  • doxxing

Declared platforms: linux · macos · windows

Install
npx skills add 'https://github.com/NousResearch/hermes-agent/tree/main/optional-skills/security/unbroker'
Download bundle ↓
main · 24fd22bScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

scripts/badbool.py

scripts/badbool.pyBrowse 56 files
View on GitHub
← Back to SKILL.md
"""Pull and parse the Big-Ass Data Broker Opt-Out List (BADBOOL) into broker records. BADBOOL (https://github.com/yaelwrites/Big-Ass-Data-Broker-Opt-Out-List) is amaintained, frequently-updated markdown list. `refresh` fetches it and parses the"People Search Sites" section into records that merge UNDER the curated DB (curatedrecords always win). Auto-parsed records carry source="BADBOOL-auto" andconfidence="auto" so the agent treats their URLs as best guesses to verify first. `parse()` is pure (markdown in, records out) so it is tested offline; `fetch()` isthe only network call and can be bypassed by passing markdown directly to refresh()."""from __future__ import annotations import reimport urllib.requestfrom pathlib import Path import storage DEFAULT_URL = (    "https://raw.githubusercontent.com/yaelwrites/"    "Big-Ass-Data-Broker-Opt-Out-List/master/README.md")USER_AGENT = "Mozilla/5.0 (compatible; unbroker/1.0; data opt-out)" # BADBOOL legend symbols.SYMBOLS = {    "crucial": "\U0001F490",  # 💐    "high": "\u2620",          # ☠    "gov_id": "\U0001F3AB",    # 🎫    "phone": "\U0001F4DE",     # 📞    "payment": "\U0001F4B0",   # 💰} _LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")_OPTOUT_HINT = re.compile(    r"opt[\- ]?out|optout|removal|remove|suppress|control-privacy|delete", re.I)_FIND_HINT = re.compile(r"find|your information|search|look ?up|look for", re.I)  def slug(name: str) -> str:    # Drop a trailing .com/.org/.info on the displayed name so "FastPeopleSearch.com"    # matches the curated id "fastpeoplesearch"; keep .net/.id so distinct sites differ.    n = re.sub(r"\.(com|org|info)\b", "", name.strip(), flags=re.I)    return re.sub(r"[^a-z0-9]+", "", n.lower())  def _heading_flags(heading: str) -> tuple[str, dict]:    flags = {key: (sym in heading) for key, sym in SYMBOLS.items()}    name = heading    for sym in SYMBOLS.values():        name = name.replace(sym, "")    name = name.replace("\ufe0f", "").strip()    return name, flags  def _priority(flags: dict) -> str:    if flags["crucial"]:        return "crucial"    if flags["high"]:        return "high"    return "standard"  def _pick(links: list[tuple[str, str]], hint: re.Pattern) -> str | None:    for _text, url in links:        if hint.search(url):            return url    for text, url in links:        if hint.search(text):            return url    return None  def _clean(text: str) -> str:    return re.sub(r"\s+", " ", text).strip()[:600]  def _build(name: str, flags: dict, body: str) -> dict:    links = _LINK_RE.findall(body)    web = [(t, u) for t, u in links if u.lower().startswith("http")]    mailtos = [u[7:] for _t, u in links if u.lower().startswith("mailto:")]    optout_url = _pick(web, _OPTOUT_HINT)    search_url = _pick(web, _FIND_HINT) or (web[0][1] if web else None)     if flags["phone"]:        method = "phone"    elif optout_url:        method = "web_form"    elif mailtos:        method = "email"    else:        method = "manual"     return {        "id": slug(name),        "name": name,        "category": "people_search",        "priority": _priority(flags),        "jurisdictions": ["US"],        "search": {"method": "url_pattern", "url": search_url, "fetch": "browser",                   "match_signal": "result", "by": ["name", "phone", "address"]},        "optout": {            "method": method,            "url": optout_url,            "email": mailtos[0] if mailtos else None,            "requires": {                "gov_id": flags["gov_id"],                "phone_voice": flags["phone"],                "payment": flags["payment"],                "email_verification": False,                "captcha": False,                "account": False,                "phone_callback": False,            },            "inputs": ["full_name", "contact_email"],            "notes": _clean(body),            "links": [{"text": t, "url": u} for t, u in links],            "est_processing_days": 14,  # unknown for auto records; drives next_recheck_at        },        "source": "BADBOOL-auto",        "confidence": "auto",        "last_verified": None,    }  def parse(markdown: str) -> list[dict]:    """Parse the 'People Search Sites' section of BADBOOL into broker records."""    records: list[dict] = []    in_people = False    heading: str | None = None    body: list[str] = []     def flush() -> None:        nonlocal heading, body        if heading is not None:            name, flags = _heading_flags(heading)            if name:                records.append(_build(name, flags, "\n".join(body).strip()))        heading, body = None, []     for line in markdown.splitlines():        if line.startswith("## "):            flush()            in_people = line[3:].strip().lower().startswith("people search")            continue        if not in_people:            continue        if line.startswith("### "):            flush()            heading = line[4:].strip()        elif heading is not None:            body.append(line)    flush()    return records  def fetch(url: str = DEFAULT_URL, timeout: int = 30) -> str:    req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})    with urllib.request.urlopen(req, timeout=timeout) as resp:  # noqa: S310        return resp.read().decode("utf-8", errors="replace")  MIN_EXPECTED = 20  # BADBOOL's People Search section lists ~47; far fewer => upstream reorg, warn  def refresh(cache_path: Path, url: str = DEFAULT_URL, markdown: str | None = None) -> dict:    """Fetch (or accept) BADBOOL markdown, parse it, and write the snapshot cache."""    md = markdown if markdown is not None else fetch(url)    records = parse(md)    storage.write_json(cache_path, records)    out = {"parsed": len(records), "cache_path": str(cache_path), "source_url": url}    if len(records) < MIN_EXPECTED:        out["warning"] = (f"only {len(records)} parsed (expected >{MIN_EXPECTED}); BADBOOL's "                          "'People Search Sites' section may have moved/reorganized - check the parser")    return out