[FEATURE] Scheduled tasks that run a command instead of a Claude session (zero usage unless they fail)

Status Open
Reported on v2.1.286
Maintainer reply None cached
Activity 0 comments · opened Oct 4, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request

Problem Statement

Every desktop scheduled task is a set of instructions for Claude, so every run starts a full Claude session. That's the right design when the task needs judgment. But many recurring jobs are just a script: refresh a data file, run a build, sync a folder, regenerate a dashboard. For those, starting a model session costs usage on every run and adds nothing.

It can also stop the job from running at all. I built a usage dashboard and scheduled an hourly refresh, a task whose whole job was to run ./refresh.sh. Its first run, at 8:01pm, failed with:

You've hit your session limit · resets 8:50pm (America/New_York)

Afterwards the task no longer existed: its run history reported taskDeleted: true. I can't tell whether the failure caused that. I ended up dropping the schedule and refreshing by hand. A dashboard meant to watch my usage was blocked because refreshing it used usage.

What a run costs before it does anything (measured from my own logs). I looked at the first model request of each session on my machine. That request carries the fixed context (system prompt, tool definitions, skill and plugin listings) before any task work happens:

| Session type | Sessions | First-request context |
|---|---:|---|
| Claude Code (Code tab, incl. subagents) | 6 | 60,755 – 88,313 tokens |
| Desktop / Cowork agent sessions | 45 | 151,628 – 162,836 tokens (median 151,794) |

Most of it is written fresh to the prompt cache each time. In the desktop sessions, about 132,000 tokens were cache writes and about 20,000 were cache reads. At the per-token rates in my own extra-usage billing, the first request alone comes to roughly $0.50–$0.60 for a Claude Code session and $1.30 for a desktop session. An hourly task would spend about $12–$32 a day on overhead before running a single command. That is a lower bound, since a real run makes further requests to call the tool and read its output.

Controlled test, with and without my setup. To separate the product's own overhead from my plugins, I ran one-turn headless sessions (claude -p "Reply with exactly: OK", Opus 5.5, Claude Code 2.1.286, empty folder). I repeated the baseline to check stability; it was 65,609 and 65,539 tokens.

| Configuration | Context before any work |
|---|---:|
| My full setup | ~65,600 tokens |
| --strict-mcp-config (no MCP servers) | 42,326 |
| --strict-mcp-config --disable-slash-commands (also no skills) | 39,584 |
| ... --setting-sources project (also no user plugins or settings) | 34,068 |
| Same, plus --tools "" (no tools) | 2,864 |

So a bare session with nothing installed carries about 34,000 tokens, about 31,000 of it built-in tool definitions. Everything else is setup-dependent. In my case MCP servers add ~23,000, plugin configuration ~5,500 and skills ~2,700.

What an actual script run costs. The same headless setup, asked to Run the shell command echo ok with the Bash tool, then reply with only its exit code:

| Configuration | Model requests | Input tokens processed | Reported cost* |
|---|---:|---:|---:|
| My full setup | 2 | 126,575 | $0.357 |
| No plugins, skills or MCP | 2 | 68,281 | $0.045 |
| Same, --tools Bash only | 3 | 14,345 | $0.028 |
| Same, --tools Bash --model haiku | 2 | 21,637 | $0.013 |

\*Reported costs benefit from prompt-cache hits left by the previous test runs. A cold run pays the cache write on its first request: at the per-token rate of my first cold run (about $8 per million cache-write tokens), that is roughly $0.28 per run for a bare session and $0.52 for my setup. An hourly script-only task would then cost about $7 a day bare or $12 a day with my setup, just to run echo. With a command task, it would cost nothing.

The last two rows also show the best workaround available today: restricting a scheduled task to the Bash tool, ideally on Haiku, cuts the overhead by roughly 80–95%. But that requires users to know about it, and it still starts a model session and still fails at the usage limit.

Where the overhead is wasted

Most of this overhead is a reasonable trade-off in a long interactive session: it is paid once, then served from the prompt cache at about 1/40 of the price on later turns. It becomes waste in three situations I measured:

  1. Short automated runs. A scheduled task that runs a script pays the cold-start cost on every run, ~34,000 tokens bare and ~65,600 with my setup, to execute one command. The model's actual contribution, deciding to call Bash and reporting the exit code, needs a few hundred output tokens. This issue fixes that case.
  2. Tools that a run cannot use. A script-only run carries ~31,000 tokens of built-in tool definitions it will never call. Restricting it to --tools Bash cut a real run from 68,281 to 14,345 tokens processed. Scheduled tasks have no setting that does this automatically.
  3. Connectors on every session. My MCP servers added ~23,000 tokens to a session whose only job was to reply "OK". That is about a third of my overhead, paid even when no connector is used.

