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!

Tags

CLI Go macOS Claude Code