croni

A friendly CLI for scheduling recurring commands on macOS — without ever touching plist XML or launchctl.

What it does

croni wraps macOS launchd behind a cron-like interface. Schedule recurring commands — especially claude -p "..." prompts — that survive reboots, without writing a single line of plist XML.

croni add backup --every 6h --command "rsync -a ~/docs /backup/"

Each job is stored as plain metadata in ~/.croni/jobs.json and compiled into a launchd agent under ~/Library/LaunchAgents/. Jobs run via croni _exec, which captures stdout/stderr logs and cleans up one-shot jobs after they fire.

Schedule types

croni add <name> --cron "0 9 * * MON-FRI" --command "..."

Standard 5-field cron expressions with full support for ranges, lists, steps, named months (JAN-DEC), named weekdays (MON-SUN), and shortcuts like @daily, @hourly, and @weekly.

croni add <name> --every 5m --command "..."

A simple recurring interval — 5m, 2h, 1d — when you don't want to think in cron fields.

croni add <name> --at 20m --command "..."

A one-shot job that fires once and then auto-removes itself. Accepts a relative offset (20m, 2h) or an absolute ISO 8601 timestamp (2026-05-13T15:00).

Commands

croni add <name>

Creates a new scheduled job. Beyond the schedule and --command, you can set a --workdir, repeatable --env KEY=VAL variables, a --description, and create it --disabled or with --run-on-load. --run-on-load is the default for --every jobs — they run once as soon as they're loaded (including at each login), then on their interval. Pass --run-on-load=false to wait for the first interval instead.

croni list
croni show <name>

List every job at a glance, or drill into the full configuration of a single one.

croni run <name>
croni edit <name>

Force-run a job now (synchronously, so you see the output) or modify an existing job in place.

croni logs <name>

Tail a job's output. Pass --stderr for the error stream, --tail N to limit lines, or --follow to stream live.

croni enable <name>
croni disable <name>

Unload a job from launchd without deleting it, then load it back later — handy for pausing something temporarily.

croni remove <name>
croni export <name>

Remove a job (with --force and --with-logs), or dump the generated launchd plist to stdout if you want to see exactly what croni produced.

Agent-ready

croni is built to be driven by scripts, CI, and AI coding agents — not just typed by hand. It follows agent-friendly CLI conventions: predictable machine-readable output, honest exit codes, and never blocking on a prompt that isn't there. Which closes a fun little loop — an agent can schedule, inspect, and tear down its own recurring work.

JSON on every command

croni add my-job --every 1h --command "echo hi" --json
# {"status":"ok","job":{...}}

The global --json flag wraps every command's output in a structured envelope you can pipe into jq instead of scraping text. Failures come back the same way — a {"status":"error","error":"..."} object — so an agent can parse success and failure alike.

Never hangs on input

Commands run to completion without waiting on a terminal. The one confirmation prompt, croni remove, detects when stdin isn't a TTY and fails fast with a clear message instead of blocking — or pass --force to skip it outright.

Honest exit codes

croni exits non-zero whenever something fails, including croni run when the underlying command fails — so a script or agent can branch on the result rather than guessing from stdout.

Recipes

Now that you've seen the pieces, here's how they come together. The sweet spot is anything recurring that should just keep happening in the background — especially claude -p prompts.

Daily standup prep

croni add standup --cron "0 8 * * MON-FRI" \
  --workdir ~/src/work/api \
  --command "claude -p 'Summarize my git commits and open PRs from yesterday'"

Let Claude write your standup before you sit down. --workdir runs it inside the right repo, so it has the context it needs.

Periodic PR / inbox triage

croni add triage --every 2h \
  --command "claude -p 'Review my open PRs and notifications, write a digest to ~/triage.md'"

A lightweight agent that keeps a running digest for you, refreshed through the day. Note that --every jobs run immediately on add (and at each login) by default — add --run-on-load=false if you'd rather wait for the first interval.

Nightly backups

croni add backup --cron "@daily" \
  --command "restic backup ~/Documents ~/src"

A classic cron job that actually survives reboots, with stdout/stderr captured so you can confirm it ran with croni logs backup.

Weekly system maintenance

croni add brew --cron "@weekly" \
  --command "brew update && brew upgrade && brew cleanup"

Keep Homebrew fresh without remembering to. Check croni logs brew to see what changed.

Local service health check

croni add health --every 5m \
  --command "curl -fsS http://localhost:8080/health || say 'service is down'"

An interval check that pings you the moment something stops responding. Any command works — croni doesn't care what language it's written in.

One-shot reminders

croni add tea --at 4m --command "say 'Your tea is ready'"

Fire once and clean itself up — nicer than a sleep loop because it survives closing the terminal. Use a relative offset or an absolute time like --at "2026-07-08T15:00".

Install

Via Homebrew:

brew tap dsaiztc/tap
brew install croni

Or with Go:

go install github.com/dsaiztc/croni@latest
CLI Go macOS Agent-ready Developer Tools Open Source