CronCreate recurring tasks fire at a fixed per-session phase offset, not at the wall-clock cron time (hourly `0 * * * *` fires 15 to 28 min past the hour, and the offset scales with the period)

Status Open
Reported on v2.1.205
Maintainer reply None cached
Activity 0 comments · opened Aug 3, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

<html>
<body>
<!--StartFragment--><!-- obsidian --><p>Recurring tasks created with CronCreate do not fire at the wall-clock times described by their cron expression. Each task fires at a fixed offset from the correct slot. That offset equals the session start phase and is re-randomized whenever the session is restarted. The offset scales with the period, reaching ~15 to 28 minutes for an hourly <code>0 </code> task. The inter-fire interval itself is correct for periods of 10 min and above. Only the phase, meaning the alignment to the wall clock, is wrong.</p>
<p>Three isolated sessions (one per version), each running five recurring <code>date >> logfile</code> tasks (periods 5/10/20/30/60 min), all created between 14:16 and 14:23 local. The 2.1.205 session ran uninterrupted until the tasks hit CronCreate's 7-day auto-expiry, yielding a full week of data:</p>

period | fires | offset from wall-clock slot | spread over 7 days | median interval | expected
-- | -- | -- | -- | -- | --
0 | 168 | +23.3 to +25.3 min | 2.0 min | 3600 s | 3600 s
*/30 | 337 | +9.2 to +10.1 min | 0.9 min | 1800 s | 1800 s
*/20 | 505 | +3.8 to +5.3 min | 1.5 min | 1200 s | 1200 s
*/10 | 1009 | -4.7 to +1.6 min | 6.3 min | 600 s | 600 s
*/5 | 2115 | precesses through the grid | full grid | 286 s | 300 s

<p>No version ever fires at :00. The offset is a fixed per-session phase, stable across days and nights, and it is re-seeded when the session restarts.</p>
<h3 data-heading="Mechanism">Mechanism</h3>
<p>Next fire = <code>session_start_phase + N * period</code>, seeded at session start or task creation, without snapping to the cron expression's wall-clock boundary. The offset therefore equals the session phase and scales with the period.</p>
<p>Live confirmation of re-phasing: the 2.1.217 session was restarted mid-run, and its hourly offset jumped from +29 min to +15 min and then stayed there. Restarting a session re-seeds the phase of every recurring task.</p><!--EndFragment-->
</body>
</html>

What Should Happen?

0 * * * * should fire at HH:00:00 and */20 * * * * at :00, :20 and :40. Documented jitter cap is small (<= 15 min, or 10% of the period).

Concretely: a task created with 0 * * * * should fire within a few seconds of HH:00:00, regardless of when the session was started, and restarting the session should not change the firing times.

Error Messages/Logs

There are no error messages. This is a timing bug, so the evidence is the fire timestamps themselves. Raw sample from the hourly task (`0 * * * *`, version 2.1.205), first and last fires of a 7-day run:


Fri Jul 24 15:23:17 CEST 2026
Fri Jul 24 16:23:17 CEST 2026
Fri Jul 24 17:23:17 CEST 2026
...
Fri Jul 31 12:23:17 CEST 2026
Fri Jul 31 13:23:17 CEST 2026
Fri Jul 31 14:23:17 CEST 2026


168 fires, none at :00.

Steps to Reproduce

  1. Create a recurring task 0 * * * * (and/or */20 * * * *) that appends date to a file.
  2. Let it run a few hours.
  3. Fires cluster at a fixed offset from :00 equal to the session phase, not at :00. Restart the session, and the offset changes.

Recurring tasks auto-expire after 7 days, which is what bounded this measurement window.

Claude Model

Not sure / Multiple models

Is this a regression?

No, this never worked

Last Working Version

2.1.220

Claude Code Version

2.1.220

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

Field observation from before the controlled test

Before running the controlled A/B, I had a daily report scheduled with 0 7 * * * in a long-running session. Its actual fire times, taken from the wrapper script's own log:

  • 2026-06-28 to 07-14: 07:00:15 through 07:00:30 every single day (the seconds are script startup, not scheduling)
  • 2026-07-16 to 07-24: 07:36, 07:30, 07:30, 07:30, 07:18, 07:33, 07:29, 07:30
  • from 07-25 onward: back to 07:00, but by then I had replaced CronCreate with an external launchd trigger

So it did fire on time for about two weeks and then stopped doing so. I am deliberately not filing this as a regression, for two reasons.

First, I cannot name a last working version. I did not record which build was running on 07-14, and the three versions I tested under controlled conditions (2.1.205, 2.1.217, 2.1.218) all show the same wrong behaviour.

Second, the phase model explains the change without any version difference. If the session that owned the task happened to start close to a full hour, the task appears to fire on time. A session restart in mid-July would re-seed the phase and move every fire to :30. I observed exactly that re-phasing live during the test, when the 2.1.217 session was restarted mid-run and its hourly offset moved from +29 min to +15 min.

I mention this because it is what put me onto the bug, not as evidence for a regression.

This is NOT a version regression

We initially suspected a regression (field logs suggested an old 0 7 report fired at 07:00, a newer one at 07:30). A controlled A/B on 2.1.205 vs 2.1.217 vs 2.1.218 refutes that: all three behave identically (fixed per-session phase, never :00). The differing offsets (+23 / +15 / +28) are just different session start times, not version differences. The old field observation is NOT reproduced under controlled conditions and should not be relied upon. (Note: 2.1.205 is installable via claude install 2.1.205, so this A/B is repeatable.)

Why this is a real bug, not documented jitter

  • Observed offsets exceed the documented cap (<= 15 min, or 10% of the period). */10 at +1 min against 1 min allowed is borderline, but */20 at +7:54 (allowed 2 min), */30 at +13:46 (allowed 3 min) and hourly at +23 to +28 min (allowed 6 min) blow past it.
  • Docs (per #56106) describe the jitter as "derived from the task ID", meaning deterministic and stable. The observed offset re-randomizes on session restart, which contradicts a task-ID-derived jitter.

Ruled out

  • Claude Code version: identical on 2.1.205 / 2.1.217 / 2.1.218.
  • OS version: persisted across a macOS update during the investigation period.
  • Host: same behavior in a tmux pane and in a plain foreground shell (a separately bash-hosted session drifts too), so it is not a tmux or background artifact.
  • Model: same on haiku (tests) and opus (the original job).
  • Cron "durability": the durable flag is a documented no-op. Every CronCreate job is session-only, so there is only one cron type. The originally-affected daily report used the exact same session-only mechanism as these tests.

Not tested

  • Headless / --print (non-interactive) mode.
  • Non-macOS platforms (Linux / Windows).
  • Timezones other than the local one.

Impact and workaround

Any time-critical scheduled task such as a daily report, reminder or sync fires at the wrong wall-clock time, and the error grows with the period. Current workaround: an external launchd StartCalendarInterval trigger that injects the prompt via tmux send-keys into the live session (OS-accurate, version-independent).

Related issues

  • #56106 (closed as not planned): same 30-min offset, hypothesised a session-listener polling cadence phase-locked to session start. This report adds a controlled multi-version, multi-period measurement, refutes the version-regression angle, and shows restart re-phasing.
  • #50911 (durable:true ignored), #36131 (fires only on focus), #44128 (not when app closed): possibly related scheduling issues.

View original on GitHub ↗