[BUG] macOS: recurring scheduled tasks advance lastRunAt but never launch a session; one-off tasks in the same minute do launch
What's Wrong?
On macOS, recurring scheduled tasks advance lastRunAt at each fire but no agent session is ever created — no transcript, no tool calls, no side effects. One-off tasks on the same machine, in the same minute, launch normally.
This is the same shape as #60658 (macOS, closed as stale/not-planned and now locked — the bot asked that a new issue be filed referencing it) and appears to be the macOS counterpart of #83253 (Windows: fires reliably only in the catch-up burst after app launch, then goes silent). It is the inverse of #89936, where lastRunAt never advances; here it advances every time.
Environment
- Claude Code 2.1.205, Claude.app 1.49585.0
- macOS 26.6.2, arm64
- 8 recurring tasks (daily/6-hourly cron) + occasional one-offs, via
mcp__scheduled-tasks__*
How this was measured
The decisive signal is the birth time of session transcripts in ~/.claude/projects/<project>/:
for f in *.jsonl; do stat -f '%SB %N' -t '%F %T' "$f"; done | sort
mtime is not usable: the scheduler resumes/reattaches to old transcripts, and an attended session opened in an old transcript updates its mtime too. Several old files were modified on the affected days while containing zero new turns — read by mtime they look like successful runs.
Evidence — 10 Sep 2026
Sessions created: exactly 2, both born 05:59:54Z, both one-off tasks. Zero created by any of the 7 recurring tasks that fired:
| Task (recurring) | lastRunAt | Session created? |
|---|---|---|
| flow-sentinel | 03:09:01.008Z | no |
| ops-digest | 03:09:00.995Z | no |
| funnel-watch | 03:09:01.017Z | no |
| e2e-skipnames | 03:25:00.979Z | no |
| revenue-followup | 03:45:01.005Z | no |
| marketing-drafts | 04:16:00.971Z | no |
| bug-triage | 05:59:43.317Z | no |
Cleanest control: the recurring bug-triage stamped 05:59:43.317Z and created nothing; two one-off tasks stamped 05:59:48Z and had transcripts on disk 6 seconds later. Same minute, same scheduler, opposite outcome — so this is not machine sleep, not time-of-day, and not cron parsing. The three tasks stamping within 22 ms of each other also look like a catch-up burst rather than three independent fires.
No task produced any side effect (expected file, git commit, or DB row) on the affected days.
Two distinct failure modes in recordedSkips
~/Library/Application Support/Claude/claude-code-sessions/<id>/<id>/scheduled-tasks.json has a recordedSkips map. On 6–9 Sep it recorded refusals with reasons global_limit and per_task_limit (e.g. revenue-followup 132 entries, funnel-watch 109, e2e-skipnames 69). Those refusals appear to be counter-based and to persist in-memory: an app restart cleared them transiently.
On 10 Sep recordedSkips is empty for every task, yet lastRunAt still advanced on all 7 recurring tasks and still nothing launched. So there is a second path where the fire is consumed and marked as run without either launching a session or recording a skip.
Timeline
Sessions created per day in this project (birth time): 8–12/day from 4 Aug through 5 Sep, then 6 Sep: 0, 7 Sep: 0, 8 Sep: 6 (all within ~25 min of an app restart), 9 Sep: 0, 10 Sep: 0 recurring.
Throughout, every task shows healthy enabled: true, a valid cronExpression, and an advancing nextRunAt. Task SKILL.md files are all present and non-empty. The UI intermittently shows "Routine file not found or has an unexpected format", but that string does not appear anywhere in the app bundle and the files load fine.
Things tried that did not fix it: re-spreading the cron schedules; archiving ~230 stale sessions; restarting Claude.app (recovers only for minutes-to-hours, matching #83253); converting a task's symlinked SKILL.md to a regular file.
What Should Happen?
lastRunAtshould advance only when a session actually starts — otherwise it is a silent false-positive, exactly as argued in the #60658 comments. Enqueue / start / complete should be distinguishable.- A fire that is refused or dropped should be visible in the UI, with its reason, rather than being indistinguishable from a successful run.
- Whatever budget causes
global_limit/per_task_limitshould be inspectable, and should not silently consume a fire.
Reproduction
Not reproducible on demand — it appears after some hours of app uptime and affects recurring tasks only. The observable is: recurring task's lastRunAt advances, and no transcript is created in ~/.claude/projects/<project>/ at that time.