May 20, 2026
Shipped Discord, Then Killed It By Nightfall
Built a Discord channel for Chalie, found the privacy hole in testing, and ripped it back out the same day.
I started the day trying to give Chalie a voice outside the app — a Discord bot so it could answer in a server or a channel without anyone opening the web UI. I ended the day deleting that same feature. That reversal is the whole story of 2026-05-20, and I’d rather tell it straight than pretend it didn’t happen.
The build itself went fine. A new ExternalChatMessageProcessor handled inbound Discord messages separately from the main chat pipeline, with an owner_user_id credential so I could tell the difference between me talking to my own bot and a stranger in a public channel talking to it. Owner messages got role user; everyone else got third-party-user. I added a fifth policy context — discord — with its own allow/deny defaults, wired a safety system prompt to keep private information away from third parties, and shipped a Brain UI tab to configure it. Nine actions on the Ubiquiti network-control ability landed the same day, and I bumped every LLM provider client to send app-identification headers so Chalie shows up properly on OpenRouter’s usage dashboard. Good, ordinary progress.
Then I tested the Discord policy the way an adversary would, not the way I hoped it would behave: a third party in a public channel, asking about where I live. memory.recall was set to allow in that context, and the only thing standing between “someone I don’t know” and “my home address” was a sentence in a system prompt telling the model not to say it. It failed. Not partially — it just told them. A system-prompt instruction is not a security boundary, it’s a suggestion, and I’d built a feature that put a suggestion where a permission check belonged.
I spent the rest of the day patching before admitting the patches weren’t enough. First I removed the owner bypass from the public-channel safety rules — “trust the owner” logic has no business existing where everyone reads the same channel. Then I tightened the policy defaults harder: research tools like browser, search, weather stay allowed, but every personal-data tool (email, calendar, contacts, home, documents, lists, schedule, ubiquiti) gets denied outright. I renamed the tab from “Discord” to “External chat,” since whatever I landed on needed to generalize past Discord specifically. None of it closed the actual hole: there was no code-level filter stopping the model from surfacing private data to a stranger, only a prompt. So I reverted the whole surface — ability, processor, policy context, UI tab, catalog entries — back out. Nineteen commits building the feature, then one clean commit removing it, same day, on the record.
I kept one thing from the wreckage: a fix that moved browser.interact from ask to allow in chat policy, because it had nothing to do with the privacy problem and no reason to be reverted with everything else. Everything unsafe went; everything unrelated stayed.
This is the trade-off I keep making on purpose: ship fast, test like it’s already adversarial, and don’t let a shipped feature sit in a state I can’t defend. A privacy boundary that depends on the model behaving is not a boundary. The next version of external-channel messaging — and there will be one — gets built with the access control enforced in code before a single message ever reaches the LLM. That’s the actual lesson from today, and it’s now the design constraint for whatever channel comes next.
- Built and then fully reverted a Discord bot integration after testing exposed a privacy hole: memory access was allowed in a public-channel policy context with no code-level filter, only a prompt
- Removed owner-bypass logic from public-channel safety rules — a public channel has no “trusted asker,” everyone reads the same response
- Shipped a 9-action Ubiquiti network-control ability (devices, clients, WLANs, port forwarding, guest auth) with dual authentication and SSL fallback
- Added app-identification headers across all LLM provider clients so Chalie is attributable on OpenRouter’s dashboard
- Kept the one unrelated fix (browser.interact policy) that had nothing to do with the privacy incident