Not measured here: desktop and Cowork agent sessions start at ~152,000 tokens in my logs, more than twice a full Claude Code session. I have not broken that down.

Charts

What a session carries before doing anything (my full setup, ~65,600 tokens, measured by switching parts off one at a time):

pie showData
    title Context per session, my full setup (tokens)
    "Built-in tool definitions" : 31200
    "MCP servers / connectors" : 23300
    "Plugin configuration" : 5500
    "System prompt + message" : 2900
    "Skills listing" : 2700

Input tokens processed by one "run echo ok" task:

xychart-beta
    title "Tokens processed per script run"
    x-axis ["Full setup", "Bare", "Bare, Bash only", "Bare, Bash only, Haiku"]
    y-axis "Input tokens" 0 --> 130000
    bar [126575, 68281, 14345, 21637]

Estimated cost of an hourly script-only task, per day. Cold-cache runs pay the full cache write. Warm runs reuse the cache, but still re-write ~3,800 tokens each time. A command task would be $0 (values in US cents: 1250 = $12.50):

xychart-beta
    title "Hourly script task: estimated US cents per day"
    x-axis ["Full setup, cold", "Bare Opus, cold", "Bare Opus, warm", "Bare Haiku, cold", "Command task"]
    y-axis "US cents per day" 0 --> 1400
    bar [1250, 670, 87, 170, 0]

Cache lifetime. I ran the bare session, waited 6 minutes 40 seconds, and ran it again. 30,290 of its 34,067 tokens were still served from cache. So the cache outlives 5 minutes, which is consistent with a 1-hour cache. I did not test the exact limit. Tasks more frequent than hourly therefore mostly pay the warm price. Hourly tasks sit at the edge, somewhere between the warm and cold figures above. About 3,800 tokens are written fresh on every run regardless.

Other things the measurements turned up

  • A headless run can start without its MCP connectors, silently. My first claude -p run after logging in measured 43,240 tokens. Every later run of the same command measured ~65,600 (65,609 and 65,539). The first figure matches the configuration with MCP turned off (42,326) to within ~900 tokens. So that run very likely started before my connectors were connected, and nothing in its output said so. For a scheduled task this matters: a run that fires right after login or wake could silently have fewer tools than the same task an hour later, and "found nothing" from a missing connector looks the same as "found nothing" from a working one. A command task would not depend on connector timing at all.
  • Cache reads across models. I ran the bare one-turn session on Opus, then immediately on Sonnet. The Sonnet run reported cache_read_input_tokens of 30,290, the identical count the Opus run had just written and read. I can't tell from outside whether that is cross-model cache reuse or a reporting artifact in usage. It affects how users should estimate costs when tasks run on different models, so it may be worth a look.
  • Haiku is cheaper per token, not smaller. A bare Haiku session carried 35,150 tokens, slightly more than Opus (34,065). On the Bash-only task, Haiku processed 21,637 tokens against Opus's 14,345, because the two models took different numbers of turns. It still cost less ($0.013 vs $0.028 in my runs). Switching a scheduled task to a cheaper model reduces the price of the overhead, not the overhead itself.
  • Cache writes cost about 40× cache reads. Working back from the total_cost_usd the CLI reported, Opus 5.5 came to $8.00 per million cache-write tokens and $0.20 per million cache-read tokens (a cold run: 43,238 written for $0.346; a warm run: 3,775 written and 30,290 read for $0.0363). Haiku 4.5 came to about $2.00 per million for cache writes. For comparison, the cost records of my 113 desktop sessions on Opus 5 fit $5 input / $25 output / $10 cache write / $0.50 cache read per million tokens exactly, with zero residual. That is a 20× write-to-read ratio on Opus 5 against 40× on Opus 5.5. These are the clients' own cost figures, not invoices. Either way, the first send of the session context is by far its most expensive, which is why the cold start dominates a short run.
  • For subscribers, this is real money. It is not only plan allowance. Between 20 July and 18 September, about 90% of what I paid for Claude was metered extra usage, not the subscription itself, as reconciled against my receipts. Once a subscriber is on extra usage, every cold-start token of a scheduled run is billed at those rates.
  • The 1-hour cache still applied while I was over my weekly limit. The settings docs describe the 1-hour prompt cache as applying "on a Claude subscription within its usage limits". My local usage history (~/Library/Application Support/Claude/plan-usage-history.json) shows the 7-day window at 100% for the whole of my test runs, so I was on extra usage. The cache still survived the 6m40s gap. So users on extra usage, who pay metered rates, do still get cache lifetimes beyond 5 minutes. That matters for estimating what an hourly task costs them.
  • Desktop sessions read a fixed ~19,700-token prefix from cache, even days apart. In 35 of my 45 desktop agent sessions (25 July – 7 August), the first request read exactly 19,723 tokens from cache. Eight more read 19,748, after what looks like an app update changed that prefix. That held for sessions started 28.7, 45.7 and 52.2 hours after the previous one, far beyond a 1-hour lifetime. I can't tell from outside whether other activity on my account kept it warm, or whether a common prefix stays cached. If it is the latter, it would be worth documenting, because only the remaining ~132,000 tokens of a desktop session are written fresh.
  • The desktop app records extra-usage spend locally. plan-usage-history.json samples the 5-hour (fh) and 7-day (sd) usage percentages every ~15 minutes. While on extra usage it also logs an undocumented xu value, which rose from 56.22 to 56.69 during my test runs and appears to track extra-usage dollars. Documenting these fields would let users build cost tracking like this themselves.
  • About 3,800 tokens are re-written on every run. Even fully warm runs wrote ~3,775 tokens to the cache on top of a 30,290-token cache hit. Prompt caching matches from the start of the prompt, so this is the tail that differs between runs: the message, plus whatever per-run details such as date or environment come after the cached prefix. It is small, but it means even a perfectly warm script run is never free: about $0.03 on Opus at my rates, or ~$0.87 a day for an hourly task.

