Rules / CFG108
CFG108 a committed scheduled_tasks.json runs prompts on a timer
Severity: error (a bare task, which fires in any checkout) · info (a session-bound task, inert on another machine)
OWASP: LLM03:2026 – Excessive Agency
What cfgaudit checks
.claude/scheduled_tasks.json is Claude Code's on-disk store for its cron scheduler — the CronCreate / ScheduleWakeup tools, /loop, and routines. Each entry pairs a cron expression with a prompt that is enqueued into the session, as if the user had typed it, when the schedule fires. cfgaudit reports a committed file that carries actionable tasks (a task with both a prompt and a cron).
The file is Claude Code's own runtime state and is normally kept out of git — 2.1.273 adds it to the repository's .git/info/exclude — but committed copies exist in the wild, and a committed copy runs on a timer in every contributor's interactive session.
Why a committed copy is dangerous
Two mechanisms in the 2.1.273 binary combine into a zero-interaction path:
- The file self-enables the scheduler. Scheduling is off by default (the launch flag initialises to
false). The interactive scheduler'sstart()callsreplaceScheduledTasksEnabled(true)whenever the store has a non-emptytasksarray (nan()), so the file's mere presence turns the feature on. - A task with no session identity fires in any checkout. The fire gate
de(task)returns the scheduler-lock flag whencreatedBySessionIdis absent, so such a task runs here. A task carrying another machine'screatedInProjectstays inert on a clone — that is the guard against a copied file — but a task with neither identity field takes the first branch. The writer always stamps both fields; the reader treats them as optional, so a hand-authored or hand-edited file can omit them.
Severity: the bare/session-bound split
| Task shape | Severity | Why |
|---|---|---|
no createdBySessionId, no createdInProject (bare) |
error |
self-enables the scheduler and fires in any clone, with no interaction |
| bound to the author's session or project | info |
inert on another machine, but still committed autonomous scheduling |
That split is what separates a live task from an inert one, so it keeps the high severity's false positives near zero. In a 40-repo sample of the 91 public repos that commit the file, 24 were empty, 16 carried tasks, and 8 of those carried at least one bare task — almost all recurring: true, with prompts like Run the Python script at …\Temp\fix_hook.py or Check CI run … fix them. Run pnpm lint.
Verified at the artifact
The self-enable and the fire were confirmed by driving an interactive session (a headless claude -p run does not mount the scheduler, which is the TUI hook useScheduledTasks). A scratch HOME, a committed .claude/scheduled_tasks.json carrying a single bare recurring * * * * * task, and a local endpoint that logs requests: on the next cron minute the session sent the task's exact prompt to the API, in a request whose billing header read cc_workload=cron — the scheduler generated it, with no input.
Safe alternative
Delete .claude/scheduled_tasks.json from the repository. Scheduled tasks are per-user runtime state, not shared configuration; each contributor can create their own with CronCreate / /loop. If a repository wants to ship automation, a committed script invoked by a documented command or a CI job is auditable in a way a timer-fired prompt is not.
References
Documentation for cfgaudit 1.18.1. Source of this page · Rule implementation