Repository navigation
Scheduled Tasks: "model" field not persisted for claude-code-sessions space #89439
Description
Activity
- addedbugSomething isn't workingSomething isn't workinghas reproHas detailed reproduction stepsHas detailed reproduction stepsarea:routinesClaude Code routines on web, used for scheduled tasks, webhook-triggered tasks, etc.Claude Code routines on web, used for scheduled tasks, webhook-triggered tasks, etc.platform:macosIssue specifically occurs on macOSIssue specifically occurs on macOS
on Aug 25, 2026 framework-optimization-lab commented
on Aug 25, 2026 More actionsAdding 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-5340 (89.7 %) claude-sonnet-522 (5.8 %) claude-haiku-4-510 (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): false
Ruled out before filing
- Both auto-switch settings —
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. - Own hooks —
~/.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. - All settings layers — user, project
.claude/settings.json,.claude/settings.local.json. Nomodelkey at project level; noANTHROPIC_MODEL/CLAUDE_MODELin the environment. - App state —
%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. - No supported lever exists — the settings schema has no
scheduledTaskModel/routineModelkey, thedesktop-scheduled-tasksdocs page says nothing about model choice, andcreate_scheduled_task/update_scheduled_taskhave nomodelparameter (see Desktop scheduled tasks: docs say "ask Claude" can change a task's model, but update_scheduled_task MCP tool has no model parameter #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
- 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
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
- Fix persistence for the
claude-code-sessionsspace (the substance of this issue). - Add a
modelparameter tocreate_scheduled_task/update_scheduled_task— already requested in Scheduled tasks: addmodelparameter to create/update_scheduled_task API #57216 and [FEATURE] scheduled-tasks MCP: add model parameter to create_scheduled_task / update_scheduled_task #62337. - 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.
- Fix persistence for the
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.
Status update (2026-09-01): still unfixed in the latest releases — Claude Code CLI v2.1.252 (2026-08-31) and Desktop v1.40609.0 (2026-08-27). Neither changelog mentions scheduled-task model persistence.
The docs now describe a model picker in the scheduled-task edit form (https://code.claude.com/docs/en/desktop-scheduled-tasks), but the selected model is still not persisted for the
claude-code-sessionsspace — exactly this issue. The scheduled-tasks MCP tools (create_scheduled_task/update_scheduled_task) still expose nomodelparameter (#87624), and the auto-closed feature request #62337 was never implemented.Impact on my side (Max plan, ~50 desktop scheduled tasks): measured ~$1,546.81 API-equivalent extra compute over 7 days from runs starting on the account default instead of the configured model (methodology in my earlier report in this thread). My current workaround is Windows UI automation that clicks the in-app model switcher on every task start — clearly not sustainable.
As a hard workaround I've now verified that running the job outside the app via an OS scheduler +
claude -p "<prompt>" --model <id>reliably pins the model (confirmed via the result JSON'smodelUsagefield) — unlike anything inside the Desktop app itself.Also tracked in support conversation 215475668068092. A maintainer response would be much appreciated.
Independent reproduction on Claude Desktop 1.46388.2 with claude-code 2.1.260, macOS 25.6.0. Same space, same missing field: 0 of 3 tasks under
claude-code-sessions/<account>/<org>/scheduled-tasks.jsoncarry amodelkey.I scanned 91 scheduler-spawned runs across 21 days and 4 routines, reading the model from each run's own transcript. The runs split by task, and each split holds for weeks:
Task Runs measured Model recorded A — created 2026-08-29, left on "Default" 9 of 9 claude-opus-5[1m]B — pinned to Sonnet in the Edit form ~50 consecutive claude-sonnet-5C — pinned to Sonnet in the Edit form all but the first two claude-sonnet-5The clearest single datum: on 2026-08-31 at 06:02:03 all three tasks fired in the same second. Two ran
claude-sonnet-5and one ranclaude-opus-5[1m]. Same build, same second, and no task carried a persistedmodel. That rules out a time-based or build-based cause.The Edit-form pin is what separates them, and losing it takes effect at once. Tasks B and C ran Sonnet for weeks while pinned. I switched both to "Default" on 2026-09-04, and the next run of each took
claude-opus-5[1m], the app's sticky pick at that moment.~/.claude/settings.jsonheld"model": "sonnet"throughout, and no run ever resolved to it. This is the impact note in this issue, measured: task A is my heaviest reader, and it had run 1M-context Opus nine times unattended before I noticed.One diagnostic note for anyone else chasing this. The resolved model, including the
[1m]context suffix, appears in the app's own per-session record atclaude-code-sessions/<account>/<org>/$CLAUDE_CODE_HOST_SESSION_ID.json. No config file records that suffix, so the session record is the only place to read what the app actually spawned.Additional macOS data point, same signature as the original report.
Environment: Claude Desktop, macOS, checked 2026-09-06.
Confirmed 0 of 26 tasks under my
claude-code-sessionsworkspace'sscheduled-tasks.jsoncarry amodelkey — every task, from tasks firing 4x/day down to monthly ones, is missing it. Verified two of the highest-frequency ones via session metadata: every recorded run for both launched onclaude-opus-5, consistently across the last several days — none ever landed on the account's configured default model (Sonnet).Real-world impact beyond cost: we run a local PreToolUse hook that hard-denies Opus/Fable tool calls outside an explicitly sandboxed directory (a cost-control policy). Once that policy went live, every one of these 26 scheduled tasks started failing outright — every tool call denied — because they launch on Opus by fallback rather than the model actually configured for the account. The only available workaround was a directory-wide authorization exception, which of course defeats the purpose of the policy but was the only lever available to keep the automations running at all.
Also confirming: there's no programmatic workaround via the
update_scheduled_taskMCP tool — it doesn't expose amodelfield, so the only entry point is the (non-persisting) Edit form.Still reproducing today, 2026-09-06 — 12 days after the last comment on this thread, with no maintainer response yet. Given the concrete cost exposure this causes (unpinned tasks silently running dozens of times a day on the most expensive tier), it'd be great to get this triaged.
Follow-up to my comment above, with a concrete repro of the persistence layer itself.
Manually pinned
claude-sonnet-5via the Edit form on 2 of the 26 affected tasks (jarvis-telemetry-push,claude-sessions-watchdog). Before: neither task had amodelkey inscheduled-tasks.json. After saving in the UI: still neither — the file's mtime hadn't even changed.To rule out a sync-timing issue, I forced a rewrite through the scheduler's own update tool (a no-op
descriptionupdate, same value in/out) and re-read the file immediately after. The write did go through, but here's the useful part: the on-disk schema for a task record in this file has no slot formodelat all — the full key set for every task is justid, cronExpression, enabled, filePath, createdAt, lastRunAt, lastScheduledFor, missedRunScanFloor, cwd. Notablydescriptionisn't in there either, even though it's shown in the task list UI — it must be sourced from elsewhere (the task's own prompt file frontmatter, presumably).So this isn't an intermittent write failure or a race between the Edit form and a background flush — the field the Edit form's model picker writes to (wherever that is) simply isn't the same store that gets persisted to
scheduled-tasks.json, and nothing in that file's schema currently has room for it. That's consistent with every report in this thread showing a flat 0-of-N regardless of OS, app version, or how many times the picker is used.
Summary
Pinning a model via the Scheduled Tasks Edit form does not persist for tasks stored under the
claude-code-sessionsworkspace. Tasks keep launching on the app's current default model instead of the pinned one, even after repeated manual edits.Environment
Steps to reproduce
claude-code-sessionsworkspace via the Edit form.~/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 underlocal-agent-mode-sessions(14/14 tasks there persistmodelfine, e.g."model": "claude-haiku-4-5-20251001").Actual
modelis absent for every task in theclaude-code-sessionsspace (0/32), even after repeating the Edit-form flow twice, on two different tasks — file mtime updates within minutes of each save, but themodelkey never appears.Additional finding
Manually writing
"model"directly intoscheduled-tasks.jsondoesn't survive either: the app rewrites the whole file from in-memory state on the nextupdate_scheduled_taskcall 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.