# TASK 49 — Chat profanity filter (local, opt-in, non-persistent) > Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here. > **Research: [`TASKS/research-store-certification.md`](research-store-certification.md) §12.** > Carved out 2026-09-27; shares a scope lock with nothing else. **Status:** ☐ Queued — not blocked (does not depend on the route decision) **Goal:** let the creator decide what reaches the projected chat box, without the app ever becoming a moderator that stores what people said. **Size:** S. Local string work, one insertion point, tests. --- ## Why YouTube's Community Guidelines carry a named **Vulgar language policy** under "Sensitive content", alongside nudity, child safety, and self-harm — so profanity is a policy surface, not an edge case. Three external reasons it earns a slot, plus the real one: 1. YouTube treats it as policy-relevant, so 11.12's "proactive detection" language has something to attach to 2. Competitive parity — overlay tools in this lane generally offer it 3. It is a reasonable reading of 11.12's proactive-detection language 4. **The real reason: we control what gets projected on screen.** That is the creator's call, not YouTube's. --- ## 1. ⛔ Four constraints — non-negotiable They exist to **protect the privacy story**, not to complicate the build: 1. **Local and on-device only.** No server, no telemetry of message content, nothing leaves the machine. A hosted profanity API would invert the entire 11.12/10.5.1 argument in `research-store-certification.md` §9 — do not add one. 2. **Non-persistent.** Filter at ingest, discard the raw text, keep only the display string. No message history, no file, no crash dump of chat payloads. This is the same property that makes the 11.12 certification answer strong. 3. **User-supplied word list, opt-in, stored locally.** ⛔ **Not a hardcoded slur list baked into the binary.** That is an unpleasant artifact, generates false positives across dialects and languages, and becomes our problem the moment someone extracts it. A user-curated list is also simply more useful — streamers know their community's euphemisms and we do not. 4. **Match display text only.** ⛔ Never the SuperChat amount or any reward-event field. Those are financial data under 10.5.5, and matching on them would be a real mistake. ## 2. ☐ Verify the insertion point first The existing alert box already has a ticker and three display methods (`Controls/OverlayHost.xaml`, `ViewModels/MainViewModel.Chat.cs`), so there is probably a **single insertion point** rather than scattered call sites. **Do not assume.** `rg` for the chat-message ingest path and confirm the count. If it is more than one site, say so in `HANDOFF.md` before building. ## 3. ☐ Implementation - ☐ Settings toggle: **off by default** (opt-in, per constraint 3) - ☐ Editable word list in the settings dialog, stored locally via the existing `LayoutStore.Settings` pattern — reuse it, do not invent a second store - ☐ Case-insensitive, **whole-word** matching with sensible boundary handling - ☐ Smallest sensible replacement (`*` or configurable) — do not build a masking engine - ☐ Filtered messages are **displayed** with the replacement, not dropped, so the chat does not develop visible holes. (Dropping is a separate opt-in if we ever want it.) - ☐ Nothing logged. The existing `AppLog` checkpoints must not capture message text - ☐ Correctness over performance: a few hundred comparisons per message is nothing - ☐ Non-ASCII and unicode case handling — a naive `ToLowerInvariant` will miss things - ☐ Ticker layout must survive longer replacement strings ## 4. ☐ Tests Follow the existing test conventions (`ytLive.Tests/`, `RuntimeSessionStateTests` etc.): - ☐ Off by default - ☐ List matches case-insensitively - ☐ Whole-word only — "class" must not be caught by "ass" - ☐ Replacement renders, message not dropped - ☐ **Amount/reward fields are never matched or masked** (constraint 4 — assert explicitly) - ☐ Empty list behaves as "no filtering" - ☐ Nothing persists to disk beyond the user's own list - ☐ Unicode case folding --- ## Related (not duplicated here) - [`task-48-distribution-msix.md`](task-48-distribution-msix.md) item 5 — the privacy policy must state "no retention of chat or reward payloads". This task is what makes that statement true. - `research-store-certification.md` §9 — the 11.12 argument depends on the persist-nothing property in constraint 2. **Do not let a future feature (chat history, moderation logs, analytics) quietly break that assumption.**