Scheduled Tasks: "model" field not persisted for claude-code-sessions space

Status Open
Maintainer reply None cached
Activity 5 comments · opened Aug 25, 2026

Summary

Pinning a model via the Scheduled Tasks Edit form does not persist for tasks stored under the claude-code-sessions workspace. Tasks keep launching on the app's current default model instead of the pinned one, even after repeated manual edits.

Environment

Steps to reproduce

  1. Open a scheduled task under a claude-code-sessions workspace via the Edit form.
  2. Pick a model (e.g. Claude Sonnet 5) and Save.
  3. Inspect ~/Library/Application Support/Claude/claude-code-sessions/<uuid>/<uuid>/scheduled-tasks.json.

Expected

The saved task object contains "model": "claude-sonnet-5" — as it correctly does for tasks under local-agent-mode-sessions (14/14 tasks there persist model fine, e.g. "model": "claude-haiku-4-5-20251001").

Actual

model is absent for every task in the claude-code-sessions space (0/32), even after repeating the Edit-form flow twice, on two different tasks — file mtime updates within minutes of each save, but the model key never appears.

Additional finding

Manually writing "model" directly into scheduled-tasks.json doesn't survive either: the app rewrites the whole file from in-memory state on the next update_scheduled_task call or task run — even a trivial unrelated field change — silently discarding the manual edit.

Impact

Tasks fire on the app's current default model instead of the intended one, which can mean unexpectedly expensive models (e.g. Opus/Fable-tier) running dozens of times a day instead of the cheaper model the user explicitly selected.

View original on GitHub ↗

5 Comments

framework-optimization-lab · 5 days ago

Adding a Windows data point that looks like a sibling of this bug. Windows 11 Home, Claude Code 2.1.232. Global ~/.claude/settings.json sets "model": "claude-fable-5"; verified no override anywhere (no ANTHROPIC_MODEL, no project/local settings model key, no model key in the desktop app config). Scheduled-task sessions launch non-deterministically on either the configured model or the app default: the identical task run 4× on 2026-08-24 with config unchanged produced claude-fable-5 (10:11), claude-opus-5[1m] (11:30), claude-fable-5 (12:44, 12:45). Two other scheduled tasks also launched on claude-opus-5. Interactive sessions in the same window launched on claude-fable-5 every time — the fault is isolated to the scheduled-session launch path. Unlike the macOS report above, the fallback here is intermittent (~half of runs) rather than consistent.

Suchspezi · 4 days ago

Additional data point from Windows 11 desktop, with a full-population measurement rather than anecdotes. The regression appears to have a start date: 2026-08-14.

Method

