2026年4月7日星期二

Wake me up, when cronjob ends


Every morning at 9:03, a cron job scans my GitHub notifications, writes a summary to disk, and exits. The file sits in gh-pending.md. Nobody opens it.

The terminal window doesn't open. There's no sound. I make coffee and forget the thing exists. Then I remember, open the file, and find yesterday's summary sitting there having waited patiently for eight hours. This is not a workflow. This is a file.

This assumes you already have a Claude Code cron skill — a slash command that runs non-interactively, writes output to disk, and exits. The one used here is gh-review, which scans GitHub notifications and writes a pending-items file. If you don't have one, there's nothing here to notify you about.

I built a fix: a shell + AppleScript pipeline that fires a persistent modal alert when the cron skill finishes, and when you click "Open CC," opens iTerm2 pre-loaded with context and ready to go.


Building it required five choices that weren't obvious in advance.

display alert, not Notification Center. Banners auto-dismiss. Modal alerts require a click. giving up after 86400 ensures it won't block forever — one day is enough.

iTerm2, not Ghostty. Windows created via open -na Ghostty don't register with macOS's Accessibility layer. System Events can't find them. I tried AXUIElement traversal, highest-PID strategy, window-x/y flags — all failed with the same result: System Events got an error: Can't get window 1 of process "Ghostty". iTerm2 has a complete, stable AppleScript dictionary. Ghostty stays on the taskbar. iTerm2 gets one job.

The temp script trick. iTerm2's command parameter accepts a single binary path — compound shell commands get truncated at the first space. Fix: write the full launch command to /tmp/cc-launch.sh, pass only that path:

cat > /tmp/cc-launch.sh << EOF
#!/bin/bash
source ~/.nvm/nvm.sh
cd ~/writer
claude --resume "$context_file"
EOF

This also solves the cron nvm problem for free.

Explicit --mode=cron. Early versions inferred run mode from session history — fragile because session files can be missing, truncated, or from a half-finished interactive run, any of which tips the inference toward "interactive."

The failure mode looked like success. The skill ran, exited clean, logged nothing anomalous. It just didn't write the summary file. I checked the script, checked permissions, re-ran it manually — worked fine. Ran it via cron — worked fine. Three mornings in a row the file sat empty, and three mornings in a row I assumed I'd looked too early or the scan had found nothing. On the fourth morning I added a debug log line and watched the skill enter the interactive code path, print a prompt, and wait — at 9:03 AM, in an unattended terminal, for a keypress that was never going to come.

Just pass the flag. The skill reads it and behaves accordingly.

Context injection based on draft state. Before launching Claude, check gh-pending.md:

if [ "$pending_count" -gt 0 ]; then
  claude --system "You have $pending_count pending items. Run /gh-review."
else
  claude -p "/gh-review"
fi

The goal is a continuous transition: notification fires, you click, Claude opens already briefed — first message something like "You have 4 pending items from this morning's scan. Ready when you are." The else branch handles the no-pending case — Claude runs a fresh scan rather than opening to an empty briefing.

It works, except when the count is stale. Walk in, Claude announces twelve pending items; you cleared ten of them yesterday. It's a confident briefing about old news. The count is frozen at cron time, not now. Close enough for a morning habit.


Known limitations: macOS-only. Requires iTerm2 Accessibility permission. /tmp/cc-open-skill.txt is a shared file — two concurrent notifications would clobber each other (not a real problem: tasks run 5+ hours apart). crontab hardcodes the nvm path.

The cron skill ran this morning at 9:03. The alert appeared at 9:04. I clicked "Open CC," iTerm2 opened, Claude was already briefed.

The job ran while I wasn't paying attention, and when it needed me, it asked.


Same logic, I'm told, applies to flossing. I'll let you know how that goes.

没有评论: