A durable, governed cron scheduler for agent jobs, written in Rust.
codex-cron is a from-scratch reimplementation of the cron subsystem of
nousresearch/hermes-agent,
keeping its durability and safety behavior while making the executor pluggable.
By default a job's prompt is run by the OpenAI codex CLI; a job can also run a
raw shell command or a governed ao2 run.
It does the boring-but-hard parts of a scheduler correctly:
- At-most-once firing — a job's next run time is advanced and persisted to disk before the job runs, so a crash mid-run never re-fires it.
- No drift — interval jobs anchor each next run to the scheduled instant, not
to wall-clock "now", so
every 30mstays on the half hour forever. - No thundering herd — a job that fell behind (laptop asleep, machine off) fast-forwards to a single next occurrence instead of firing a burst of catch-up runs.
- Crash recovery — a job left mid-run by a killed process is reset on the next load.
- One writer at a time — every scheduling pass takes a cross-process file
lock, so the built-in daemon and an external
codex-cron tickcan coexist. - Durable writes — the job store is written to a temp file,
fsync'd, then atomically renamed; the home dir is0700and its files0600on Unix.
Requires a Rust toolchain (see rust-toolchain.toml).
cargo install --path crates/codex-cron-cli
# or, from a clone:
cargo build --release # binary at target/release/codex-cronFor the codex executor (the default), the codex CLI must be on PATH. The
shell executor needs nothing extra; the ao2 executor needs ao2 on PATH.
codex-cron doctor tells you what it can find.
# A daily agent job, run by `codex`, delivered to a per-run markdown file:
codex-cron add "0 9 * * *" "Summarize my unread email and list action items"
# A shell job every 30 minutes:
codex-cron add "every 30m" "disk check" --executor shell \
--script "df -h | tail -n +2"
# See everything, run one now, inspect it, remove it:
codex-cron list
codex-cron run <id>
codex-cron show <id>
codex-cron remove <id>
# Then either run the built-in loop…
codex-cron daemon --interval 60
# …or drive it from your OS scheduler once a minute:
# * * * * * codex-cron tickThe first argument to add (and edit --schedule) is one of:
| Form | Example | Meaning |
|---|---|---|
every <dur> |
every 30m, every 2h, every 1d |
Recurring interval |
| cron (5 or 6 field) | 0 9 * * *, */15 * * * * |
Recurring cron expression |
| bare duration | 2h, 90m, 1d |
Run once, at now + duration |
| ISO-8601 timestamp | 2026-06-01T09:00:00Z |
Run once, at that instant |
Durations accept m/min, h/hour, d/day (a trailing s is fine):
15m, 15min, 3h, 3hours, 2d, 2days.
Choose with --executor (default codex, configurable):
codex— runscodex exec [--model <m>] <prompt>. The prompt is the job's prompt, optionally prefixed with another job's latest output (see--context-from). The prompt is passed as a process argument, never through a shell.shell— runs the job's--scriptviash -c(Unix) orcmd /C(Windows).ao2— runsao2 run --spec <script>, where--scriptis a spec path or inline spec.
--script accepts @path/to/file to load the script/spec from a file.
A run is recorded with one of four statuses: success, failed, silent
(see the wakeAgent gate below), or refused (see prompt-injection guard).
Every run always writes a markdown record to
<home>/output/<id>/<timestamp>.md and appends a line to
<home>/output/<id>/runs.jsonl. This file sink is unconditional.
Add a webhook with --deliver:
codex-cron add "every 1h" "hourly report" \
--deliver webhook:https://hooks.example.com/abcWebhook delivery POSTs JSON {job_id, name, status, output_md, ts} with a short
bounded retry. --deliver is repeatable and also accepts file. With a
default_webhook configured you can write just --deliver webhook.
These mirror hermes-agent's guardrails:
- Prompt-injection scan —
codexprompts are screened for known injection phrases (e.g. "ignore previous instructions") and a match yields arefusedrun before any process is spawned. The assembled prompt — including any injected--context-fromoutput — is re-scanned in the executor as defense in depth. - wakeAgent gate — if a run's first stdout line is
{"wakeAgent": false}, the run issilent: nothing is delivered. This is the hermes "no-agent watchdog" convention for "I checked; nothing to report." - Webhook allowlist — set
webhook_allowlistto restrict webhook delivery to specific hosts (an SSRF guard). Empty means allow all.
Two ways to run on a schedule:
- Built-in loop:
codex-cron daemon --interval 60ticks every 60s. - OS scheduler: invoke
codex-cron tick(one pass, then exit) from cron, launchd, or Task Scheduler.
Install the built-in loop as a managed service:
codex-cron daemon install --interval 60 # launchd (macOS) / systemd --user (Linux)
codex-cron daemon uninstallOn Windows, daemon install prints the schtasks command to register it at
logon.
codex-cron can run a job as a bounded zero-wait event loop. This is different
from daemon --interval: the interval only decides when the first iteration is
eligible. Once started, an event-loop job immediately runs the next iteration
when its output emits:
{"schema_version":"codex-cron.event-loop-decision.v1","event_loop":{"action":"continue"}}The loop stops on stop, backoff, fail, command failure, max_chain_runs,
or max_runtime_seconds.
For long-running overnight/weekend chains, add --event-loop-goal-id <id> to
pin a loop to one durable goal. A continue decision that emits a different
goal_id stops the chain with goal_drift instead of letting the work wander
into a different objective.
For tools that write durable artifacts, configure
--event-loop-decision-file <path> instead of relying on stdout. Relative
decision-file paths resolve against the job --workdir; this lets AO2 Pulse
write target/pulse-next-recommended-tasks/codex-cron-event-loop-decision.json
and lets codex-cron consume the structured decision directly.
Example:
codex-cron add "every 30m" "AO2 Pulse production readiness" \
--executor shell \
--workdir /path/to/ao2 \
--script "npm run pulse:generate-next" \
--event-loop \
--event-loop-goal-id ao2-production-readiness \
--event-loop-decision-file target/pulse-next-recommended-tasks/codex-cron-event-loop-decision.json \
--max-chain-runs 3 \
--max-runtime-seconds 2700
codex-cron tick-loop
codex-cron daemon --event-loop --interval 60Evidence is written under:
~/.codex-cron/event-loop/<job-id>/latest.json
Each event-loop iteration receives these environment variables:
| Variable | Meaning |
|---|---|
CODEX_CRON_HOME |
Active codex-cron home directory |
CODEX_CRON_JOB_ID |
Current job id |
CODEX_CRON_EVENT_LOOP_SESSION_ID |
Stable id for the current zero-wait chain |
CODEX_CRON_EVENT_LOOP_ITERATION |
1-based iteration number inside the chain |
CODEX_CRON_EVENT_LOOP_GOAL_ID |
Configured goal id, or empty when unset |
CODEX_CRON_EVENT_LOOP_DECISION_FILE |
Resolved decision-file path when configured |
Decision JSON may include goal_id and memory_session_id:
{"schema_version":"codex-cron.event-loop-decision.v1","event_loop":{"action":"continue","goal_id":"ao2-production-readiness","memory_session_id":"mem-123","next_task_id":"next-check"}}For a detailed integration guide (e.g. with AO2 Pulse), see docs/examples/ao2-pulse-event-loop.md.
codex-cron config show | get <key> | set <key> <value> reads and writes
<home>/config.toml. Keys:
| Key | Default | Purpose |
|---|---|---|
default_executor |
codex |
Executor for add when --executor is omitted |
max_parallel |
0 |
Jobs run concurrently per tick (0 = CPU count) |
codex_path |
codex |
Path/name of the codex binary |
ao2_path |
ao2 |
Path/name of the ao2 binary |
default_webhook |
(unset) | URL used by --deliver webhook with no URL |
webhook_allowlist |
(empty) | Comma-separated allowed webhook hosts |
timezone |
UTC |
Informational display label |
Everything lives under $CODEX_CRON_HOME (default ~/.codex-cron):
~/.codex-cron/
jobs.json # the durable job store (schema-versioned)
config.toml # configuration
.tick.lock # cross-process scheduling lock
output/<id>/
<timestamp>.md # one markdown file per run
runs.jsonl # append-only run log
A two-crate Cargo workspace:
codex-cron-core— pure scheduling logic with no I/O: the schedule grammar, theJobmodel, the durabletickalgorithm, the injection scanner, and the trait boundaries (Clock,JobStore,Executor,Delivery,InjectionScanner). Keeping side effects out is what makes the durability properties deterministically testable with a fake clock and an in-memory store.codex-cron-cli— the effects and thecodex-cronbinary: the filesystem job store and lock, the three process executors, file and webhook delivery, the daemon, and the clap command surface.
Run the test suite and lints:
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warningscodex-cron is licensed under Apache-2.0. See LICENSE.