Scheduled-task runs were extracted from the session transcripts under ~/.claude/projects/<project>/*.jsonl (a run is a transcript containing the <scheduled-task name= prompt marker). For each assistant message the message.model field was counted, restricted to isSidechain=false so that subagents — which legitimately run on other models — cannot skew the result. Population: 379 scheduled-task runs.

Results

| Model of the run's first message | Runs |
|---|---|
| claude-opus-5 | 340 (89.7 %) |
| claude-sonnet-5 | 22 (5.8 %) |
| claude-haiku-4-5 | 10 (2.6 %) |

| Pattern within a run | Runs |
|---|---|
| entirely Opus, never switches | 279 (73.6 %) |
| Opus → Sonnet | 55 |
| entirely Sonnet | 22 |
| Sonnet → Opus | 1 |

Switch direction matters here: a switch towards Opus essentially never happens. Opus is the starting state, not the destination of a switch.

The regression has a date

All 22 Sonnet runs fall between 2026-08-12 and 2026-08-14. From 2026-08-14 onward, every one of 340 consecutive runs starts on Opus, without a single exception across 13 calendar days. That rules out load balancing, capacity fallback or randomness — it is a state change at a point in time.

I could not find a changelog entry around 2026-08-14 that touches model resolution for scheduled tasks. Auto-update channel is set to latest.

Configuration (verified clean)

  • ~/.claude/settings.json"model": "sonnet", modelSettings."claude-sonnet-5".effortLevel: "medium"
  • /statusmodel sonnet (claude-sonnet-5)
  • /configModel: sonnet, Fast mode (Opus 5): false

Ruled out before filing

  • Both auto-switch settingsSwitch models when a message is flagged and Continue automatically at usage limit were true and were set to false on 2026-08-26. The regression is unchanged and predates that by 12 days.
  • Own hooks~/.claude/hooks and ~/.claude/scripts/hooks full-text searched for "opus": only entries in a local price table, nothing that writes a model setting. The hook output schema has no model field, so a hook cannot steer model choice.
  • All settings layers — user, project .claude/settings.json, .claude/settings.local.json. No model key at project level; no ANTHROPIC_MODEL/CLAUDE_MODEL in the environment.
  • App state%APPDATA%\Claude\claude-code-sessions\<ws>\<session>\scheduled-tasks.json contains no model field at all (full-text search for "model": zero hits). This matches this issue's title.
  • No supported lever exists — the settings schema has no scheduledTaskModel/routineModel key, the desktop-scheduled-tasks docs page says nothing about model choice, and create_scheduled_task/update_scheduled_task have no model parameter (see #87624).

UI observation

On a manual start the run displays "Sonnet 5" for roughly 3–6 seconds and then switches to "Opus 5". The raw transcript shows Opus from the very first message, so the initial display seems to show the configured value before catching up to the model actually in use. Worth noting, because it makes the symptom easy to misread as a mid-run switch when it is in fact the wrong model from message one.

Contrast worth flagging

Cloud routines created via the RemoteTrigger API do accept an explicit model: job_config.ccr.session_context.model. The capability exists in the product — local scheduled tasks simply have no equivalent, and no configuration layer they honour.

Impact

On a single day (2026-08-26) across 70 runs: 1908 of 4906 main-loop messages ran on Opus (38.9 %) on a setup configured for Sonnet throughout. For anyone running scheduled tasks at scale this is a silent, ongoing cost difference with no user-visible signal that anything is wrong.

Happy to provide the extraction script or the per-run breakdown if that helps narrow down what changed on 2026-08-14.

Suchspezi · 4 days ago

Follow-up to my earlier comment: the regression is now pinned to a single global cutover inside a 6.5-hour window on 2026-08-14 — not a recurring pattern, not per-task configuration, not quota exhaustion. All three of those were tested and ruled out.

A better way to separate cron runs from interactive ones

My earlier numbers relied on a transcript heuristic. There is an exact method: %APPDATA%\Claude\claude-code-sessions\<workspace>\<cliSessionId>\local_*.json carries a scheduledTaskId field — null for interactive sessions, set for scheduled-task runs. That removes the guesswork entirely, and anyone reproducing this should use it rather than parsing transcripts.

Result over 368 exactly-attributed cron sessions (64 distinct tasks), all before 2026-08-26 17:00 UTC

| | |
|---|---|
| Tasks running entirely on Opus | 51 |
| Tasks running entirely on Sonnet | 0 |
| Mixed tasks | 13 |
| Anomalies across 362 runs | 0 |

Every one of the 13 mixed tasks shows the same single transition: Sonnet up to 2026-08-14, Opus afterwards, never back.

The cutover

  • Last cron run starting on Sonnet: 2026-08-14 06:49 UTC
  • First Opus message in any cron run: 2026-08-14 13:09:39 UTC

I verified this independently of the analysis that produced it (different method, same result).

One precision, because it is easy to misread: "Opus from then on, never back" holds at the run-start level. At the message level, Sonnet still appears inside cron runs after 2026-08-14 (3,263 messages between 08-15 and 08-25) — those come from the mid-run Opus→Sonnet switches described in my first comment. The two levels measure different things; conflating them makes the finding look self-contradicting when it isn't.

What this rules out

  • Per-task model configuration — actively ruled out, not merely unproven. No task behaves differently from the rest; there is no task that stayed on Sonnet.
  • A recurring time-of-day pattern — an earlier analysis suggested an 18:13–18:19 UTC window. That was an artifact of the metric (the last Opus call of a day naturally lands near the end of that day's activity). It does not survive the per-task attribution above.
  • Quota exhaustion — the switch point is not tied to consumption. Across seven days the cumulative Opus token count at the moment of switching ranged from 6,355 to 1,782,403 (factor ~280), while the wall-clock time stayed within minutes of itself. A quota would show the opposite.

Client releases in the window

v2.1.232 (2026-08-13) and v2.1.233 (2026-08-14) both fall inside or immediately before the window. Neither changelog mentions model resolution, default model, fallback behaviour, or scheduled tasks — as far as I could check. That negative finding is worth stating: if the cause were a client change, I would expect it to be visible there.

How this fits your original diagnosis

Your report is that the model field is never persisted for tasks in the claude-code-sessions space. I can confirm the affected side on Windows: 71 tasks in %APPDATA%\Claude\claude-code-sessions\...\scheduled-tasks.json, model present on 0 of them. I cannot confirm the working side — our local-agent-mode-sessions space contains no tasks at all, so there is nothing there to compare against.

That fits what I measured, and I think the two findings compose into one story: if the field never persists, every scheduled run falls back to whatever the default resolution yields — and that default is what changed on 2026-08-14. Before the cutover the fallback landed on the configured Sonnet; after it, on Opus. The persistence bug is the precondition; the cutover is what turned it from harmless into expensive.

If that reading is right, fixing persistence alone would resolve it for anyone who sets a model explicitly — but users who never touched the model setting would still be exposed to whatever the fallback resolves to.

Which leaves the question I cannot answer from the client side:

Was anything changed server-side on 2026-08-14 between 06:49 and 13:09 UTC that affects default model resolution or fallback for background / scheduled-task sessions, independently of the 2.1.232 / 2.1.233 client releases?

Environment unchanged from my first comment: Claude Code Desktop, Windows 11, ~/.claude/settings.json"model": "sonnet", /status and /config both confirming Sonnet, Fast mode off, auto-update channel latest.

Happy to share the extraction scripts for either the scheduledTaskId attribution or the per-run model counting if that would help.

Suchspezi · 4 days ago

Correction to my previous comment — I found the trigger, and it is not a server-side change.

I asked what changed server-side on 2026-08-14. The answer is much simpler: the account was upgraded to the Max plan that day. A commit in my own notes from 2026-08-14 22:52 CEST records the upgrade explicitly. I should have checked that before speculating, and I'm correcting it here so nobody spends time chasing a phantom release change.

The forensics line up precisely:

| Time (UTC), 2026-08-14 | Event |
|---|---|
| before 13:06 | account upgraded to Max |
| 13:06:40 | an interactive session answers on Opus for the first time — before any cron run |
| 13:09:39 | first Opus message in a scheduled-task run |
| from then on | 340 consecutive cron runs, all starting on Opus |

The interactive session flipping first is the tell: this is account-level default model resolution, not something specific to scheduled tasks.

I also checked whether a client update happened in that window — not determinable. The updater logs on this machine only start on 2026-08-20; the whole update directory was recreated six days after the fact. So v2.1.232 / v2.1.233 can be neither confirmed nor excluded as a factor, but they are no longer needed to explain anything.

Why I still think this is worth fixing

The plan upgrade raised the account's default model from Sonnet to Opus. That by itself is reasonable — arguably it is what you'd want. The problem is the interaction with the bug in this issue:

Because the model field is never persisted for tasks in the claude-code-sessions space, scheduled tasks silently inherit that new account default and there is no supported way to override it. Not through settings.json ("model": "sonnet" is ignored), not through /config, not through the MCP tools (create_scheduled_task / update_scheduled_task have no model parameter), not through the task file. I checked all of them.

So the user-visible behaviour is: you upgrade your plan, and 70 unattended background jobs silently switch to a model costing 2.5× more, with no notification and no way to opt out. On this machine that ran for twelve days before it was noticed, at roughly $1,500 in avoidable spend over the last seven of them.

That makes the persistence bug in this issue considerably more consequential than "the model picker doesn't stick" — it is the only lever a user would have, and it doesn't work.

Suggestions, for whatever they are worth

  1. Fix persistence for the claude-code-sessions space (the substance of this issue).
  2. Add a model parameter to create_scheduled_task / update_scheduled_task — already requested in #57216 and #62337.
  3. Consider surfacing a notice when a plan change alters the default model that existing background tasks will run on. The silence is what made this expensive.

Everything else in my two earlier comments stands — the measurement method, the 368 attributed sessions, and the fact that 51 tasks ran entirely on Opus with none entirely on Sonnet. Only the attribution of the cause was wrong, and that is now corrected.

Suchspezi · 4 days ago

Additional full-population measurement (Windows 11 desktop), narrowing down the timing.

For every one of 379 local scheduled-task runs since 2026-08-12, I extracted the model of the run's first main-agent message (isSidechain=false) together with its timestamp, grouped by scheduledTaskId (64 distinct tasks).

Result: this isn't a recurring daily pattern — it's a single, permanent, global switch. Last Sonnet run: 2026-08-14T06:49:09Z. First Opus run: 2026-08-14T13:09:39Z. Zero exceptions across all 64 tasks and 362 runs in that window: every task that ever ran Sonnet stopped doing so at that point and never switched back, and no task ran Opus before it. That rules out per-task configuration (a config-based split would leave some tasks permanently on Sonnet) and matches @drpaulouti's persistence-bug finding: the "model" field isn't written back for tasks under the claude-code-sessions workspace, so whatever model the app defaults to at task-fire time is what runs — and something changed that default around 2026-08-14T06:49–13:09 UTC.

Happy to share the extraction script if useful for reproducing this on another machine.