No cron, just stamps
· Türkçe
Some work comes back every week. A report to write up, a standup update, a service to check on. Claude Code does all of it fine, I’ve got a skill for each one. The part that kept breaking was me remembering it was due.
So, cron. Or launchd really, since this is a Mac. You write a plist, pick a time, and that’s supposed to be that.
Except my laptop is shut at 9 on Monday morning. Sometimes I’m on a plane, sometimes Friday was off and I’m not back yet. launchd has StartCalendarInterval and it’s meant to catch up after a wake, but I’ve sat there and watched it not do that. The worse part is that nothing tells you. The job didn’t run, nobody mentions it, and you find out on Thursday when someone asks.
It took me a while to say the obvious thing out loud. A timer only knows whether it fired. It has no idea whether the work actually happened. Those are two different facts. They agree most weeks, which is why I’d never had to separate them.
The stamp
So I stopped scheduling anything. There’s just a question now, asked over and over.
When a job finishes it writes a file named after itself with one string in it, the period it just did.
2026-W35
That’s all the state in the whole thing. To find out whether this week is still owed, you work out what week it is and compare the two strings.
period_key() {
case "$1" in
weekly) date -v-7d +%G-W%V ;;
monthly) date -v-1m +%Y-%m ;;
daily) date -v-1d +%Y-%m-%d ;;
esac
}
The comparison is one line:
[[ "$dn" == "$per" ]]
$dn is what’s in the stamp file, $per is where we are now. Match means done, no match means owed.
Nothing can miss a trigger, because nothing triggers. You can reboot, shut the lid for a week, go away on holiday. When you come back the stamp still says last week and the row is still amber. There’s nothing to get out of sync either, since the only state is that one string and the rest gets worked out from the calendar every time anyone looks.
The date -v-7d is easy to skip over. %G-W%V gives you the ISO week, and taking seven days off first means you’re asking about the week that just closed rather than the one you’re standing in. You can’t owe a report for a week that hasn’t ended.
Nothing runs any of this, either. SwiftBar just prints a shell script’s output into the menu bar, and if you name the script something.2m.sh it reruns it every two minutes. So the question gets asked 720 times a day and costs nothing.
Why it opens a Terminal window
Every AI scheduler I looked at runs headless. That was the dealbreaker, so I wrote my own.
One of my skills asks me questions before it does anything. It shows me a draft and waits. Run that under claude -p and it hangs on the first question, or worse, exits zero having done nothing at all, which stamps the period as done and then quietly blocks the auto-trigger for the rest of the week.
So a job says which mode it wants. Headless is claude -p in the background with an explicit --allowedTools list. Interactive opens a real session in Terminal, and when the period comes round the plugin opens that window for me. I answer a few questions, it exits zero, the row goes green.
The cadence that didn’t fit
There’s one more cadence, interval, for health checks. Run every N minutes. It has no period, so no stamp, so none of what I just described applies to it. I gave it a .lastrun timestamp instead and moved on.
Then, the bug. .lastrun was only ever written by the automatic trigger. So if I ran an interval job by hand from the menu nothing recorded it, and two minutes later the next refresh saw an old timestamp and fired the same job again, right as my manual run was finishing.
Fixing it took a few lines. The part that stuck with me is that the one cadence I bolted on outside the model is the only one that has ever gone wrong. The rest keep their state in a single string in a single file, and I haven’t had to think about them since.
It’s on GitHub, MIT, around 600 lines of shell. Honestly there isn’t much to it — the stamp file is the whole idea, the rest is menu plumbing.