Introducing croni: cron-like scheduling for macOS
TL;DR
I built croni, a small CLI that schedules recurring commands on macOS — launchd under the hood, cron on the surface. It's especially handy for scheduling claude -p prompts. Check it out at dsaiztc.com/croni.
Context
I wanted to run a command on a schedule. On my Mac. That's it. That was the whole ask.
On Linux I'd reach for cron without a second thought — one line in a crontab and I'd move on with my life. But on macOS the blessed path is launchd. And launchd wants a plist. In XML.
So to run one command every six hours I'm supposed to hand-write something like this:
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.me.backup</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>rsync -a ~/docs /backup/</string>
</array>
<key>StartInterval</key>
<integer>21600</integer>
</dict>
</plist>
…drop it in ~/Library/LaunchAgents/ with exactly the right filename, then launchctl bootstrap gui/$(id -u) ... it (and remember the incantation to reload it after every edit). I always have to look it all up. Every single time.
Wouldn't it be nice if I could just say "run this, every six hours" and be done?
What if it just looked like cron
That's the whole idea behind croni. I know cron. You know cron. So let's keep that on the surface and generate the plist underneath.
The plist above becomes:
croni add backup --every 6h --command "rsync -a ~/docs /backup/"
That's it. croni writes the plist, loads it into launchd, and the job now survives reboots. Prefer real cron syntax? Use --cron "0 9 * * MON-FRI". Just want it to fire once and clean itself up? --at 20m.
And because the job's metadata lives in plain JSON at ~/.croni/jobs.json, the rest of the commands you'd expect just fall out of it: croni list, croni logs backup, croni disable backup, croni remove backup. No more wondering whether a job is actually loaded, or hunting for where its output went.
The part I actually wanted it for
The honest reason I finally built this: I wanted to schedule Claude.
claude -p "..." runs a prompt headlessly and prints the result — no interactive session. Which means a prompt is just… a command. And a command on a schedule is exactly what croni does.
So now, every weekday morning before I sit down, a job runs inside my main work repo and drafts my standup:
croni add standup --cron "0 8 * * MON-FRI" \ --workdir ~/src/work/api \ --command "claude -p 'Summarize my git commits and open PRs from yesterday'"
The --workdir flag matters here: it runs the prompt inside the right repo, so Claude has the context it needs. From there it's a short hop to a triage digest every couple of hours, a nightly pass over a project for stale TODOs, or a weekly changelog draft. Anything you'd type as a prompt, you can now put on a timer.
Made for scripts and agents too
I designed croni to be driven by an agent from the start, following the kind of agent-friendly CLI conventions Peter Steinberger has written about. Every command takes a --json flag, so the output is machine-readable rather than something you have to scrape:
croni add my-job --every 1h --command "echo hi" --json
# {"status":"ok","job":{...}}
Failures come back as a {"status":"error","error":"..."} object too, and croni exits non-zero when something goes wrong — so an agent can branch on the result instead of guessing. Nothing ever blocks on a prompt either: the single confirmation, on croni remove, notices when there's no terminal and fails fast rather than hanging (or you pass --force).
Which closes a fun little loop: an AI agent can schedule — and inspect, and tear down — its own recurring work.
Try it
It's a single Go binary, installable via Homebrew:
brew tap dsaiztc/tap brew install croni
Head to dsaiztc.com/croni for the full command reference and a handful of recipes to copy-paste.
Let me know if you find it useful!