Caveats: I have many plugins and skills installed, which makes this overhead larger than for a typical user. Runs exactly an hour apart may reuse some cache from the previous run. And plan users see this as usage-limit consumption rather than dollars. But the shape holds for everyone: every script-only run pays for a whole session's context, and a command task would pay nothing.

Proposed Solution

Let a scheduled task run a command instead of a prompt. It would get the same scheduling, notifications, run history and sidebar entry as today, with no model call unless something goes wrong.

How it could work, building on the current design:

  1. A command task type. Tasks are already stored as ~/.claude/scheduled-tasks/<name>/SKILL.md with YAML frontmatter. Add optional fields:

``yaml
---
name: refresh-ledger
description: Refresh the Token Ledger snapshot
command: ./refresh.sh # if present, run this instead of a Claude session
cwd: ~/Projects/token-ledger
timeout: 10m
on_failure: notify # notify | prompt | none
---
If on_failure is "prompt", this body becomes the instructions for a Claude
session that receives the failing command's exit code and last output.
`
With no
command`, the task behaves exactly as it does today.

  1. Run it like a hook. Claude Code already runs user-defined shell commands for hooks. A command task would reuse that path: your login shell, cwd, the same sandbox settings, and a timeout. It would capture the exit code, stdout and stderr.
  1. Record it in the existing run history. Each run records its exit code, duration and the last lines of output. list_task_runs and the sidebar's Scheduled section show them, so a failure is visible instead of silent.
  1. Only call Claude when it's useful. on_failure: prompt starts a Claude session only when the command exits non-zero, with the error output as context. Healthy runs cost zero tokens, and failed runs get diagnosed automatically.
  1. Let Claude create these too. Add an optional command parameter to create_scheduled_task and update_scheduled_task. Claude can then set up "run ./refresh.sh every hour" without making it a prompt.
  1. Approval. Show the exact command when the task is created and ask the user to approve it, as hook commands are reviewed today. Re-ask whenever the command changes.
  1. Never delete a task because a run failed. Whether or not it's related, a failed run should leave the task in place with the failure recorded.

Cloud routines could get the same command option, running in their environment's sandbox.

Why it helps beyond tokens

  • Scale. An hourly task is 24 sessions a day, about 720 a month, each just to start a script. With a command task, all of those cost nothing unless a run fails.
  • Reliability. Command tasks would keep running when the user is at their usage limit. Today that is exactly when prompt tasks fail, which is what stopped my hourly refresh.
  • Speed and predictability. A script runs the same way every time and finishes in seconds. A model session can stall on a permission prompt with nobody watching, take longer, or interpret the instructions slightly differently from one run to the next.
  • Less clutter. Routine successful runs would not each add a session to the sidebar's Scheduled section. Only failures would need attention.

Alternative Solutions

launchd or cron can run the script today, but then the job sits outside the app. There are no desktop notifications, no run history in the sidebar, Claude can't create or inspect it with the scheduled-task tools, and it doesn't work the same way across platforms. Tasks that run a prompt also already handle the app's wake and sleep behavior, which a separate cron job doesn't share.

Priority

Medium - Would be very helpful

Feature Category

Configuration and settings

Use Case Example

"Every hour, run ./refresh.sh in my dashboard folder. If it fails, start a Claude session to figure out why." Today that costs a Claude session every hour, and the job stops whenever I'm at my usage limit. With a command task, it costs nothing on a normal hour, and Claude is only called when there's actually a problem to solve.

View original on GitHub ↗