Warm your Claude Code 5-hour usage window on a schedule so the dead time lands off-hours — and stagger several accounts if you rotate them.
Project description
cwarm
Warm your Claude Code 5-hour usage window on a schedule, so its dead
regeneration time lands outside your working hours instead of stranding you
mid-flow. cwarm sends a tiny "Hi" at the times you choose to anchor the window
early — and can stagger several accounts too, if you rotate them with
claude-swap (cswap).
cwarm stores no credentials — claude-swap owns your account and tokens;
cwarm only triggers the warmup.
Why I built it
I kept hitting the same wall: my Claude Code 5-hour window starts on the first message and resets exactly 5 hours later — and I burn through its budget in about 2 hours, then sit idle for the remaining 3 waiting for the reset. Start cold at 8 AM and that dead stretch lands around 10 AM–1 PM, right in the middle of my day.
The fix: send a throwaway "Hi" before I start. Fire it at 5 AM and the window runs 05:00–10:00 — so I work 8–10, exhaust it, and the reset is already there; the 3-hour gap happened before I sat down instead of mid-morning. Re-warm at the reset times and the windows stay anchored to clock times I choose, so the idle stretches land on lunch instead of mid-flow. It doesn't create more budget — it just moves the dead time out of my way.
I kept meaning to fire that ping every morning and kept forgetting. cwarm just does it, on a schedule. (It handles several accounts too, if you rotate them — that's just not how I use it.)
How it works
A Claude Code 5-hour window starts on your first message and resets exactly 5 hours later. It's a fixed budget, not free capacity — warming only relocates the dead/regeneration time. Send a warmup "Hi" at 05:00 and the window runs 05:00–10:00, so the idle stretch happens before work instead of mid-morning. Give an account several warmup times to re-anchor each new window to clock times you pick, dropping the dead gaps onto your breaks.
Multiple accounts (optional). If you keep more than one account in
claude-swap, cwarm warms each on its own staggered schedule — switching to it,
sending the message, then restoring whichever account you had active before
(always, even if a warmup fails, so running it never leaves your setup changed).
Staggered resets mean there's always a fresh account to switch into.
A weekly cap is shared across web, app, and Claude Code; cwarm does not track it.
Install
With uv (recommended):
uv tool install cwarm # installs the `cwarm` command, isolated
# or run it without installing:
uvx cwarm --help
Or with pipx / pip:
pipx install cwarm
# or
pip install cwarm
Requires Python 3.12+ (uv/pipx handle that for you).
Quick start
cwarm init # writes a starter config to ~/.config/cwarm/config.json
$EDITOR ~/.config/cwarm/config.json
cwarm validate # checks your config + that cswap/claude are set up; sends nothing
cwarm list # shows each account, whether it's warm, and the next run
cwarm run # warm everything now (optional sanity check)
cwarm daemon # leave running to warm on schedule
cwarm reads ~/.config/cwarm/config.json by default (override with --config),
so the commands above work from any directory.
You also need, for each account you list:
claude-swapinstalled, with the account added (cswap --add-accountorcswap --add-token sk-ant-oat01-…),- Claude Code (
claude) installed.
Configuration (config.json)
No tokens. Accounts are referenced by their cswap handle — a slot number
or email. The simplest config is a single account warmed before work:
{
"defaults": {
"message": "Hi",
"timezone": "Asia/Kolkata",
"settle_seconds": 3,
"skip_if_warm": true
},
"accounts": [
{ "id": "1", "schedules": ["0 5 * * 1-5", "0 10 * * 1-5", "0 15 * * 1-5"] }
]
}
That warms account 1 at 05:00, 10:00, and 15:00 on weekdays, anchoring each new
5-hour window to those clock times. An account can warm at several times a
day — give it a schedules array — or use the singular schedule string for
one time. Each cron time fires in the account's timezone.
Rotating multiple accounts? Add more entries on staggered times so a fresh window is always available to switch into:
"accounts": [
{ "id": "1", "schedule": "0 5 * * 1-5" },
{ "id": "2", "schedule": "30 7 * * 1-5" },
{ "id": "3", "enabled": false, "schedule": "0 10 * * *" }
]
| Field | Required | Default | Notes |
|---|---|---|---|
id |
yes | — | unique; the account's cswap slot number or email |
schedule / schedules |
yes | — | one (string) or many (array) of 5-field cron times, read in the account's timezone |
enabled |
no | true |
false skips the account entirely |
message |
no | "Hi" |
the warmup message |
timezone |
no | Asia/Kolkata |
IANA tz name |
settle_seconds |
no | 3 |
delay after switching before sending |
skip_if_warm |
no | false |
skip if the window is already open (saves usage) |
agent |
no | claude |
which coding agent warms this account (currently claude) |
Your config lives at ~/.config/cwarm/config.json (it holds your real account
ids — keep it out of version control). Point anywhere else with --config.
Commands
cwarm init # write a starter config.json
cwarm validate # check config + setup; sends nothing
cwarm list # accounts, live window state, next run
cwarm run # warm all enabled accounts now
cwarm run --account work@example.com # warm just one
cwarm daemon # long-lived; fires each account on its cron
# global flags
cwarm --config /path/to/config.json --log-file /path/to/cwarm.log <command>
validate— confirms the config is valid,cswap/claudeare installed, and every configuredidexists. Exits non-zero and sends nothing on a problem.list— read-only table: account, enabled, live window state (warm/cold), next scheduled run, cron times. Sends nothing.run— warms enabled accounts immediately. Non-zero if any warmup failed.daemon— schedules one job per account per cron time. Warmups are serial.
Logging
Every attempt emits one structured line to stderr (and the log file if set):
2026-06-21T05:00:03+0530 INFO save-active switcher=cswap account=work@example.com ref=1
2026-06-21T05:00:09+0530 INFO warmup account=work@example.com outcome=ok reset=10:00
2026-06-21T05:00:10+0530 INFO restore-active switcher=cswap account=work@example.com ref=1 outcome=ok
Outcomes: ok (with the window reset time), failed (with an error
summary), skipped (already warm, with skip_if_warm).
Running it on a schedule
systemd (Linux)
A sample unit ships in systemd/cwarm.service; it reads
the default ~/.config/cwarm/config.json that cwarm init writes.
mkdir -p ~/.config/systemd/user ~/.local/state/cwarm
cp systemd/cwarm.service ~/.config/systemd/user/ # adjust the cwarm path if needed
systemctl --user daemon-reload
systemctl --user enable --now cwarm
loginctl enable-linger "$USER" # run without an active login session
journalctl --user -u cwarm -f
It starts on boot and restarts on failure. On (re)start the schedule is rebuilt
from config.json; a warmup missed by under an hour still fires once on
recovery, but a warmup missed across a long power-off is skipped (firing a 05:00
warmup at noon would defeat the staggering).
Alternative: cron
One line per warmup time (cwarm must be on PATH):
0 5 * * 1-5 cwarm --config ~/.config/cwarm/config.json run --account work@example.com
0 11 * * 1-5 cwarm --config ~/.config/cwarm/config.json run --account work@example.com
30 7 * * 1-5 cwarm --config ~/.config/cwarm/config.json run --account 2
macOS / Windows
cwarm run/daemon are cross-platform — only the boot-persistence recipe
differs. On macOS use launchd or cron; on Windows use Task Scheduler (or run
cwarm daemon as a service). The tool needs claude and cswap available on
that OS.
Tips
skip_if_warm: trueis the biggest usage saver — it skips the wholeclaude -pcall (a real LLM request) when an account's window is already open.- Stagger, don't stack. Overlapping schedules waste warmups and the shared weekly cap; one warmup per window per account is enough to anchor it.
- The daemon doesn't poll — it sleeps until the next job (~25 MB RAM, ~0% CPU idle).
Safety
- No credentials stored or logged —
claude-swapowns them. - Switching changes your active account momentarily; the save/restore guarantees your default is unchanged after a run, so prefer off-hours anyway.
- Only configure accounts you legitimately own or are authorised to use.
License
MIT — see LICENSE. Project internals and contribution notes live in CLAUDE.md.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cwarm-0.1.1.tar.gz.
File metadata
- Download URL: cwarm-0.1.1.tar.gz
- Upload date:
- Size: 23.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3725fcf3aab339ece1e6b1895f4a038d2a7c79e0b2eae04d5ef51a77a539df44
|
|
| MD5 |
cf53ad44f4dcb58fef5be8fe4fee54b2
|
|
| BLAKE2b-256 |
667960b81faecff166acc4361a1a4475627157defc79a9d2a1062fab2e321eac
|
Provenance
The following attestation bundles were made for cwarm-0.1.1.tar.gz:
Publisher:
publish.yml on wonderbyte/cwarm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cwarm-0.1.1.tar.gz -
Subject digest:
3725fcf3aab339ece1e6b1895f4a038d2a7c79e0b2eae04d5ef51a77a539df44 - Sigstore transparency entry: 1894502795
- Sigstore integration time:
-
Permalink:
wonderbyte/cwarm@524e8e4c581e123ed4043567b35972d08d8ca87f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/wonderbyte
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@524e8e4c581e123ed4043567b35972d08d8ca87f -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file cwarm-0.1.1-py3-none-any.whl.
File metadata
- Download URL: cwarm-0.1.1-py3-none-any.whl
- Upload date:
- Size: 19.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea771ceae2a81fc3690bd61e447f1a5cdec0126ff1a5546945b77aec04492781
|
|
| MD5 |
de1ed1b0abcf3ca95019b89be4b1e3f8
|
|
| BLAKE2b-256 |
5c33d01295457f230d770ac5dd84bc817d6d7c20cb6db0b10d77b93e3361c069
|
Provenance
The following attestation bundles were made for cwarm-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on wonderbyte/cwarm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cwarm-0.1.1-py3-none-any.whl -
Subject digest:
ea771ceae2a81fc3690bd61e447f1a5cdec0126ff1a5546945b77aec04492781 - Sigstore transparency entry: 1894502938
- Sigstore integration time:
-
Permalink:
wonderbyte/cwarm@524e8e4c581e123ed4043567b35972d08d8ca87f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/wonderbyte
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@524e8e4c581e123ed4043567b35972d08d8ca87f -
Trigger Event:
workflow_dispatch
-
Statement type: