A Slack Digest Hook That Catches What I Miss
When I am deep in another problem, Slack stops being something I check. I made the digest fire itself so the blockages waiting on me surface anyway.
I cover 3+ positions at a startup. The mode-switching cost shows up in Slack. Across a normal day there are decisions and judgments that only move when I look — and when I am deep in another problem, I do not look for hours.
I had a Slack digest skill I had built earlier this year. It scanned channels, summarised the noise, gave me a readable view — useful, but I had to remember to run it. On the mornings when I was already lost in something, I forgot, which is exactly when I most needed it. So I made it run itself at the start of every workday and land in the one place I cannot ignore.
What I actually wanted
The digest should arrive without me asking. It should land somewhere I cannot ignore. And DMs and mentions should appear verbatim at the top, with the channel digest curated below. DMs and mentions are the messages I lose; the rest is noise the LLM can compress.
Where to land it was the first decision. A Slack DM-to-self would be reliable but live in the same noise I already miss things in. A push notification would be unmissable but ephemeral. The Obsidian daily note solved both problems: I populate it manually anyway with what I do during the day, so I open it on every workday. Appending the digest there means I see it on every workday by default.
I did the requirements pass with /deep-interview, a Socratic interview skill that asks targeted questions until the spec is concrete. Six rounds in, ambiguity was at 5%. The interview itself did the useful work. It forced me to be honest about cases I had been hand-waving past. The most important one was the time window for "unread."
My original framing was “past 24 hours.” The contrarian round asked me what happens on Mondays. If I finish Friday afternoon and the digest fires Monday at 13:00, “past 24 hours” only scans Sunday. I miss Friday. After a week of vacation I miss the whole week. The answer was “since the last digest.” A single state-file timestamp. One rule that handles weekends, vacations, and post-meeting backlog at the same time.
The workday-boundary case needed its own care. I work Canadian hours, which lands somewhere from late afternoon through early morning my local time. My morning is around noon local time. A naive “fire on first launch of the day” would fire mid-shift, around midnight my time, when I am still working. The interview pushed back at the noon gate, arguing that since-last-digest already guarantees nothing is missed and the gate is just complexity. Once I supplied the Canadian-hours context, it walked back. The gate stayed: it is not there to catch missed messages but to put the digest at the start of my workday, not in the middle.
What I built
A SessionStart hook runs every time I launch Claude Code. It checks three gates: weekday, post-noon local, and a state file that records whether I have already fired today. If all three pass, it spawns a detached background runner and exits. The hook had to be fast — anything that slowed every session start would force me to disable it.
The background runner calls claude -p /slack-morning-digest <window-start>, which invokes a skill that fetches DMs and mentions verbatim from the Slack MCP server and runs the channel-digest logic against everything else. The output is appended to today's Obsidian daily note. If the note does not exist yet, the runner creates it.
I ran the plan through Planner, Architect, and Critic before any code got written. The Architect flagged two real correctness bugs in the hook configuration: wrong settings.json nesting, and a CLI flag that does not actually exist. The Critic pushed back on a third concern and proposed a verification spike to confirm two architectural assumptions before I committed to the design. Both passed. The spike was cheaper than discovering either failure mid-implementation.
What broke at runtime
I had verified the trigger fired correctly on a Saturday. It ran fast enough not to be noticed, the weekend gate worked, the state file was untouched. Monday’s first production fire hit two bugs the test pass had not exercised.
The first was spawnSync ... claude.cmd EINVAL. Node 24 rejects spawnSync of .cmd and .bat files unless shell: true is passed. The claude command on Windows is installed as a .cmd wrapper around the real .exe. The fix was to point the runner at the underlying .exe directly instead of the wrapper, and the EINVAL stopped. The Saturday test had exercised the trigger but not the runner end-to-end.
The second bug was a five-minute hang. With EINVAL fixed, claude.exe -p was running but never completing. The cause was that in headless mode, every MCP tool call hits a permission prompt that nobody can answer. The process waits for input that will never come. The fix was --permission-mode bypassPermissions, with --disallowedTools mcp__slack__conversations_add_message as an extra read-only guarantee. Even if permissions are bypassed, the runner is structurally incapable of writing to Slack.
A third issue surfaced as a side observation. The trigger that ran in a few milliseconds on Saturday was hitting close to a second in production. PowerShell Start-Process was what the Planner had written into the runner — defensible on Windows, but the cold-start cost was avoidable. The agent caught the regression while fixing EINVAL and replaced it with Node's native spawn(detached: true, stdio: 'ignore', windowsHide: true) + child.unref(). The trigger went back to a few milliseconds.
And one bug only I could see. The first successful Monday fire pulled “the past 24 hours” because lastSuccessfulFire was still null. There had been no prior successful run to anchor the window to. So the digest covered Sunday only, not Friday-through-Sunday. The fix was a weekday-aware cold-start fallback: Monday gets 72 hours, Tuesday through Friday gets 24 hours. After the first successful fire, lastSuccessfulFire takes over and the cold-start branch never runs again. The bug was in a corner case that only fires once in the system's life. It would also have made the first Monday useless.
What the system now does
On the first weekday launch after noon local, the trigger fires. A few minutes later, my Obsidian daily note gains a section with verbatim DMs and mentions at the top and a curated channel summary below. I read it whether I want to or not, because the daily note is already where I write down what I am doing during the day. The friction of opening it is zero.
The first digest landed before the cold-start fix shipped, so it covered Sunday only — a near-empty day. Even on that thin window it caught a couple of colleagues’ daily updates I had been meaning to track and would not have remembered to scan for.
The build itself was AI-assisted end to end. I directed a /deep-interview pass for the spec, then a Planner / Architect / Critic pass on the design. Claude Code wrote the hook and runner under that direction. All of those agents live in Oh My ClaudeCode, a plugin for Claude Code that I use daily for most non-trivial work.
The interview and Architect/Critic passes caught the correctness bugs before any code was written. Production caught what those passes had not exercised. I’d be interested to hear how others use Claude Code, or any AI tool, to catch what would otherwise pile up unseen in their own day.