# What is private, and what stops it leaking This repository is public. The bot it describes reads a private group chat, posts from a private photo album, and keeps a database of who said how much. None of that may be in here, and on 2026-08-30 some of it was. ## What leaked, and how Real first names, one full name, two phone numbers and a group member's Signal uuid were in the tree *and* in the public history. They arrived one way every time: **somebody reached for a real name because it was the example in front of them** — a test fixture built from a live database dump, an envelope pasted verbatim out of a running container, a help text written while looking at the actual group. Fixing it needed the tree scrubbed, all 56 commits rewritten with `git filter-repo`, and then the repository **deleted and recreated**, because GitHub keeps orphaned commits fetchable by their SHA after a force-push and only the repository going away takes them with it. The closed pull requests and the star went too. ## What is private, and where it lives | | Where | Never in git because | |---|---|---| | the photos and who is in them | `~/.local/share/signal-bot/pack/` | it is outside the repo, and `.gitignore` also names `people.json`, `tags.json`, `media/`, `pack/` and every media extension | | the message database | `signal_bot.sqlite3` | `*.sqlite3` and its `-wal`/`-shm` siblings are ignored | | the account's number, groups, inviters | `.env` | `.env` is ignored; `.env.sample` carries the shape and no values | | everyone's names | the database and the pack manifest | nothing reads them into the repo; the bot logs `pack:p1`, never a name | All of it is `700`/`600` on disk. The repository never needs any of it: the tests, the documents and the offline end-to-end run use invented people — alfa, beta, gamma, ala, basia, celina — and invented ids that base64-decode twice into a sentence saying what they are. ## The gate `scripts/check_personal_data.sh` refuses a commit that carries personal data. It runs on staged files, on the commit message, and over the whole tree in CI. Two halves, because they fail differently: - **Names cannot be listed here** — a file of real names kept in the repo to check against *is* the leak. So the checker learns them at run time from outside: the photo pack's manifest, the message database, and `.env`. It never prints the word it matched, because this output reaches CI logs. - **Shapes can be listed** — a phone number, a uuid and a Signal group id look like themselves anywhere, so those are matched by pattern and compared against `.personal-data-allowlist`, which holds only invented values. On a machine with none of that private data (CI, a fresh clone) the name half has nothing to compare against and **says so** rather than passing quietly. It found a real leak the first time it was run: the live test group's id, in two spellings, in `tests/test_config.py` — which four rounds of grepping by hand had missed, because nobody thinks to grep for base64. ## When it fires ``` tests/test_thing.py:12: a name from your private data ``` Use an invented name. `ala`, `basia`, `celina`, `darek` are already in use; so are `+15550000000` and ids that decode to `e2e-watched-group-id-32-bytes!!!`. ``` DOCS-thing.md:40: phone number '+48123456789' is not in .personal-data-allowlist ``` Either it belongs to somebody — replace it — or it is invented, in which case add it to `.personal-data-allowlist` **after checking that it is**. A line there may be scoped to one file (`signal_bot/popes.json::hadrian`), which is how a given name that is also a pope stays refused in every test fixture that has no business carrying it. ## What the gate cannot do - It does not scan history. Rewriting that is a deliberate, separate act. - It knows only what is on *this* machine. A name nobody has ever typed into the group is a name it cannot recognise. - It does not stop the bot's own logs holding display names — `journalctl --user -u signal-bot` does, and `--vacuum-time=` is how that is emptied.