Bug, not yet spec'd
Found 2026-08-16 while troubleshooting "/hawkbot calendar returns Not Found for every calendar" on the production workspace, despite /hawkbot config showing correct-looking calendar ids and all three calendars confirmed shared with the service account. Logging it as a note to revisit with a grilling session later, not ready for implementation.
Root cause (found with help from Ty Tremblay, see Slack thread): Google Calendar ids look like email addresses (c_xxx@group.calendar.google.com). When a coach types one into /hawkbot config set team_meeting_calendar_id <id>, Slack's client auto-converts the email-shaped text into a mailto: link before the server ever sees it — the command payload arrives as <mailto:c_xxx@group.calendar.google.com|c_xxx@group.calendar.google.com>. checkSetting in src/domain/settings.ts already unwraps Slack's escaping for two other formats — <#C0123|name> channels via unwrapChannel and <!subteam^S0123|@handle> user groups via unwrapUserGroupHandle — but has no equivalent unwrap for Slack's link/mailto format. So the stored value became mailto:c_xxx@group.calendar.google.com, which isn't a valid calendar id, and fetchCalendarEvents in src/calendar/client.ts got a 404 Not Found from Google for a calendar that doesn't exist under that literal string.
Why it took so long to find: the corruption was invisible in Slack's UI. Once the bad value was stored, every place that echoed it back — /hawkbot config's display, and the bot's own error message (mailto:c_xxx@... doesn't exist as a calendar id) — got the mailto: prefix re-rendered by Slack as a link, which displays as just the plain id. So the displayed text looked identical to a correct calendar id in every surface we checked; only right-clicking the rendered link and choosing "Remove Link" (or reading the raw value server-side) revealed the prefix.
Affected settings today: the three calendar ids (team_meeting_calendar_id, informational_calendar_id, mentor_calendar_id) — the only settings shaped like an email address. Worth checking the rest of the SETTINGS list for anything else that could be URL-shaped (Slack also auto-links bare URLs, not just emails) and hit the same trap.
Why it's not simple — a few open questions for the grilling session:
- What's the general fix: a targeted
unwrapMailto-style helper applied only to the calendar-id settings (matching how unwrapChannel/unwrapUserGroupHandle are targeted), or a blanket unwrap-any-Slack-link step applied to every setting value in checkSetting, since we can't fully predict which future settings might be email/URL-shaped?
- Slack's link syntax varies (
<mailto:x|y> for auto-linked emails, <http://x|y> / <https://x|y> for auto-linked URLs) — does the unwrap need to handle both, or just mailto for now?
- Should
/hawkbot config set silently unwrap (like the existing channel/usergroup unwraps do), or tell the coach it detected and stripped a link wrapper, so they know Slack did something to their input?
- Three settings in the production DB were already corrupted this way before we caught it — worth a one-off audit/fix of existing stored values (or a migration), not just fixing new writes.
Not scoped or prioritized — just don't want to lose the finding or make someone else rediscover it the hard way.
Bug, not yet spec'd
Found 2026-08-16 while troubleshooting "
/hawkbot calendarreturnsNot Foundfor every calendar" on the production workspace, despite/hawkbot configshowing correct-looking calendar ids and all three calendars confirmed shared with the service account. Logging it as a note to revisit with a grilling session later, not ready for implementation.Root cause (found with help from Ty Tremblay, see Slack thread): Google Calendar ids look like email addresses (
c_xxx@group.calendar.google.com). When a coach types one into/hawkbot config set team_meeting_calendar_id <id>, Slack's client auto-converts the email-shaped text into amailto:link before the server ever sees it — the command payload arrives as<mailto:c_xxx@group.calendar.google.com|c_xxx@group.calendar.google.com>.checkSettinginsrc/domain/settings.tsalready unwraps Slack's escaping for two other formats —<#C0123|name>channels viaunwrapChanneland<!subteam^S0123|@handle>user groups viaunwrapUserGroupHandle— but has no equivalent unwrap for Slack's link/mailto format. So the stored value becamemailto:c_xxx@group.calendar.google.com, which isn't a valid calendar id, andfetchCalendarEventsinsrc/calendar/client.tsgot a404 Not Foundfrom Google for a calendar that doesn't exist under that literal string.Why it took so long to find: the corruption was invisible in Slack's UI. Once the bad value was stored, every place that echoed it back —
/hawkbot config's display, and the bot's own error message (mailto:c_xxx@... doesn't exist as a calendar id) — got themailto:prefix re-rendered by Slack as a link, which displays as just the plain id. So the displayed text looked identical to a correct calendar id in every surface we checked; only right-clicking the rendered link and choosing "Remove Link" (or reading the raw value server-side) revealed the prefix.Affected settings today: the three calendar ids (
team_meeting_calendar_id,informational_calendar_id,mentor_calendar_id) — the only settings shaped like an email address. Worth checking the rest of theSETTINGSlist for anything else that could be URL-shaped (Slack also auto-links bare URLs, not just emails) and hit the same trap.Why it's not simple — a few open questions for the grilling session:
unwrapMailto-style helper applied only to the calendar-id settings (matching howunwrapChannel/unwrapUserGroupHandleare targeted), or a blanket unwrap-any-Slack-link step applied to every setting value incheckSetting, since we can't fully predict which future settings might be email/URL-shaped?<mailto:x|y>for auto-linked emails,<http://x|y>/<https://x|y>for auto-linked URLs) — does the unwrap need to handle both, or just mailto for now?/hawkbot config setsilently unwrap (like the existing channel/usergroup unwraps do), or tell the coach it detected and stripped a link wrapper, so they know Slack did something to their input?Not scoped or prioritized — just don't want to lose the finding or make someone else rediscover it the hard way.