Skip to content

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

Description

@drpaulouti

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.

Activity

  1. added
    bugSomething isn't working
    has reproHas detailed reproduction steps
    area:routinesClaude Code routines on web, used for scheduled tasks, webhook-triggered tasks, etc.
    platform:macosIssue specifically occurs on macOS
    on Aug 25, 2026
  2. framework-optimization-lab commented on Aug 25, 2026

    @framework-optimization-lab

    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.

  3. Suchspezi commented on Aug 26, 2026

    @Suchspezi

    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"
    • /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 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 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 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.

  4. Suchspezi commented on Aug 26, 2026

    @Suchspezi

    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.

  5. Suchspezi commented on Aug 26, 2026

    @Suchspezi

    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 Scheduled tasks: add model parameter to create/update_scheduled_task API #57216 and [FEATURE] scheduled-tasks MCP: add model parameter to create_scheduled_task / update_scheduled_task #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.

  6. Suchspezi commented on Aug 26, 2026

    @Suchspezi

    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.

  7. Suchspezi commented on Sep 1, 2026

    @Suchspezi

    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-sessions space — exactly this issue. The scheduled-tasks MCP tools (create_scheduled_task/update_scheduled_task) still expose no model parameter (#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's modelUsage field) — unlike anything inside the Desktop app itself.

    Also tracked in support conversation 215475668068092. A maintainer response would be much appreciated.

  8. davidpistachio commented on Sep 4, 2026

    @davidpistachio

    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.json carry a model key.

    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-5
    C — pinned to Sonnet in the Edit form all but the first two claude-sonnet-5

    The clearest single datum: on 2026-08-31 at 06:02:03 all three tasks fired in the same second. Two ran claude-sonnet-5 and one ran claude-opus-5[1m]. Same build, same second, and no task carried a persisted model. 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.json held "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 at claude-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.

  9. drpaulouti commented on Sep 6, 2026

    @drpaulouti
    Author

    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-sessions workspace's scheduled-tasks.json carry a model key — 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 on claude-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_task MCP tool — it doesn't expose a model field, 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.

  10. drpaulouti commented on Sep 6, 2026

    @drpaulouti
    Author

    Follow-up to my comment above, with a concrete repro of the persistence layer itself.

    Manually pinned claude-sonnet-5 via the Edit form on 2 of the 26 affected tasks (jarvis-telemetry-push, claude-sessions-watchdog). Before: neither task had a model key in scheduled-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 description update, 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 for model at all — the full key set for every task is just id, cronExpression, enabled, filePath, createdAt, lastRunAt, lastScheduledFor, missedRunScanFloor, cwd. Notably description isn'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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:desktoparea:routinesClaude Code routines on web, used for scheduled tasks, webhook-triggered tasks, etc.bugSomething isn't workinghas reproHas detailed reproduction stepsplatform:macosIssue specifically occurs on macOS

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions