Scheduled Tasks: "model" field not persisted for claude-code-sessions space
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
- App: Claude Desktop (app.asar v1.34493.1)
- OS: macOS 15 (Darwin 25.5.0)
- Doc referenced: https://code.claude.com/docs/en/desktop-scheduled-tasks — "The instructions input includes pickers for the permission mode and model"
Steps to reproduce
- Open a scheduled task under a
claude-code-sessionsworkspace via the Edit form. - Pick a model (e.g. Claude Sonnet 5) and Save.
- 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.
5 Comments
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.
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 themessage.modelfield was counted, restricted toisSidechain=falseso 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"/status→model sonnet (claude-sonnet-5)/config→Model: sonnet,Fast mode (Opus 5): falseRuled out before filing
Switch models when a message is flaggedandContinue automatically at usage limitweretrueand were set tofalseon 2026-08-26. The regression is unchanged and predates that by 12 days.~/.claude/hooksand~/.claude/scripts/hooksfull-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..claude/settings.json,.claude/settings.local.json. Nomodelkey at project level; noANTHROPIC_MODEL/CLAUDE_MODELin the environment.%APPDATA%\Claude\claude-code-sessions\<ws>\<session>\scheduled-tasks.jsoncontains no model field at all (full-text search for "model": zero hits). This matches this issue's title.scheduledTaskModel/routineModelkey, thedesktop-scheduled-tasksdocs page says nothing about model choice, andcreate_scheduled_task/update_scheduled_taskhave nomodelparameter (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
RemoteTriggerAPI 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.
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_*.jsoncarries ascheduledTaskIdfield —nullfor 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
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
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
modelfield is never persisted for tasks in theclaude-code-sessionsspace. I can confirm the affected side on Windows: 71 tasks in%APPDATA%\Claude\claude-code-sessions\...\scheduled-tasks.json,modelpresent on 0 of them. I cannot confirm the working side — ourlocal-agent-mode-sessionsspace 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",/statusand/configboth confirming Sonnet, Fast mode off, auto-update channellatest.Happy to share the extraction scripts for either the
scheduledTaskIdattribution or the per-run model counting if that would help.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
modelfield is never persisted for tasks in theclaude-code-sessionsspace, scheduled tasks silently inherit that new account default and there is no supported way to override it. Not throughsettings.json("model": "sonnet"is ignored), not through/config, not through the MCP tools (create_scheduled_task/update_scheduled_taskhave nomodelparameter), 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
claude-code-sessionsspace (the substance of this issue).modelparameter tocreate_scheduled_task/update_scheduled_task— already requested in #57216 and #62337.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.
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.